Compare commits

...
35 Commits
Author SHA1 Message Date
dcrubro 2d43a65f1f update cmakelists 2026-09-05 17:51:04 +02:00
dcrubro af7ed86424 update cmakelists 2026-09-05 17:48:21 +02:00
dcrubro 3dcb73aaec suppress the weird cast-to-int warnings in geo.c when doing (int)floor() 2026-09-04 18:02:14 +02:00
dcrubro 623d121c05 Add /dxcc command for converting a callsign into a DXCC entity - optional grid argument calculates distance from 2026-09-03 16:10:13 +02:00
dcrubro c7d2052fac make percision conversion explicit in utils.c 2026-09-03 15:42:58 +02:00
dcrubro fcf510afa4 Fix warnings on clang (literally 95% implicit conversion to char* from const char*)# 2026-09-02 09:07:16 +02:00
dcrubro 3d6b08d9d5 Fix warning - regarding missing prototypes and sign conversions 2026-09-01 19:20:50 +02:00
dcrubro cca190e2f4 update cmakelists and gitignore - fanalyzer and ignore build dirs 2026-09-01 19:02:42 +02:00
dcrubro 7ca1f80c01 update readme 2026-09-01 18:29:07 +02:00
dcrubro d7b9582cc0 Add /abbr command - converts abbreviations to meanings and contexts from sqlite 2026-09-01 18:20:27 +02:00
dcrubro ee045cd1dd Update /phonetic to be generic with truncation above 12 characters 2026-09-01 17:45:06 +02:00
dcrubro ae5389a7d5 Add /phonetic command for converting Callsign (might expand to generic text) to phonetics + pronunciation 2026-09-01 15:10:48 +02:00
dcrubro cab60bbd57 test phonetics 2026-09-01 15:02:30 +02:00
dcrubro 923ddc5bd2 codeeeeecs 2026-09-01 14:32:22 +02:00
dcrubro 44d13ab071 Add /q command for converting Q-Code (case-insensitive) to Question/Answer combination 2026-08-31 22:04:07 +02:00
dcrubro c056b1b67a add /freq command - allows lookup of data regarding ham bands for region-specific details 2026-08-31 20:15:06 +02:00
dcrubro 7acb2aabd4 sql commands for querying the frequency bands and segments 2026-08-31 13:19:50 +02:00
dcrubro 2aa0ae3d9a morse command 2026-08-31 01:26:33 +02:00
dcrubro 7b5514bf67 Add SQLite handler (refdb) and hook into master/workers/job -> commands 2026-08-31 00:18:31 +02:00
dcrubro 7e4e738c19 SQLite building for data 2026-08-30 21:05:46 +02:00
dcrubro 35591b0701 Add registration (needs leak fix), remove something from ping 2026-08-30 17:11:36 +02:00
dcrubro 9b7ce082c2 add header 2026-08-30 16:16:34 +02:00
dcrubro 177528cf60 update readme 2026-08-29 20:47:24 +02:00
dcrubro faa73838c0 embeds in responses 2026-08-29 20:34:39 +02:00
dcrubro 57ff0d0378 Fix command registration and add command infra 2026-08-29 18:46:38 +02:00
dcrubro ee3aa1e488 fix crash on macos for dev 2026-08-28 23:31:23 +02:00
dcrubro ecc792bcb6 command 2026-08-28 23:01:03 +02:00
dcrubro e6eba80150 commands 2026-08-28 22:57:07 +02:00
dcrubro 9ea5d69cc1 threading infra 2026-08-28 22:46:58 +02:00
dcrubro 97edfc59fb add types.h 2026-08-28 21:45:34 +02:00
dcrubro b72676659a workers 2026-08-28 21:36:07 +02:00
dcrubro 01741ba375 readme changes 2026-08-28 00:44:28 +02:00
dcrubro 73809eaa7c some infra 2026-08-27 21:47:03 +02:00
dcrubro 7c2a2aecd1 base logon 2026-08-27 20:26:34 +02:00
dcrubro 80a413efdc test build 2026-08-27 20:14:05 +02:00
93 changed files with 27173 additions and 14 deletions
+5
View File
@@ -1,3 +1,8 @@
.env
build/
build*/
.DS_Store
config.json
.vscode
.vscode/
sqlite/*.sqlite
+167 -11
View File
@@ -198,7 +198,7 @@ else()
-Wshadow
-Wconversion
-Wsign-conversion
-Wcast-qual
#-Wcast-qual
-Wcast-align
-Wstrict-prototypes
-Wmissing-prototypes
@@ -215,6 +215,7 @@ else()
-Wundef
-Winit-self
-Wmissing-include-dirs
-Wno-discarded-qualifiers # Temp
)
if(CMAKE_C_COMPILER_ID MATCHES "GNU")
@@ -245,7 +246,9 @@ else()
hammy_append_supported_c_flags(HAMMY_ANALYZER_FLAGS
-fanalyzer-verbosity=${ANALYZER_VERBOSITY}
-fanalyzer
)
--param=analyzer-checker=taint
--Wanalyzer-too-complex
)
endif()
elseif(CMAKE_C_COMPILER_ID MATCHES "Clang")
# Clang-only diagnostics. Clang has no in-compiler equivalent of
@@ -316,6 +319,7 @@ else()
-fsanitize=bounds-strict
-fno-sanitize-recover=undefined
-fno-omit-frame-pointer
-fno-sanitize=function
)
endif()
endif()
@@ -336,7 +340,14 @@ if(HAMMY_ENABLE_LTO)
endif()
# ---------------------------------------------------------
# Concord (Discord API wrapper), fetched from the dev branch
# Concord (Discord API wrapper), pinned to the v3.0.1 release
#
# Pinned rather than tracking dev: dev reverted the notifier's portable fcntl
# setup back to ioctl(FIONBIO), cast to int. macOS FIONBIO is 0x8004667E, so the
# cast goes negative, sign-extends into ioctl's unsigned long parameter and the
# call fails -- ccord_global_init() then dies before the client is ever built.
# The release tags still carry the fcntl version. Check that the regression is
# gone before moving this back to a branch.
#
# Concord ships a hand-written Makefile, not a CMake build, so this is a
# two-stage arrangement: FetchContent clones it at configure time (recursively,
@@ -357,7 +368,7 @@ include(ExternalProject)
FetchContent_Declare(
concord
GIT_REPOSITORY https://github.com/Cogmasters/concord.git
GIT_TAG dev
GIT_TAG v3.0.1
GIT_SHALLOW TRUE
GIT_PROGRESS TRUE
)
@@ -387,25 +398,147 @@ else()
# which is before Concord has been built.
file(MAKE_DIRECTORY "${CONCORD_INCLUDE_DIR}")
# Upstream's `make install` is three shell globs relative to the working
# directory, fed to install(1). When one matches nothing the shell passes it
# through literally and install reports `cannot stat 'include/*.h'`, which
# says nothing about which glob mattered or why. Do the copy ourselves with
# absolute paths and name the two failure modes that actually occur.
#
# The globs run at install time rather than configure time because
# generated/discord_codecs.h does not exist until the build has emitted it.
#
# Keep the directory list below in step with the `install:` target in
# upstream's Makefile; v3.0.1 renamed gencodecs/ to generated/ and split
# reflect-c.h out into its own directory.
set(CONCORD_INSTALL_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/concord_install.cmake")
file(CONFIGURE
OUTPUT "${CONCORD_INSTALL_SCRIPT}"
@ONLY
CONTENT [[
cmake_policy(SET CMP0057 NEW)
file(MAKE_DIRECTORY "@CONCORD_INCLUDE_DIR@/concord" "@CONCORD_PREFIX@/lib")
# Upstream flattens all three header directories into one include/concord. Check
# them one at a time: a single empty directory is the interesting failure, and a
# combined glob would hide it behind whichever siblings still matched.
set(_hdrs "")
foreach(_dir include core generated)
file(GLOB _found "@concord_SOURCE_DIR@/${_dir}/*.h")
if(NOT _found)
message(FATAL_ERROR
"Concord: no headers in @concord_SOURCE_DIR@/${_dir} -- the source "
"tree is incomplete; remove build/_deps and reconfigure to re-clone")
endif()
list(APPEND _hdrs ${_found})
endforeach()
# Not part of any of the three directories, but generated/discord_codecs.h
# includes it, so discord.h does not parse without it.
if(NOT EXISTS "@concord_SOURCE_DIR@/reflect-c/reflect-c.h")
message(FATAL_ERROR
"Concord: missing @concord_SOURCE_DIR@/reflect-c/reflect-c.h -- the "
"reflect-c submodule was not cloned; remove build/_deps and reconfigure")
endif()
list(APPEND _hdrs "@concord_SOURCE_DIR@/reflect-c/reflect-c.h")
file(COPY ${_hdrs} DESTINATION "@CONCORD_INCLUDE_DIR@/concord")
# third_party/concord is a checked-in mirror of the headers just installed. It
# exists so an editor has something to resolve <concord/*.h> against in a fresh
# clone, before anything has been built -- build/ is gitignored, so the copy
# above is not there yet.
#
# It is refreshed from the same file list on every build rather than left to be
# updated by hand, because it also sits FIRST on hammy's include path
# (-isystem third_party precedes the installed headers). A stale mirror would
# not merely confuse the editor, it would be what the compiler actually reads --
# hammy would build against one version of Concord and link another. Keeping the
# two byte-identical makes the ordering irrelevant. Moving GIT_TAG therefore
# shows up as a diff under third_party/concord; commit it along with the bump.
#
# Prune first: a version bump can retire a header, and one left behind in the
# mirror would still be found by the compiler. Only files that vanished upstream
# are removed -- file(COPY) preserves timestamps and skips files already
# matching, so mirroring costs one stat per header on a build that changed
# nothing. The exception is discord_codecs.h, which Concord's own build
# regenerates every time (BUILD_ALWAYS re-runs make); its mtime moves, so it is
# recopied. That does not add a rebuild -- the regenerated original already
# forces one -- and its contents are deterministic, so git stays clean.
set(_want "")
foreach(_hdr IN LISTS _hdrs)
get_filename_component(_name "${_hdr}" NAME)
list(APPEND _want "${_name}")
endforeach()
file(GLOB _mirrored "@CMAKE_CURRENT_SOURCE_DIR@/third_party/concord/*.h")
foreach(_old IN LISTS _mirrored)
get_filename_component(_name "${_old}" NAME)
if(NOT "${_name}" IN_LIST _want)
file(REMOVE "${_old}")
endif()
endforeach()
file(COPY ${_hdrs} DESTINATION "@CMAKE_CURRENT_SOURCE_DIR@/third_party/concord")
file(GLOB _libs "@concord_SOURCE_DIR@/lib/libdiscord.*")
if(NOT _libs)
message(FATAL_ERROR "Concord: build produced no library in @concord_SOURCE_DIR@/lib")
endif()
file(COPY ${_libs} DESTINATION "@CONCORD_PREFIX@/lib")
]])
# Concord gets our instrumentation but not our warning set: ASan only sees a
# bug if the translation unit that owns the memory was compiled with it, and
# Concord allocates plenty that our code then touches. CFLAGS goes through
# 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.
# On a system where cc is GCC and we build with Clang, the generator is then
# handed our Clang-only flags (-fno-sanitize=function) and dies. Command-line
# assignments do beat makefile assignments, so force the compiler there --
# while leaving CFLAGS in the environment, per the note above.
#
# gencodecs-pp is a build-time text filter, run as `cpp ... | ./gencodecs-pp`.
# ASan's exit-time leak check would turn a leak in that throwaway tool into a
# build failure reported as a broken pipeline, so switch it off for the
# duration of Concord's build only; our own binary is unaffected.
ExternalProject_Add(concord_external
SOURCE_DIR "${concord_SOURCE_DIR}"
DOWNLOAD_COMMAND "" # FetchContent already cloned it
UPDATE_COMMAND ""
CONFIGURE_COMMAND ""
BUILD_IN_SOURCE TRUE # upstream's Makefile has no out-of-tree mode
BUILD_COMMAND ${CMAKE_COMMAND} -E env "CC=${CMAKE_C_COMPILER}" "CFLAGS=${CONCORD_CFLAGS}"
# Upstream tracks nothing we can express as a byproduct, so the stamp
# would happily report "built" after a `make clean` emptied lib/.
BUILD_ALWAYS TRUE
BUILD_COMMAND ${CMAKE_COMMAND} -E env
"CC=${CMAKE_C_COMPILER}"
"CFLAGS=${CONCORD_CFLAGS}"
"ASAN_OPTIONS=detect_leaks=0"
${HAMMY_MAKE_EXECUTABLE}
INSTALL_COMMAND ${HAMMY_MAKE_EXECUTABLE} install "PREFIX=${CONCORD_PREFIX}"
"CC=${CMAKE_C_COMPILER}"
"HOSTCC=${CMAKE_C_COMPILER}"
"CPP=${CMAKE_C_COMPILER} -E"
INSTALL_COMMAND ${CMAKE_COMMAND} -P "${CONCORD_INSTALL_SCRIPT}"
BUILD_BYPRODUCTS "${CONCORD_LIBRARY}"
USES_TERMINAL_BUILD TRUE
# USES_TERMINAL_BUILD would silently disable LOG_BUILD; with
# LOG_OUTPUT_ON_FAILURE we get the full log exactly when it matters.
LOG_BUILD TRUE
LOG_INSTALL TRUE
LOG_OUTPUT_ON_FAILURE TRUE
@@ -423,6 +556,15 @@ else()
set(HAMMY_CONCORD_EXTERNAL concord_external)
endif()
# ---------------------------------------------------------
# SQLite3 (hammy's own dependency, unlike the Concord ones above)
#
# CMake's bundled FindSQLite3 module (3.14+) exports the SQLite::SQLite3
# imported target and covers both a system package and a manually-specified
# SQLITE3_INCLUDE_DIR/SQLITE3_LIBRARY, so no vendoring is needed here.
# ---------------------------------------------------------
find_package(SQLite3 REQUIRED)
# ---------------------------------------------------------
# Output directories
# ---------------------------------------------------------
@@ -447,7 +589,20 @@ target_include_directories(hammy PRIVATE
${PROJECT_SOURCE_DIR}/include
)
target_link_libraries(hammy PRIVATE concord::concord)
target_include_directories(hammy SYSTEM PRIVATE
${PROJECT_SOURCE_DIR}/third_party
)
# Concord's headers arrive through concord::concord. CMake passes an imported
# target's interface includes as -isystem, so upstream's headers never trip our
# warning set and no second copy under third_party/ is needed.
target_link_libraries(hammy PRIVATE concord::concord SQLite::SQLite3)
if (NOT MSVC)
target_link_libraries(hammy PRIVATE m)
endif()
if(HAMMY_CONCORD_EXTERNAL)
add_dependencies(hammy ${HAMMY_CONCORD_EXTERNAL})
endif()
@@ -492,8 +647,9 @@ message(STATUS "hammy: sanitizers [Debug/Strict] ${HAMMY_SANITIZER_FLAGS}")
message(STATUS "hammy: static analyzer [Analyzer] ${HAMMY_ANALYZER_FLAGS}")
message(STATUS "hammy: hardening ${HAMMY_ENABLE_HARDENING}")
message(STATUS "hammy: LTO ${HAMMY_ENABLE_LTO}")
message(STATUS "hammy: concord ${concord_SOURCE_DIR} (branch dev)")
message(STATUS "hammy: concord ${concord_SOURCE_DIR} (v3.0.1)")
message(STATUS "hammy: sqlite3 ${SQLite3_LIBRARIES} (v${SQLite3_VERSION})")
if(CMAKE_BUILD_TYPE STREQUAL "Analyzer" AND NOT HAMMY_ANALYZER_FLAGS)
message(STATUS "hammy: NOTE - Analyzer config has no static analyzer on "
"${CMAKE_C_COMPILER_ID}; use scan-build over this build tree")
endif()
endif()
+23 -3
View File
@@ -1,12 +1,32 @@
# Hammy
A Discord Bot for Ham/Amateur Radio
# AI Disclosure
Fun fact: The logo of Hammy is a function! It is: f(x) = e^(-x^2)sin(9x)
## Features
Current features:
- /freq &lt;frequency (MHz)&gt; [country (default: US)] - Show the band, segment and who can transmit (for that country).
- /abbr &lt;abbreviation&gt; - Convert Abbreviation to Meaning and Context of Meaning.
- /q &lt;q-code&gt; - Convert Q-Code to Question/Answer.
- /phonetic &lt;text&gt; - Convert Text to Phonetics (useful for callsigns); If text is under 12 characters, also shows pronunciaction.
- /morse &lt;text&gt; - Convert Text to Morse Code.
- /dxcc &lt;callsign&gt; [grid (maidenhead locator)] - Show the DXCC Entity of a Callsign.
- /ping - Ping the Bot.
Currently in-development, many features planned!
## Architecture
Hammy is written in C, using the [Concord Library](https://github.com/Cogmasters/concord). It's multithreaded, with a thread pool system for queuing jobs.
User interactions can be of two types:
- Instant Interactions: Handled directly by the master thread, don't require any communication with the backend, performed on-bot.
- Non-Instant Interactions: Queued into the job pool, where a worker thread picks them up (handoff) for processing in the background and responds later.
## AI Disclosure
AI tools were used responsibly in the creation of this project, mainly for cmakelists off a pre-made template. It has also been used for minor things like functions, reviews, commits, etc.
Contributors are responsible for their commits, regardless of the usage of AI or not.
# Legal
## Legal
Licensed under the GPLv3 license, with the exception of other third-party libraries/code which may be included under compatible licenses.
Copyright (c) 2026 The Hammy Contributors
+58
View File
@@ -0,0 +1,58 @@
#ifndef HAMMY_BOT_H
#define HAMMY_BOT_H
#include <concord/discord.h>
#include <dlibc/vector.h>
#include <stdlib.h>
#include <hammy/types.h>
#include <hammy/command.h>
#include <hammy/refdb.h>
struct hammy_bot_t {
struct discord* client; // Owning reference to the client - handoff from main.c
hammy_refdb_t* refdb; // Owning reference to sqlite - the master and each worker has their own
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
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. 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);
// 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);
// Wipes all commands from the Discord registry. Return true on success, false on failure.
bool hammy_bot_deregister_all_commands(hammy_bot_t* bot);
// 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
bool hammy_bot_destroy(hammy_bot_t** bot);
#endif
+66
View File
@@ -0,0 +1,66 @@
#ifndef HAMMY_COMMAND_H
#define HAMMY_COMMAND_H
#include <concord/discord.h>
#include <concord/log.h>
#include <stdbool.h>
#include <hammy/types.h>
#include <hammy/refdb.h>
// A command handler. Runs either on the gateway thread (instant commands) or on
// a worker thread (deferred ones), so it must not assume either. client is
// whichever client is correct for the calling thread; use it and nothing else.
//
// Instant handlers send a fresh interaction response.
// Deferred handlers EDIT the already-deferred response, via hammy_job_reply()
// or discord_edit_original_interaction_response().
//
// The handler does NOT own the job and must not destroy it.
typedef void (*hammy_command_fn)(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb);
// Plain old data. Every field points at static storage, so this struct owns
// nothing, copies freely, and needs no destructor - the vector can be created
// with a NULL destructor hook.
struct hammy_command_t {
const char* name; // Discord command name. Not owning; expected to be a literal.
const char* description; // Not owning.
// Registration options, or NULL for none. Not owning; point at a static
// struct discord_application_command_options, e.g.
//
// static struct discord_application_command_option grid_opt_array[] = {
// { .type = DISCORD_APPLICATION_OPTION_STRING, .name = "locator",
// .description = "Maidenhead grid", .required = true },
// };
// static struct discord_application_command_options grid_opts = {
// .size = 1, .array = grid_opt_array
// };
struct discord_application_command_options* options;
hammy_command_fn handler;
// true -> pure computation; answered inline on the gateway thread, no
// defer, no queue. Must complete in microseconds.
// false -> may block (HTTP, DB, file IO); deferred and queued to a worker.
//
// Marking a blocking command instant stalls the gateway and risks missed
// heartbeats. When unsure, leave it false.
bool instant;
};
// The built-in command table. Returns a pointer to static storage; outCount
// receives the number of entries. Never NULL.
const hammy_command_t* hammy_command_builtins(size_t* outCount);
// Linear lookup by name over a plain array. The table is small enough that
// anything cleverer is not worth the code.
// Returns NULL if not found.
const hammy_command_t* hammy_command_find(const hammy_command_t* commands, size_t count,
const char* name);
// True if the command is safe to run inline on the gateway thread.
// Tolerates NULL so callers can skip a null check.
bool hammy_command_is_instant(const hammy_command_t* command);
#endif
+16
View File
@@ -0,0 +1,16 @@
// commands.h
#ifndef HAMMY_COMMANDS_H
#define HAMMY_COMMANDS_H
#include <concord/discord.h>
#include <hammy/types.h>
void hammy_cmd_ping(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb); // Ping
void hammy_cmd_morse(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb); // Text -> Morse
void hammy_cmd_freq(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb); // Frequency lookup
void hammy_cmd_q(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb); // Q-Code lookup
void hammy_cmd_abbr(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb); // Abbreviation lookup
void hammy_cmd_phonetic(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb); // Callsign -> Phonetics
void hammy_cmd_dxcc(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb); // Callsign + Grid -> DXCC
#endif
+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 = (char*)"Hammy Bot" };
out[0] = (struct discord_embed){
.title = (char*)"An Error Occurred",
.description = (char*)"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,
const char *title,
const 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 = (char*)"Hammy Bot" };
if (fieldCount > 0) {
*fieldsOut = (struct discord_embed_fields){
.size = fieldCount,
.array = fields,
};
}
out[0] = (struct discord_embed){
.title = (char*)title,
.description = (char*)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,
const char *title,
const 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 = (char*)"Hammy Bot" };
if (fieldCount > 0) {
*fieldsOut = (struct discord_embed_fields){
.size = fieldCount,
.array = fields,
};
}
out[0] = (struct discord_embed){
.title = (char*)title,
.description = (char*)description,
.color = color,
.timestamp = discord_timestamp(client),
.footer = &footer,
.fields = (fieldCount > 0) ? fieldsOut : NULL,
};
return true;
}
#endif
+43
View File
@@ -0,0 +1,43 @@
#ifndef HAMMY_GEO_H
#define HAMMY_GEO_H
#include <stdbool.h>
#include <stddef.h>
// Maidenhead locators and great-circle maths. No dependencies beyond libm, no
// state, no database - safe to call from any thread.
//
// Lives in its own module rather than utils.h because /grid, /beacon and /sat
// will all want it, and mixing coordinate maths in with string helpers makes
// both harder to find.
// 8 characters plus NUL. Longer locators exist in theory; nothing uses them.
#define HAMMY_GRID_MAX 9
// Writes a Maidenhead locator for the given coordinates. precision must be
// 2 (field), 4 (square), 6 (subsquare) or 8 (extended square); 6 is what people
// quote. Returns false on out-of-range coordinates, bad precision, or too small
// a buffer.
bool hammy_grid_from_latlon(double lat, double lon, int precision, char* out, size_t cap);
// Parses a 2, 4, 6 or 8 character locator. Case-insensitive, ignores embedded
// whitespace, rejects anything longer rather than truncating it.
//
// Returns the CENTER of the square, not its south-west corner: using the corner
// biases every distance by up to half a square, which at 4-character precision
// is about 60 km.
bool hammy_grid_to_latlon(const char* grid, double* outLat, double* outLon);
// Haversine distance in km and initial bearing in degrees true. Either output
// pointer may be NULL.
void hammy_great_circle(double lat1, double lon1, double lat2, double lon2,
double* outDistKm, double* outBearingDeg);
// The other way round the planet.
double hammy_long_path_km(double shortPathKm);
double hammy_reciprocal_bearing(double bearingDeg);
// 16-point compass abbreviation ("NNE"). Points at static storage.
const char* hammy_compass_point(double bearingDeg);
#endif
+71
View File
@@ -0,0 +1,71 @@
#ifndef HAMMY_JOB_H
#define HAMMY_JOB_H
#include <concord/discord.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <hammy/types.h>
#include <hammy/refdb.h>
// A single slash-command option. Flattened out of the interaction event.
// Both strings are owned by the job.
typedef struct hammy_arg_t {
char* name;
char* value;
} hammy_arg_t;
// A unit of deferred work handed from the gateway thread to a worker.
//
// OWNERSHIP: a job is created by the gateway thread and, on a successful
// hammy_pool_push(), ownership transfers to the pool. After that the creating
// thread MUST NOT touch it again. The worker that pops it owns it and destroys
// it. On a failed push the caller still owns it and must destroy it itself.
struct hammy_job_t {
char* token; // Interaction token - owning
u64snowflake id; // Interaction ID
u64snowflake appId; // Application ID, needed to edit the original response
u64snowflake user; // Invoking User ID, rate limiting, logging, etc.
char* command; // Command name - owning
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(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.
bool hammy_job_destroy(hammy_job_t** job);
// Looks up an option by name. Returns NULL if absent. Result is owned by the job.
const char* hammy_job_get_arg(const hammy_job_t* job, const char* name);
// Milliseconds elapsed since the job was created.
int64_t hammy_job_age_ms(const hammy_job_t* job, struct discord* client);
// Runs the job to completion and sends the reply. Called from a worker thread,
// so client MUST be that worker's own clone, never the gateway client.
// Does not destroy the job.
void hammy_job_run(hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb);
// Edits the (already deferred) interaction response with a titled embed,
// falling back to a plain text body only if the embed cannot be built.
// Used by hammy_job_run() and by the pool's error paths.
//
// Only valid on the deferred path: it edits a response, so something must have
// answered the interaction already. Instant handlers want hammy_job_respond().
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 titled embed, falling back to a
// plain text body only if the embed cannot be built.
// 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* title, const char* content, bool isError);
#endif
+73
View File
@@ -0,0 +1,73 @@
#ifndef HAMMY_POOL_H
#define HAMMY_POOL_H
#include <concord/discord.h>
#include <pthread.h>
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include <hammy/types.h>
// TODO: Change this accordingly; also probably make it configurable without rebuilding at some point
#define HAMMY_POOL_DEFAULT_WORKERS 2
#define HAMMY_POOL_DEFAULT_CAPACITY 64
// Technically, discord allows 15 minutes... but hell no. 30 seconds.
#define HAMMY_JOB_MAX_AGE_MS 30000
typedef enum {
HAMMY_PUSH_OK = 0, // Queued. Ownership transferred to the pool.
HAMMY_PUSH_FULL = 1, // At capacity. Caller still owns the job.
HAMMY_PUSH_SHUTDOWN = 2 // Pool is closing. Caller still owns the job.
} hammy_push_result_t;
struct hammy_pool_t {
pthread_mutex_t lock;
pthread_cond_t notEmpty;
hammy_job_t** jobs; // Ring buffer of cap job pointers - owning while queued
size_t head; // Next slot to pop
size_t tail; // Next slot to push
size_t count; // Live entries - tracked so full != empty ambiguity doesn't exist
size_t cap;
size_t busy; // Workers currently processing jobs.
bool shutdown;
hammy_worker_t* workers;
size_t nWorkers;
};
// 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.
// Never blocks on anything but the (uncontended, short) queue mutex, so it is
// safe to call from the gateway thread.
hammy_push_result_t hammy_pool_push(hammy_pool_t* pool, hammy_job_t* job);
// Signals all workers to finish the queue and exit, then joins them.
// Idempotent. Queued jobs are still run, so an in-flight command still gets a
// reply; use hammy_pool_shutdown_now() if you would rather drop them.
void hammy_pool_shutdown(hammy_pool_t* pool);
// As above but discards anything still queued (each dropped job gets an
// 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.
// NULLs the passing reference.
bool hammy_pool_destroy(hammy_pool_t** pool);
// Snapshot of queue depth and busy workers, for a /stats command or logging.
void hammy_pool_stats(hammy_pool_t* pool, size_t* outQueued, size_t* outBusy);
#endif
+224
View File
@@ -0,0 +1,224 @@
#ifndef HAMMY_REFDB_H
#define HAMMY_REFDB_H
#include <stdbool.h>
#include <stddef.h>
#include <sqlite3.h>
#include <stdint.h>
#include <hammy/types.h>
// Read-only handle to the reference bundle. Owned by exactly one thread.
// Theading rules: Do NOT share a hammy_refdb_t between threads. Instead, a worker should
// open it's own in hammy_worker_start(), and the gateway should open one for instant
// commands. The bundle is read-only, so no writers for sync. Seperate sqlite3 connections
// never contend.
#define HAMMY_CALLSIGN_MAX 32
#define HAMMY_ENTITY_NAME_MAX 64
#define HAMMY_COUNTRY_MAX 4
// Longest code in the morse table is '$' ('...-..-'), seven characters.
#define HAMMY_MORSE_MAX 16
// Longest Phonetic code. Fuck it, 32 characters
#define HAMMY_PHONETIC_MAX 32
// 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
// Abbreviations
#define HAMMY_ABBR_LONGEST_STR 96
#define HAMMY_ABBR_LONGEST_CTX 16
// 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
#define HAMMY_FREQ_IARU_MAX 8
#define HAMMY_FREQ_COUNTRIES_MAX 64
struct hammy_refdb_t {
sqlite3* handle;
sqlite3_stmt* stExact;
sqlite3_stmt* stPrefix;
sqlite3_stmt* stMorse;
sqlite3_stmt* stQCode;
sqlite3_stmt* stPhonetic;
sqlite3_stmt* stAbbr;
sqlite3_stmt* stFreqMain;
sqlite3_stmt* stFreqIaru;
sqlite3_stmt* stFreqNearest;
sqlite3_stmt* stFreqSegEdge;
sqlite3_stmt* stCountryKnown;
sqlite3_stmt* stCountryList;
char morseCode[HAMMY_MORSE_MAX]; // Scratch for the last hammy_refdb_get_morse() hit
char phoneticCode[HAMMY_PHONETIC_MAX];
char phoneticCodePronunciation[HAMMY_PHONETIC_MAX];
char abbrStr[HAMMY_ABBR_LONGEST_STR];
char abbrCtx[HAMMY_ABBR_LONGEST_CTX];
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];
};
struct hammy_dxcc_t {
int entityId; // ADIF DXCC entity code
char name[HAMMY_ENTITY_NAME_MAX];
char continent[4];
int cqZone; // Override applied if present
int ituZone;
double latitude; // North-positive
double longitude; // East-positive
double utcOffset; // UTC + utcOffset = local
char matchedPrefix[HAMMY_CALLSIGN_MAX];
bool exact;
};
// ---------------------------------------------------------------------------
// Frequency lookup
// ---------------------------------------------------------------------------
struct hammy_freq_priv_t {
char code[8]; // 'E', 'G', 'T'
char name[48]; // 'Amateur Extra'
int rank;
bool permitted; // false = this class may NOT transmit here
char modes[64]; // 'CW,DATA' - empty when !permitted
int64_t segLowHz;
int64_t segHighHz;
int maxPowerW; // 0 = national default, no specific limit
char notes[160];
};
struct hammy_freq_iaru_t {
int region; // 1, 2 or 3
int64_t lowHz;
int64_t highHz;
char modes[32];
};
struct hammy_freq_t {
int64_t freqHz;
bool inBand;
char band[16]; // '20m'
int64_t bandLowHz;
int64_t bandHighHz;
// The frequency sits exactly on a band or segment boundary. Worth saying
// out loud: a signal of any width centerd there straddles both sides.
bool atBandEdge;
bool atSegmentEdge;
// False when the bundle carries no licence data for this country at all.
// The band and IARU results are still valid; only the privilege table is
// missing. Say so rather than showing an empty table.
bool countryKnown;
char country[HAMMY_COUNTRY_MAX];
hammy_freq_priv_t privs[HAMMY_FREQ_PRIVS_MAX];
size_t nPrivs;
hammy_freq_iaru_t iaru[HAMMY_FREQ_IARU_MAX];
size_t nIaru;
// Only meaningful when !in_band.
char nearestBand[16];
int64_t nearestLowHz;
int64_t nearestHighHz;
int64_t nearestDistanceHz;
};
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
// Opens the bundle read-only and prepares the hot statements.
// Returns NULL and logs on failure.
hammy_refdb_t* hammy_refdb_open(const char* path);
// Finalises statements and closes the connection. NULLs the passing reference.
bool hammy_refdb_close(hammy_refdb_t** db);
// Bundle version string from ref_meta, or NULL. Owned by the refdb.
const char* hammy_refdb_version(hammy_refdb_t* db);
// ---------------------------------------------------------------------------
// Queries
// ---------------------------------------------------------------------------
// Resolves a callsign to a DXCC entity.
//
// Uses candidate-prefix equality seeks rather than "? GLOB prefix || '*'". The
// GLOB form puts the indexed column on the wrong side of the comparison, so
// SQLite scans all 7000 prefix rows; generating the candidates and seeking by
// equality is roughly 40x faster on the current bundle.
//
// Returns false if nothing matched.
bool hammy_refdb_dxcc(hammy_refdb_t* db, const char* callsign, hammy_dxcc_t* out);
// Looks up a character in the Morse table. Case-insensitive; non-ASCII bytes
// never match. Returns false if not found, leaving *out untouched.
//
// On a hit *out points at storage owned by the 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_morse(hammy_refdb_t* db, char c, const char** out);
// Looks up a character in the Phonetic table. Case-insensitive; non-ASCII bytes
// never match. Returns false if not found, leaving *out(s) untouched.
//
// On a hit *out points at storage owned by the 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_phonetic(hammy_refdb_t* db, char c, const char** out, const char** outPronunciation);
// Looks up a QSO code in the qcodes table. Case-insensitive; non-ASCII bytes
// never match. Returns false if not found, leaving *out(s) 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);
// Looks up an abbreviation in the abbreviations table. Case-insensitive; non-ASCII bytes
// never match. Returns false if not found, leaving *out(s) 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_abbr(hammy_refdb_t* db, const char* code, const char** outStr, const char** outContext);
// 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
// returns true unless the arguments are bad: "not in a band" and "no data for
// that country" are results, not failures. Check out->in_band and
// out->country_known.
bool hammy_refdb_freq(hammy_refdb_t* db, int64_t freqHz, const char* country,
hammy_freq_t* out);
// Which countries have licence data, for the "no data for XX yet" message.
// Writes up to cap codes and returns how many were written.
size_t hammy_refdb_countries(hammy_refdb_t* db,
char out[][HAMMY_COUNTRY_MAX], size_t cap);
// ---------------------------------------------------------------------------
// Input parsing
// ---------------------------------------------------------------------------
// Parses a user-supplied frequency into integer Hz. Accepts "14.150",
// "14150 kHz", "146.52 MHz", "1.2 GHz", "14150000 Hz". A bare number with no
// unit is read as MHz, which is what people type.
//
// Deliberately avoids floating point: "14.150" is converted digit by digit to
// 14150000 exactly. Going via double gives 14150000.000000002, which compares
// wrong against an integer column at exactly a band edge - the one case where
// being right matters most.
//
// Returns false on unparseable input or a value outside 1 Hz .. 300 GHz.
bool hammy_freq_parse(const char* text, int64_t* outHz);
#endif
+15
View File
@@ -0,0 +1,15 @@
#ifndef HAMMY_TYPES_H
#define HAMMY_TYPES_H
typedef struct hammy_pool_t hammy_pool_t;
typedef struct hammy_job_t hammy_job_t;
typedef struct hammy_worker_t hammy_worker_t;
typedef struct hammy_command_t hammy_command_t;
typedef struct hammy_bot_t hammy_bot_t;
typedef struct hammy_refdb_t hammy_refdb_t;
typedef struct hammy_dxcc_t hammy_dxcc_t;
typedef struct hammy_freq_priv_t hammy_freq_priv_t;
typedef struct hammy_freq_iaru_t hammy_freq_iaru_t;
typedef struct hammy_freq_t hammy_freq_t;
#endif
+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
+29
View File
@@ -0,0 +1,29 @@
#ifndef HAMMY_WORKER_H
#define HAMMY_WORKER_H
#include <concord/discord.h>
#include <hammy/types.h>
#include <pthread.h>
#include <hammy/refdb.h>
struct hammy_worker_t {
pthread_t thread;
struct discord* clientCopy; // Clone of the client for concord threading safety - owning
hammy_pool_t* pool; // Non-owning back-reference
hammy_refdb_t* refdb; // Owning sqlite ref
int id; // Log logging mainly
bool started; // For joining
};
// 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.
// The caller must have already set pool->shutdown and broadcast, or this hangs.
void hammy_worker_join(hammy_worker_t* worker);
#endif
+196
View File
@@ -0,0 +1,196 @@
-- Hammy reference bundle schema
--
-- Read-only at runtime. Regenerated as a whole file, never migrated in place.
-- Open with SQLITE_OPEN_READONLY.
PRAGMA foreign_keys = ON;
-- ---------------------------------------------------------------------------
-- Bundle metadata
-- ---------------------------------------------------------------------------
CREATE TABLE ref_meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
-- Where each dataset came from and when. Shown by /about and used to decide
-- whether a newer bundle is worth downloading.
CREATE TABLE ref_sources (
dataset TEXT PRIMARY KEY,
source_name TEXT NOT NULL,
source_url TEXT,
license TEXT,
retrieved TEXT, -- ISO 8601 date
notes TEXT
);
-- ---------------------------------------------------------------------------
-- Bands and privileges
-- ---------------------------------------------------------------------------
-- Named bands, region-independent. edge_low/high are the widest extent across
-- all regions; per-region reality lives in band_segments.
CREATE TABLE bands (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE, -- '20m', '70cm'
edge_low_hz INTEGER NOT NULL,
edge_high_hz INTEGER NOT NULL,
wavelength_m REAL,
sort_order INTEGER NOT NULL
);
CREATE TABLE license_classes (
id INTEGER PRIMARY KEY,
country TEXT NOT NULL, -- ISO 3166-1 alpha-2
code TEXT NOT NULL, -- 'E', 'G', 'T'
name TEXT NOT NULL,
rank INTEGER NOT NULL, -- higher = more privileges
notes TEXT,
UNIQUE (country, code)
);
-- One row per (country, class, contiguous frequency range, mode group).
-- A class with split phone/CW privileges on a band gets several rows.
CREATE TABLE band_segments (
id INTEGER PRIMARY KEY,
band_id INTEGER NOT NULL REFERENCES bands(id),
country TEXT NOT NULL,
iaru_region INTEGER, -- 1, 2, 3, or NULL if not region-scoped
class_id INTEGER REFERENCES license_classes(id),
low_hz INTEGER NOT NULL,
high_hz INTEGER NOT NULL,
modes TEXT NOT NULL, -- 'CW', 'CW,DATA', 'PHONE,IMAGE'
max_power_w INTEGER, -- NULL = national default
notes TEXT
);
CREATE INDEX idx_band_segments_freq ON band_segments (country, low_hz, high_hz);
CREATE INDEX idx_band_segments_class ON band_segments (class_id);
-- ---------------------------------------------------------------------------
-- DXCC
-- ---------------------------------------------------------------------------
-- id is the ADIF/ARRL DXCC entity code, supplied by cty2sql.py's DXCC_IDS map
-- (cty.dat itself carries no entity numbers). WAE and other non-DXCC entities
-- are excluded by default since they have no code.
--
-- latitude is north-positive and longitude is east-positive; utc_offset is the
-- usual "UTC + this = local". All three are flipped relative to cty.dat's own
-- positive-west conventions by the importer.
CREATE TABLE dxcc_entities (
id INTEGER PRIMARY KEY, -- ADIF DXCC entity code
name TEXT NOT NULL,
primary_prefix TEXT NOT NULL UNIQUE,
continent TEXT, -- 'EU', 'NA', ...
cq_zone INTEGER, -- record default
itu_zone INTEGER, -- record default
latitude REAL,
longitude REAL,
utc_offset REAL,
deleted INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX idx_dxcc_entities_prefix ON dxcc_entities (primary_prefix);
-- Prefixes are matched longest-first:
-- SELECT ... WHERE ? GLOB prefix || '*' ORDER BY length(prefix) DESC LIMIT 1
-- cq_zone/itu_zone here OVERRIDE the entity default when non-NULL. About 75% of
-- rows carry one, so COALESCE them in every query:
-- COALESCE(p.cq_zone, e.cq_zone)
CREATE TABLE dxcc_prefixes (
prefix TEXT NOT NULL,
entity_id INTEGER NOT NULL REFERENCES dxcc_entities(id),
exact INTEGER NOT NULL DEFAULT 0, -- 1 = whole callsign must match
cq_zone INTEGER,
itu_zone INTEGER,
PRIMARY KEY (prefix, entity_id)
);
CREATE INDEX idx_dxcc_prefixes_len ON dxcc_prefixes (length(prefix) DESC);
-- ---------------------------------------------------------------------------
-- Operating reference
-- ---------------------------------------------------------------------------
CREATE TABLE qcodes (
code TEXT PRIMARY KEY,
question TEXT NOT NULL,
answer TEXT NOT NULL,
category TEXT -- 'general', 'aeronautical', 'maritime'
);
CREATE TABLE prosigns (
symbol TEXT PRIMARY KEY, -- 'AR', 'SK', 'BT'
morse TEXT NOT NULL,
meaning TEXT NOT NULL,
usage TEXT
);
CREATE TABLE phonetics (
letter TEXT PRIMARY KEY,
word TEXT NOT NULL,
pronunciation TEXT
);
CREATE TABLE morse (
character TEXT PRIMARY KEY,
code TEXT NOT NULL, -- '.-' with . and -
category TEXT NOT NULL -- 'letter', 'digit', 'punctuation'
);
CREATE TABLE abbreviations (
abbr TEXT PRIMARY KEY,
meaning TEXT NOT NULL,
context TEXT -- 'CW', 'general', 'contest'
);
-- RST readability/strength/tone scale
CREATE TABLE rst_scale (
component TEXT NOT NULL, -- 'R', 'S', 'T'
value INTEGER NOT NULL,
meaning TEXT NOT NULL,
PRIMARY KEY (component, value)
);
-- ---------------------------------------------------------------------------
-- Engineering reference
-- ---------------------------------------------------------------------------
-- Matched loss follows the usual two-term model:
-- loss_dB_per_100ft = k1 * sqrt(f_MHz) + k2 * f_MHz
CREATE TABLE coax_types (
name TEXT PRIMARY KEY,
impedance_ohm REAL NOT NULL,
velocity_factor REAL NOT NULL,
capacitance_pf_per_ft REAL,
outer_diameter_mm REAL,
loss_k1 REAL,
loss_k2 REAL,
max_power_hf_w INTEGER,
notes TEXT
);
-- ---------------------------------------------------------------------------
-- NCDXF/IARU beacon network
-- ---------------------------------------------------------------------------
-- 18 stations, 10 s per slot, 3 min for a full cycle on each of 5 frequencies.
-- Which station is on which band at time t is pure arithmetic from slot_index.
CREATE TABLE ncdxf_beacons (
slot_index INTEGER PRIMARY KEY, -- 0..17, order within the cycle
callsign TEXT NOT NULL,
location TEXT NOT NULL,
grid TEXT,
latitude REAL,
longitude REAL,
dxcc_id INTEGER REFERENCES dxcc_entities(id),
active INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE ncdxf_frequencies (
band_id INTEGER PRIMARY KEY REFERENCES bands(id),
freq_hz INTEGER NOT NULL,
slot_offset INTEGER NOT NULL -- slots to add for this band
);
+33
View File
@@ -0,0 +1,33 @@
INSERT INTO ref_meta (key, value) VALUES
('schema_version', '1'),
('bundle_version', '2026.08.30-dev'),
('generated_at', '2026-08-30'),
('project', 'Hammy'),
('project_url', 'https://hammybot.org');
INSERT INTO ref_sources (dataset, source_name, source_url, license, retrieved, notes) VALUES
('band_segments', 'FCC Part 97 Subpart D', 'https://www.ecfr.gov/current/title-47/part-97',
'US Government work, public domain', NULL,
'HAND-ENTERED STARTER DATA. Must be verified against the current Part 97 text before shipping. Wrong band edges can put an operator out of band.'),
('band_segments_iaru', 'IARU Region 1/2/3 band plans', 'https://www.iaru.org/', 'See IARU', NULL,
'Only allocation extents are seeded, not the full mode/bandwidth plans. Incomplete.'),
('dxcc_entities', 'ARRL DXCC list / cty.dat', 'https://www.country-files.com/',
'Free for amateur radio use, see country-files.com', NULL,
'DEMO SUBSET ONLY - about 25 entities to exercise the schema. Real bundle must be generated by parsing cty.dat.'),
('qcodes', 'ITU Q-code series', NULL, 'Public domain', NULL,
'Amateur-relevant subset of the QRA-QUZ series.'),
('phonetics', 'ITU/NATO phonetic alphabet', NULL, 'Public domain', NULL, NULL),
('morse', 'ITU-R M.1677-1', NULL, 'Public domain', NULL,
'International Morse. Does not include non-Latin extensions.'),
('coax_types', 'Manufacturer datasheets', NULL, 'Various', NULL,
'Impedance, velocity factor and capacitance are nominal published figures. loss_k1/loss_k2 are deliberately NULL - populate from the actual datasheet for each cable rather than a generic approximation, since loss is what the calculator returns.'),
('ncdxf_beacons', 'NCDXF/IARU International Beacon Project', 'https://www.ncdxf.org/beacon/',
'See NCDXF', NULL,
'Station order is stable; individual beacons go offline for extended periods. The active flag needs refreshing against the NCDXF status page at build time.');
+211
View File
@@ -0,0 +1,211 @@
-- ---------------------------------------------------------------------------
-- Bands
-- ---------------------------------------------------------------------------
INSERT INTO bands (id, name, edge_low_hz, edge_high_hz, wavelength_m, sort_order) VALUES
(1, '2200m', 135700, 137800, 2200, 10),
(2, '630m', 472000, 479000, 630, 20),
(3, '160m', 1800000, 2000000, 160, 30),
(4, '80m', 3500000, 4000000, 80, 40),
(5, '60m', 5330500, 5406400, 60, 50),
(6, '40m', 7000000, 7300000, 40, 60),
(7, '30m', 10100000, 10150000, 30, 70),
(8, '20m', 14000000, 14350000, 20, 80),
(9, '17m', 18068000, 18168000, 17, 90),
(10, '15m', 21000000, 21450000, 15, 100),
(11, '12m', 24890000, 24990000, 12, 110),
(12, '10m', 28000000, 29700000, 10, 120),
(13, '6m', 50000000, 54000000, 6, 130),
(14, '2m', 144000000, 148000000, 2, 140),
(15, '1.25m', 222000000, 225000000, 1.25, 150),
(16, '70cm', 420000000, 450000000, 0.70, 160),
(17, '33cm', 902000000, 928000000, 0.33, 170),
(18, '23cm', 1240000000, 1300000000, 0.23, 180);
-- ---------------------------------------------------------------------------
-- US license classes
-- ---------------------------------------------------------------------------
INSERT INTO license_classes (id, country, code, name, rank, notes) VALUES
(1, 'US', 'E', 'Amateur Extra', 50, NULL),
(2, 'US', 'A', 'Advanced', 40, 'Closed to new issue since 1 April 2000; existing licences remain valid.'),
(3, 'US', 'G', 'General', 30, NULL),
(4, 'US', 'T', 'Technician', 20, NULL),
(5, 'US', 'N', 'Novice', 10, 'Closed to new issue since 1 April 2000; existing licences remain valid.');
-- ---------------------------------------------------------------------------
-- US band segments (47 CFR Part 97)
--
-- VERIFY BEFORE SHIPPING. Hand-entered from the Part 97 privilege tables.
-- Wrong edges or wrong privileges can put an operator outside their licence.
--
-- Two shapes are used here, deliberately:
--
-- 1. Bands where every eligible class gets the SAME range are generated with
-- INSERT..SELECT over license_classes and a rank threshold. The rule is
-- stated once, in the WHERE clause, and the rows follow from it.
--
-- 2. Bands where classes get GENUINELY DIFFERENT ranges (most of HF: Extra
-- has 14.000-14.150 where General has 14.025-14.150) are written out
-- explicitly, one row per class. A rank threshold cannot express that.
--
-- Shape 1 exists because hand-enumerating six bands times five classes is
-- thirty near-identical rows, and the first version of this file missed several
-- of them - Advanced was absent from 2200m, 630m and 60m entirely, and VHF/UHF
-- was seeded for Technician only, so an Extra was told they had no 2m access.
-- audit.py's privilege-nesting check catches that class of mistake now, but not
-- making it is better.
-- ---------------------------------------------------------------------------
-- 2200m and 630m: all classes. Strict EIRP limits, and the operator must notify
-- UTC and await a response before transmitting.
-- VERIFY: whether Novice holds these is the part I am least sure of.
INSERT INTO band_segments (band_id, country, class_id, low_hz, high_hz, modes, max_power_w, notes)
SELECT v.band_id, 'US', lc.id, v.low_hz, v.high_hz, v.modes, v.max_power_w, v.notes
FROM license_classes lc
JOIN (
SELECT 1 AS band_id, 135700 AS low_hz, 137800 AS high_hz,
'CW,DATA' AS modes, NULL AS max_power_w, '1 W EIRP maximum' AS notes
UNION SELECT 2, 472000, 479000, 'CW,DATA', NULL, '5 W EIRP maximum'
) v
WHERE lc.country = 'US';
-- 160m, 30m, 17m, 12m: General and above (rank >= 30), whole band, same for all.
INSERT INTO band_segments (band_id, country, class_id, low_hz, high_hz, modes, max_power_w, notes)
SELECT v.band_id, 'US', lc.id, v.low_hz, v.high_hz, v.modes, v.max_power_w, v.notes
FROM license_classes lc
JOIN (
SELECT 3 AS band_id, 1800000 AS low_hz, 2000000 AS high_hz,
'CW,DATA,PHONE,IMAGE' AS modes, NULL AS max_power_w, NULL AS notes
UNION SELECT 7, 10100000, 10150000, 'CW,DATA', 200, 'No phone or image permitted'
UNION SELECT 9, 18068000, 18110000, 'CW,DATA', NULL, NULL
UNION SELECT 9, 18110000, 18168000, 'PHONE,IMAGE', NULL, NULL
UNION SELECT 11, 24890000, 24930000, 'CW,DATA', NULL, NULL
UNION SELECT 11, 24930000, 24990000, 'PHONE,IMAGE', NULL, NULL
) v
WHERE lc.country = 'US' AND lc.rank >= 30;
-- 60m: five discrete channels, General and above. Stored as channel-width
-- ranges; the USB dial frequency is the channel center minus 1.5 kHz.
INSERT INTO band_segments (band_id, country, class_id, low_hz, high_hz, modes, max_power_w, notes)
SELECT 5, 'US', lc.id, v.low_hz, v.high_hz, 'CW,DATA,PHONE', NULL, v.notes
FROM license_classes lc
JOIN (
SELECT 5330500 AS low_hz, 5333300 AS high_hz, 'Channel 1, 100 W ERP, USB' AS notes
UNION SELECT 5346500, 5349300, 'Channel 2, 100 W ERP, USB'
UNION SELECT 5357000, 5359800, 'Channel 3, 100 W ERP, USB'
UNION SELECT 5371500, 5374300, 'Channel 4, 100 W ERP, USB'
UNION SELECT 5403500, 5406300, 'Channel 5, 100 W ERP, USB'
) v
WHERE lc.country = 'US' AND lc.rank >= 30;
-- VHF/UHF and above: Technician and higher (rank >= 20) hold the full
-- allocation. There is no per-class variation above 50 MHz except for Novice.
INSERT INTO band_segments (band_id, country, class_id, low_hz, high_hz, modes, max_power_w, notes)
SELECT v.band_id, 'US', lc.id, v.low_hz, v.high_hz, v.modes, v.max_power_w, v.notes
FROM license_classes lc
JOIN (
SELECT 13 AS band_id, 50000000 AS low_hz, 50100000 AS high_hz,
'CW' AS modes, NULL AS max_power_w, 'CW only below 50.1 MHz' AS notes
UNION SELECT 13, 50100000, 54000000, 'CW,DATA,PHONE,IMAGE', NULL, NULL
UNION SELECT 14, 144000000, 144100000, 'CW', NULL, 'CW only below 144.1 MHz'
UNION SELECT 14, 144100000, 148000000, 'CW,DATA,PHONE,IMAGE', NULL, NULL
UNION SELECT 15, 222000000, 225000000, 'CW,DATA,PHONE,IMAGE', NULL, NULL
UNION SELECT 16, 420000000, 450000000, 'CW,DATA,PHONE,IMAGE', NULL,
'Geographic restrictions apply near some radar sites'
UNION SELECT 17, 902000000, 928000000, 'CW,DATA,PHONE,IMAGE', NULL,
'Secondary allocation, shared with Part 15 devices'
UNION SELECT 18, 1240000000, 1300000000, 'CW,DATA,PHONE,IMAGE', NULL, NULL
) v
WHERE lc.country = 'US' AND lc.rank >= 20;
-- Novice above 50 MHz: two reduced-power slices and nothing else.
-- VERIFY: Novice licences have not been issued since 2000, so an error here
-- would go unnoticed for years.
INSERT INTO band_segments (band_id, country, class_id, low_hz, high_hz, modes, max_power_w, notes)
SELECT v.band_id, 'US', lc.id, v.low_hz, v.high_hz, 'CW,DATA,PHONE,IMAGE', v.max_power_w,
'Novice segment, reduced power'
FROM license_classes lc
JOIN (
SELECT 15 AS band_id, 222000000 AS low_hz, 225000000 AS high_hz, 25 AS max_power_w
UNION SELECT 18, 1270000000, 1295000000, 5
) v
WHERE lc.country = 'US' AND lc.code = 'N';
-- ---------------------------------------------------------------------------
-- Bands where classes get genuinely different ranges. Written out explicitly:
-- a rank threshold cannot express "Extra from 3.500, General from 3.525".
-- ---------------------------------------------------------------------------
-- 80m / 75m
INSERT INTO band_segments (band_id, country, class_id, low_hz, high_hz, modes, max_power_w, notes) VALUES
(4, 'US', 1, 3500000, 3600000, 'CW,DATA', NULL, NULL),
(4, 'US', 1, 3600000, 4000000, 'PHONE,IMAGE', NULL, NULL),
(4, 'US', 2, 3525000, 3600000, 'CW,DATA', NULL, NULL),
(4, 'US', 2, 3700000, 4000000, 'PHONE,IMAGE', NULL, NULL),
(4, 'US', 3, 3525000, 3600000, 'CW,DATA', NULL, NULL),
(4, 'US', 3, 3800000, 4000000, 'PHONE,IMAGE', NULL, NULL),
(4, 'US', 4, 3525000, 3600000, 'CW', 200, 'CW only'),
(4, 'US', 5, 3525000, 3600000, 'CW', 200, 'CW only');
-- 40m
INSERT INTO band_segments (band_id, country, class_id, low_hz, high_hz, modes, max_power_w, notes) VALUES
(6, 'US', 1, 7000000, 7125000, 'CW,DATA', NULL, NULL),
(6, 'US', 1, 7125000, 7300000, 'PHONE,IMAGE', NULL, NULL),
(6, 'US', 2, 7025000, 7125000, 'CW,DATA', NULL, NULL),
(6, 'US', 2, 7125000, 7300000, 'PHONE,IMAGE', NULL, NULL),
(6, 'US', 3, 7025000, 7125000, 'CW,DATA', NULL, NULL),
(6, 'US', 3, 7175000, 7300000, 'PHONE,IMAGE', NULL, NULL),
(6, 'US', 4, 7025000, 7125000, 'CW', 200, 'CW only'),
(6, 'US', 5, 7025000, 7125000, 'CW', 200, 'CW only');
-- 20m
INSERT INTO band_segments (band_id, country, class_id, low_hz, high_hz, modes, max_power_w, notes) VALUES
(8, 'US', 1, 14000000, 14150000, 'CW,DATA', NULL, NULL),
(8, 'US', 1, 14150000, 14350000, 'PHONE,IMAGE', NULL, NULL),
(8, 'US', 2, 14025000, 14150000, 'CW,DATA', NULL, NULL),
(8, 'US', 2, 14175000, 14350000, 'PHONE,IMAGE', NULL, NULL),
(8, 'US', 3, 14025000, 14150000, 'CW,DATA', NULL, NULL),
(8, 'US', 3, 14225000, 14350000, 'PHONE,IMAGE', NULL, NULL);
-- 15m
INSERT INTO band_segments (band_id, country, class_id, low_hz, high_hz, modes, max_power_w, notes) VALUES
(10, 'US', 1, 21000000, 21200000, 'CW,DATA', NULL, NULL),
(10, 'US', 1, 21200000, 21450000, 'PHONE,IMAGE', NULL, NULL),
(10, 'US', 2, 21025000, 21200000, 'CW,DATA', NULL, NULL),
(10, 'US', 2, 21225000, 21450000, 'PHONE,IMAGE', NULL, NULL),
(10, 'US', 3, 21025000, 21200000, 'CW,DATA', NULL, NULL),
(10, 'US', 3, 21275000, 21450000, 'PHONE,IMAGE', NULL, NULL),
(10, 'US', 4, 21025000, 21200000, 'CW', 200, 'CW only'),
(10, 'US', 5, 21025000, 21200000, 'CW', 200, 'CW only');
-- 10m: the only HF band with Technician phone privileges
INSERT INTO band_segments (band_id, country, class_id, low_hz, high_hz, modes, max_power_w, notes) VALUES
(12, 'US', 1, 28000000, 28300000, 'CW,DATA', NULL, NULL),
(12, 'US', 1, 28300000, 29700000, 'PHONE,IMAGE', NULL, NULL),
(12, 'US', 2, 28000000, 28300000, 'CW,DATA', NULL, NULL),
(12, 'US', 2, 28300000, 29700000, 'PHONE,IMAGE', NULL, NULL),
(12, 'US', 3, 28000000, 28300000, 'CW,DATA', NULL, NULL),
(12, 'US', 3, 28300000, 29700000, 'PHONE,IMAGE', NULL, NULL),
(12, 'US', 4, 28000000, 28300000, 'CW,DATA', 200, NULL),
(12, 'US', 4, 28300000, 28500000, 'PHONE', 200, 'Technician phone privileges'),
(12, 'US', 5, 28100000, 28300000, 'CW,DATA', 200, NULL),
(12, 'US', 5, 28300000, 28500000, 'PHONE', 200, NULL);
-- ---------------------------------------------------------------------------
-- IARU allocation extents (class_id NULL = the allocation, not privileges)
-- Partial. Enough to answer "is 7.250 legal here" for Region 1 vs 2.
-- ---------------------------------------------------------------------------
INSERT INTO band_segments (band_id, country, iaru_region, class_id, low_hz, high_hz, modes, notes) VALUES
(3, '', 1, NULL, 1810000, 2000000, 'ALL', 'Varies by country within R1'),
(4, '', 1, NULL, 3500000, 3800000, 'ALL', NULL),
(6, '', 1, NULL, 7000000, 7200000, 'ALL', NULL),
(8, '', 1, NULL, 14000000, 14350000, 'ALL', NULL),
(10, '', 1, NULL, 21000000, 21450000, 'ALL', NULL),
(12, '', 1, NULL, 28000000, 29700000, 'ALL', NULL),
(14, '', 1, NULL, 144000000, 146000000, 'ALL', NULL),
(16, '', 1, NULL, 430000000, 440000000, 'ALL', 'Varies by country within R1'),
(4, '', 3, NULL, 3500000, 3900000, 'ALL', 'Varies by country within R3'),
(6, '', 3, NULL, 7000000, 7200000, 'ALL', NULL),
(14, '', 3, NULL, 144000000, 148000000, 'ALL', NULL);
+171
View File
@@ -0,0 +1,171 @@
-- ---------------------------------------------------------------------------
-- Morse (ITU-R M.1677-1)
-- ---------------------------------------------------------------------------
INSERT INTO morse (character, code, category) VALUES
('A', '.-', 'letter'), ('B', '-...', 'letter'), ('C', '-.-.', 'letter'),
('D', '-..', 'letter'), ('E', '.', 'letter'), ('F', '..-.', 'letter'),
('G', '--.', 'letter'), ('H', '....', 'letter'), ('I', '..', 'letter'),
('J', '.---', 'letter'), ('K', '-.-', 'letter'), ('L', '.-..', 'letter'),
('M', '--', 'letter'), ('N', '-.', 'letter'), ('O', '---', 'letter'),
('P', '.--.', 'letter'), ('Q', '--.-', 'letter'), ('R', '.-.', 'letter'),
('S', '...', 'letter'), ('T', '-', 'letter'), ('U', '..-', 'letter'),
('V', '...-', 'letter'), ('W', '.--', 'letter'), ('X', '-..-', 'letter'),
('Y', '-.--', 'letter'), ('Z', '--..', 'letter'),
('0', '-----', 'digit'), ('1', '.----', 'digit'), ('2', '..---', 'digit'),
('3', '...--', 'digit'), ('4', '....-', 'digit'), ('5', '.....', 'digit'),
('6', '-....', 'digit'), ('7', '--...', 'digit'), ('8', '---..', 'digit'),
('9', '----.', 'digit'),
('.', '.-.-.-', 'punctuation'), (',', '--..--', 'punctuation'),
('?', '..--..', 'punctuation'), ('''', '.----.', 'punctuation'),
('!', '-.-.--', 'punctuation'), ('/', '-..-.', 'punctuation'),
('(', '-.--.', 'punctuation'), (')', '-.--.-', 'punctuation'),
('&', '.-...', 'punctuation'), (':', '---...', 'punctuation'),
(';', '-.-.-.', 'punctuation'), ('=', '-...-', 'punctuation'),
('+', '.-.-.', 'punctuation'), ('-', '-....-', 'punctuation'),
('_', '..--.-', 'punctuation'), ('"', '.-..-.', 'punctuation'),
('$', '...-..-','punctuation'), ('@', '.--.-.', 'punctuation');
-- ---------------------------------------------------------------------------
-- ITU/NATO phonetics
-- ---------------------------------------------------------------------------
INSERT INTO phonetics (letter, word, pronunciation) VALUES
('A', 'Alfa', 'AL-FAH'), ('B', 'Bravo', 'BRAH-VOH'),
('C', 'Charlie', 'CHAR-LEE'), ('D', 'Delta', 'DELL-TAH'),
('E', 'Echo', 'ECK-OH'), ('F', 'Foxtrot', 'FOKS-TROT'),
('G', 'Golf', 'GOLF'), ('H', 'Hotel', 'HOH-TELL'),
('I', 'India', 'IN-DEE-AH'), ('J', 'Juliett', 'JEW-LEE-ETT'),
('K', 'Kilo', 'KEY-LOH'), ('L', 'Lima', 'LEE-MAH'),
('M', 'Mike', 'MIKE'), ('N', 'November', 'NO-VEM-BER'),
('O', 'Oscar', 'OSS-CAH'), ('P', 'Papa', 'PAH-PAH'),
('Q', 'Quebec', 'KEH-BECK'), ('R', 'Romeo', 'ROW-ME-OH'),
('S', 'Sierra', 'SEE-AIR-RAH'), ('T', 'Tango', 'TANG-GO'),
('U', 'Uniform', 'YOU-NEE-FORM'),('V', 'Victor', 'VIK-TAH'),
('W', 'Whiskey', 'WISS-KEY'), ('X', 'X-ray', 'ECKS-RAY'),
('Y', 'Yankee', 'YANG-KEY'), ('Z', 'Zulu', 'ZOO-LOO'),
('0', 'Zero', 'ZEE-RO'), ('1', 'One', 'WUN'),
('2', 'Two', 'TOO'), ('3', 'Three', 'TREE'),
('4', 'Four', 'FOW-ER'), ('5', 'Five', 'FIFE'),
('6', 'Six', 'SIX'), ('7', 'Seven', 'SEV-EN'),
('8', 'Eight', 'AIT'), ('9', 'Nine', 'NIN-ER');
-- ---------------------------------------------------------------------------
-- Q-codes (amateur-relevant subset)
-- ---------------------------------------------------------------------------
INSERT INTO qcodes (code, question, answer, category) VALUES
('QRA', 'What is the name of your station?', 'The name of my station is ...', 'general'),
('QRG', 'Will you tell me my exact frequency?', 'Your exact frequency is ... kHz', 'general'),
('QRK', 'What is the readability of my signals?', 'The readability of your signals is ... (1 to 5)', 'general'),
('QRL', 'Are you busy?', 'I am busy, please do not interfere', 'general'),
('QRM', 'Is my transmission being interfered with?', 'Your transmission is being interfered with', 'general'),
('QRN', 'Are you troubled by static?', 'I am troubled by static', 'general'),
('QRO', 'Shall I increase transmit power?', 'Increase transmit power', 'general'),
('QRP', 'Shall I decrease transmit power?', 'Decrease transmit power', 'general'),
('QRQ', 'Shall I send faster?', 'Send faster (... words per minute)', 'general'),
('QRS', 'Shall I send more slowly?', 'Send more slowly (... words per minute)', 'general'),
('QRT', 'Shall I stop sending?', 'Stop sending', 'general'),
('QRU', 'Have you anything for me?', 'I have nothing for you', 'general'),
('QRV', 'Are you ready?', 'I am ready', 'general'),
('QRX', 'When will you call me again?', 'I will call you again at ...', 'general'),
('QRZ', 'Who is calling me?', 'You are being called by ...', 'general'),
('QSA', 'What is the strength of my signals?', 'The strength of your signals is ... (1 to 5)', 'general'),
('QSB', 'Are my signals fading?', 'Your signals are fading', 'general'),
('QSK', 'Can you hear me between your signals?', 'I can hear you between my signals', 'general'),
('QSL', 'Can you acknowledge receipt?', 'I acknowledge receipt', 'general'),
('QSO', 'Can you communicate with ... directly?', 'I can communicate with ... directly', 'general'),
('QSP', 'Will you relay to ...?', 'I will relay to ...', 'general'),
('QSY', 'Shall I change frequency?', 'Change frequency to ...', 'general'),
('QTC', 'How many messages have you to send?', 'I have ... messages for you', 'general'),
('QTH', 'What is your position?', 'My position is ...', 'general'),
('QTR', 'What is the correct time?', 'The correct time is ...', 'general');
-- ---------------------------------------------------------------------------
-- Prosigns
-- ---------------------------------------------------------------------------
INSERT INTO prosigns (symbol, morse, meaning, usage) VALUES
('AR', '.-.-.', 'End of message', 'Sent at the end of a transmission to a specific station'),
('AS', '.-...', 'Wait / stand by', 'Asking the other station to hold'),
('BK', '-...-.-', 'Break', 'Interrupting to hand over quickly'),
('BT', '-...-', 'Separator', 'Between paragraphs or sections; sent as a long dash'),
('CL', '-.-..-..', 'Closing station', 'Going off the air entirely'),
('CT', '-.-.-', 'Attention / start', 'Marks the beginning of a transmission'),
('HH', '........', 'Error', 'Eight dits, meaning the last word was wrong'),
('KN', '-.--.', 'Go ahead, named station only', 'Invites only the station being worked to reply'),
('SK', '...-.-', 'End of contact', 'Final transmission of a QSO'),
('SN', '...-.', 'Understood', 'Also written VE'),
('SOS', '...---...', 'Distress', 'International distress signal; sent as one symbol');
-- ---------------------------------------------------------------------------
-- CW abbreviations
-- ---------------------------------------------------------------------------
INSERT INTO abbreviations (abbr, meaning, context) VALUES
('73', 'Best regards', 'general'),
('88', 'Love and kisses', 'general'),
('ABT', 'About', 'CW'),
('AGN', 'Again', 'CW'),
('ANT', 'Antenna', 'CW'),
('BURO', 'QSL bureau', 'general'),
('CFM', 'Confirm', 'CW'),
('CQ', 'Calling any station', 'general'),
('CUL', 'See you later', 'CW'),
('DE', 'From (this is)', 'CW'),
('DX', 'Distance / long-distance station', 'general'),
('ES', 'And', 'CW'),
('FB', 'Fine business (excellent)', 'CW'),
('GA', 'Good afternoon / go ahead', 'CW'),
('GE', 'Good evening', 'CW'),
('GM', 'Good morning', 'CW'),
('HI', 'Laughter', 'CW'),
('HW', 'How copy?', 'CW'),
('K', 'Invitation to transmit', 'CW'),
('OM', 'Old man (any operator)', 'CW'),
('PSE', 'Please', 'CW'),
('PWR', 'Power', 'CW'),
('RIG', 'Station equipment', 'general'),
('RPT', 'Repeat', 'CW'),
('RST', 'Readability, strength, tone', 'general'),
('RX', 'Receiver', 'general'),
('SIG', 'Signal', 'CW'),
('SK', 'Silent key (deceased operator)', 'general'),
('SRI', 'Sorry', 'CW'),
('TNX', 'Thanks', 'CW'),
('TU', 'Thank you', 'CW'),
('TX', 'Transmitter', 'general'),
('UR', 'Your / you are', 'CW'),
('VY', 'Very', 'CW'),
('WX', 'Weather', 'CW'),
('XYL', 'Wife', 'CW'),
('YL', 'Young lady (female operator)','CW');
-- ---------------------------------------------------------------------------
-- RST scale
-- ---------------------------------------------------------------------------
INSERT INTO rst_scale (component, value, meaning) VALUES
('R', 1, 'Unreadable'),
('R', 2, 'Barely readable, occasional words distinguishable'),
('R', 3, 'Readable with considerable difficulty'),
('R', 4, 'Readable with practically no difficulty'),
('R', 5, 'Perfectly readable'),
('S', 1, 'Faint, signals barely perceptible'),
('S', 2, 'Very weak'),
('S', 3, 'Weak'),
('S', 4, 'Fair'),
('S', 5, 'Fairly good'),
('S', 6, 'Good'),
('S', 7, 'Moderately strong'),
('S', 8, 'Strong'),
('S', 9, 'Extremely strong'),
('T', 1, 'Extremely rough hissing note'),
('T', 2, 'Very rough AC note, no trace of musicality'),
('T', 3, 'Rough, low-pitched AC note, slightly musical'),
('T', 4, 'Rather rough AC note, moderately musical'),
('T', 5, 'Musically modulated note'),
('T', 6, 'Modulated note, slight trace of whistle'),
('T', 7, 'Near DC note, smooth ripple'),
('T', 8, 'Good DC note, trace of ripple'),
('T', 9, 'Purest DC note');
+1962
View File
File diff suppressed because it is too large Load Diff
+57
View File
@@ -0,0 +1,57 @@
-- ---------------------------------------------------------------------------
-- Coax
--
-- Impedance, velocity factor and capacitance are nominal published figures.
-- loss_k1/loss_k2 are intentionally NULL: matched loss varies enough between
-- manufacturers of nominally the same cable that a generic approximation would
-- give the calculator false precision. Populate from the datasheet of the cable
-- you actually mean.
-- ---------------------------------------------------------------------------
INSERT INTO coax_types (name, impedance_ohm, velocity_factor, capacitance_pf_per_ft, outer_diameter_mm, loss_k1, loss_k2, notes) VALUES
('RG-58C/U', 50.0, 0.66, 30.8, 4.95, NULL, NULL, 'Thin, lossy, common for short VHF jumpers'),
('RG-8X', 50.0, 0.82, 25.0, 6.10, NULL, NULL, 'Foam dielectric, a middle ground'),
('RG-213/U', 50.0, 0.66, 30.8, 10.30, NULL, NULL, 'Workhorse HF cable'),
('RG-214/U', 50.0, 0.66, 30.8, 10.80, NULL, NULL, 'Double-shielded RG-213 equivalent'),
('LMR-400', 50.0, 0.85, 23.9, 10.29, NULL, NULL, 'Low loss, common for VHF/UHF runs'),
('LMR-600', 50.0, 0.87, 23.4, 14.99, NULL, NULL, 'Lower loss, stiffer'),
('RG-6/U', 75.0, 0.83, 16.2, 6.90, NULL, NULL, '75 ohm, cheap, fine for receive'),
('RG-11/U', 75.0, 0.66, 20.6, 10.30, NULL, NULL, '75 ohm, lower loss than RG-6'),
('RG-174/U', 50.0, 0.66, 30.8, 2.55, NULL, NULL, 'Very thin, very lossy, patch use only'),
('Belden 9913', 50.0, 0.84, 24.6, 10.30, NULL, NULL, 'Air dielectric, needs care against water ingress'),
('Hardline LDF4-50A', 50.0, 0.88, 25.9, 15.90, NULL, NULL, '1/2 inch corrugated hardline');
-- ---------------------------------------------------------------------------
-- NCDXF/IARU beacons
--
-- 18 stations, one 10 s slot each, 180 s for a full cycle per band.
-- Station transmitting on a given band at UTC time t:
-- slot = (floor(t / 10) - slot_offset) mod 18
-- ---------------------------------------------------------------------------
INSERT INTO ncdxf_beacons (slot_index, callsign, location, grid, latitude, longitude, dxcc_id) VALUES
(0, '4U1UN', 'United Nations, New York', 'FN30AS', 40.75, -73.97, NULL),
(1, 'VE8AT', 'Inuvik, NT, Canada', 'CP38GJ', 68.38, -133.72, 1),
(2, 'W6WX', 'Mt Umunhum, CA, USA', 'CM97BD', 37.16, -121.90, 291),
(3, 'KH6RS', 'Maui, Hawaii', 'BL10TS', 20.79, -156.46, NULL),
(4, 'ZL6B', 'Masterton, New Zealand', 'RE78TW', -40.92, 175.61, 170),
(5, 'VK6RBP', 'Rolystone, WA, Australia', 'OF87AV', -32.11, 116.05, 150),
(6, 'JA2IGY', 'Mt Asama, Japan', 'PM84JK', 34.45, 136.79, 339),
(7, 'RR9O', 'Novosibirsk, Russia', 'NO14KX', 54.98, 82.89, NULL),
(8, 'VR2B', 'Hong Kong', 'OL72BG', 22.28, 114.16, NULL),
(9, '4S7B', 'Colombo, Sri Lanka', 'MJ96WV', 6.91, 79.87, NULL),
(10, 'ZS6DN', 'Pretoria, South Africa', 'KG33XI', -25.90, 28.26, 462),
(11, '5Z4B', 'Kariobangi, Kenya', 'KI88HR', -1.24, 36.89, NULL),
(12, '4X6TU', 'Tel Aviv, Israel', 'KM72JB', 32.05, 34.78, NULL),
(13, 'OH2B', 'Lohja, Finland', 'KP20EH', 60.25, 24.40, 224),
(14, 'CS3B', 'Madeira', 'IM12JT', 32.72, -16.99, NULL),
(15, 'LU4AA', 'Buenos Aires, Argentina', 'GF05TJ', -34.62, -58.37, 100),
(16, 'OA4B', 'Lima, Peru', 'FH17MW', -12.07, -77.03, NULL),
(17, 'YV5B', 'Caracas, Venezuela', 'FJ69CC', 10.50, -66.92, NULL);
INSERT INTO ncdxf_frequencies (band_id, freq_hz, slot_offset) VALUES
(8, 14100000, 0),
(9, 18110000, 1),
(10, 21150000, 2),
(11, 24930000, 3),
(12, 28200000, 4);
+3661
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
-- Generated by audit.py --indexes. Runs last in the build.
-- The bundle is read-only, so indexes cost file size and nothing else.
-- candidate-prefix equality lookup; leading PK column, but stated explicitly because everything depends on it
CREATE INDEX IF NOT EXISTS idx_dxcc_prefixes_prefix ON "dxcc_prefixes" ("prefix");
-- joins back to dxcc_entities
CREATE INDEX IF NOT EXISTS idx_dxcc_prefixes_entity_id ON "dxcc_prefixes" ("entity_id");
ANALYZE;
+342
View File
@@ -0,0 +1,342 @@
#!/usr/bin/env python3
"""
adif2sql.py -- fetch the current ADIF release's exported data files and turn the
enumerations into SQL for the Hammy reference bundle.
Usage:
python3 adif2sql.py --list # show what the zip contains
python3 adif2sql.py -o 07-adif.sql # fetch, parse, emit SQL
python3 adif2sql.py --zip local.zip --list # work from an already-downloaded zip
python3 adif2sql.py --version 316 -o out.sql
How the site works
------------------
* https://adif.org.uk/adiflatestrelease.txt returns the release as three
digits, e.g. "317" meaning ADIF 3.1.7.
* Most filenames can be constructed from that: 317/adx317.xsd and so on.
* The resources zip CANNOT: its name embeds a release date that is not derivable
from the version number (ADIF_317_resources_2026_03_22.zip). So this script
scrapes /317/index.htm to find it rather than guessing.
* The hosting provider blocks unrecognised User-Agent strings. Python's default
("Python-urllib/3.x") is one of the blocked ones, so a UA is set explicitly
below. Do not remove it.
Be polite: the ADIF site asks that applications download and keep local copies
rather than fetching on every run. This script is a build-time tool, not a
runtime dependency - run it when you cut a new bundle, commit the output.
"""
import argparse
import csv
import io
import os
import re
import sys
import urllib.error
import urllib.request
from datetime import date, timezone
import zipfile
BASE = "https://adif.org.uk"
LATEST_URL = BASE + "/adiflatestrelease.txt"
# The hosting provider blocks Python's default UA. Any plausible string works;
# this one identifies the tool honestly, which is the polite option.
USER_AGENT = "Hammy-refbundle/1.0 (+https://hammybot.org)"
RETRIEVED = date.today().isoformat()
# The resources zip ships every enumeration in six formats:
# exports/{csv,json,ods,tsv,xlsx,xml}/enumerations_<name>.<ext>
# Only one text format is wanted. ods and xlsx are themselves zip archives and
# json/xml are not delimited, so feeding them to a CSV reader produces garbage.
FORMATS = ("tsv", "csv")
# Members are matched with an anchored pattern rather than a substring test.
# Substring matching collapsed enumerations_mode, enumerations_submode and
# enumerations_propagation_mode into one table.
def member_pattern(fmt):
return re.compile(
rf"(?:^|/)exports/{fmt}/enumerations_(?P<name>[A-Za-z0-9_]+)\.{fmt}$",
re.IGNORECASE)
# Optional prettier names. Anything not listed becomes adif_<enumeration_name>.
TABLE_ALIASES = {
"dxcc_entity_code": "adif_dxcc",
"primary_administrative_subdivision": "adif_subdivisions",
"secondary_administrative_subdivision": "adif_subdivisions_secondary",
"secondary_administrative_subdivision_alt": "adif_subdivisions_secondary_alt",
}
def table_for(enum_name):
key = enum_name.lower()
return TABLE_ALIASES.get(key, "adif_" + key)
def fetch(url, binary=False):
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
try:
with urllib.request.urlopen(req, timeout=60) as resp:
data = resp.read()
except urllib.error.HTTPError as exc:
if exc.code == 403:
sys.exit(f"403 from {url} - the User-Agent is being blocked. "
f"Current UA: {USER_AGENT!r}")
raise
return data if binary else data.decode("utf-8", errors="replace")
def latest_version():
return fetch(LATEST_URL).strip()
def find_resources_zip(version):
"""Scrape the version index for the resources zip, whose name carries a date."""
index = fetch(f"{BASE}/{version}/index.htm")
m = re.search(rf"ADIF_{version}_resources[_\d]*\.zip", index, re.IGNORECASE)
if not m:
sys.exit(f"no resources zip linked from {BASE}/{version}/index.htm - "
f"the page layout may have changed, check it by hand")
return f"{BASE}/{version}/{m.group(0)}"
def load_zip(path_or_url):
if os.path.exists(path_or_url):
return zipfile.ZipFile(path_or_url)
return zipfile.ZipFile(io.BytesIO(fetch(path_or_url, binary=True)))
def sniff_rows(zf, name):
"""Read a member as delimited text, guessing the delimiter."""
raw = zf.read(name).decode("utf-8-sig", errors="replace")
# ADIF's exports ship with CRLF. io.StringIO translates newlines by default,
# which leaves the \r attached to the last field of every row and makes csv
# raise "new-line character seen in unquoted field". Normalise first, then
# open with newline="" so csv does its own line splitting.
raw = raw.replace("\r\n", "\n").replace("\r", "\n")
first_line = raw.split("\n", 1)[0]
# Prefer an explicit tab check over Sniffer. These files are tab-separated
# and their description columns contain commas, which Sniffer sometimes
# mistakes for the delimiter.
if "\t" in first_line:
dialect = csv.excel_tab
else:
try:
dialect = csv.Sniffer().sniff(raw[:4096], delimiters="\t,;")
except csv.Error:
dialect = csv.excel_tab
reader = csv.reader(io.StringIO(raw, newline=""), dialect)
try:
rows = [r for r in reader if any(cell.strip() for cell in r)]
except csv.Error as exc:
# Not a delimited file at all - a .adi test QSO file, say. Skip it
# rather than taking the whole run down.
sys.stderr.write(f"skipping {name}: {exc}\n")
return [], []
if not rows:
return [], []
return rows[0], rows[1:]
def ident(name):
"""Turn an arbitrary header cell into a safe SQL column name."""
col = re.sub(r"[^0-9a-zA-Z]+", "_", name.strip().lower()).strip("_")
return col or "col"
def q(value):
if value is None or value == "":
return "NULL"
return "'" + str(value).replace("'", "''") + "'"
# Pure ADIF bookkeeping: identical on every row of every file, and recorded once
# in ref_sources instead. Everything else is kept even when constant, because a
# constant can be meaningful - adif_award.import_only is 'Import-only' on all 29
# rows, which says something real about those awards.
METADATA_COLUMNS = {"enumeration_name", "adif_version", "adif_status"}
def prune_columns(cols, rows, keep_all=False):
"""Drop ADIF bookkeeping and columns that are empty in this file.
Returns (kept_column_names, kept_rows, dropped_report).
"""
if keep_all:
return cols, rows, []
keep, dropped = [], []
for i, c in enumerate(cols):
values = {(r[i].strip() if i < len(r) and r[i] is not None else "") for r in rows}
# Compare on the normalised name: headers arrive as "Enumeration Name",
# "ADIF Version" and so on, and are only identifier-ised later.
if ident(c) in METADATA_COLUMNS:
sample = next(iter(values)) if len(values) == 1 else None
dropped.append((c, f"metadata{'=' + sample if sample else ''}"))
continue
if values <= {""}:
dropped.append((c, "empty"))
continue
keep.append(i)
kept_cols = [cols[i] for i in keep]
kept_rows = [[(r[i] if i < len(r) else "") for i in keep] for r in rows]
return kept_cols, kept_rows, dropped
def emit_table(table, header, rows, out):
cols = []
seen = {}
for h in header:
c = ident(h)
if c in seen:
seen[c] += 1
c = f"{c}_{seen[c]}"
else:
seen[c] = 0
cols.append(c)
out.write(f"\nDROP TABLE IF EXISTS {table};\n")
out.write(f"CREATE TABLE {table} (\n")
out.write(",\n".join(f" {c} TEXT" for c in cols))
out.write("\n);\n\n")
out.write(f"INSERT INTO {table} ({', '.join(cols)}) VALUES\n")
lines = []
for r in rows:
# Pad or trim to the header width; ADIF exports occasionally have
# ragged trailing columns.
r = (list(r) + [""] * len(cols))[:len(cols)]
lines.append(" (" + ", ".join(q(cell.strip()) for cell in r) + ")")
out.write(",\n".join(lines) + ";\n")
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--version", help="3-digit ADIF version (default: whatever is current)")
ap.add_argument("--zip", dest="zip_path", help="use a local zip instead of downloading")
ap.add_argument("--list", action="store_true", help="list zip contents and exit")
ap.add_argument("-o", "--output", help="write SQL here (default: stdout)")
ap.add_argument("--format", choices=FORMATS, default="tsv",
help="which export format to read (default: tsv). The zip also "
"contains json, xml, ods and xlsx copies, which are not "
"delimited text and are ignored")
ap.add_argument("--keep-all-columns", action="store_true",
help="keep ADIF bookkeeping columns (enumeration_name, "
"adif_version, adif_status) and columns that are empty "
"in this release")
ap.add_argument("--only", nargs="+", metavar="ENUM",
help="import just these enumerations by name, e.g. "
"--only mode band dxcc_entity_code")
args = ap.parse_args()
if args.zip_path:
source = args.zip_path
version = args.version or "local"
else:
version = args.version or latest_version()
sys.stderr.write(f"ADIF version {version}\n")
source = find_resources_zip(version)
sys.stderr.write(f"resources: {source}\n")
zf = load_zip(source)
members = [n for n in zf.namelist() if not n.endswith("/")]
if args.list:
for n in sorted(members):
info = zf.getinfo(n)
print(f" {info.file_size:9d} {n}")
print(f"\n{len(members)} files")
return 0
pat = member_pattern(args.format)
only = {o.lower() for o in args.only} if args.only else None
picked = []
for n in members:
m = pat.search(n)
if not m:
continue
enum_name = m.group("name").lower()
if only and enum_name not in only:
continue
picked.append((n, enum_name, table_for(enum_name)))
picked.sort(key=lambda x: x[2])
if not picked:
sys.exit(f"no exports/{args.format}/enumerations_*.{args.format} members found. "
f"Run with --list to see the zip layout.")
# One member per table, or a later file silently clobbers an earlier one.
by_table = {}
for n, enum_name, table in picked:
if table in by_table:
sys.exit(f"{table} claimed by both {by_table[table]} and {n} - "
f"add an entry to TABLE_ALIASES to disambiguate")
by_table[table] = n
out = open(args.output, "w", encoding="utf-8") if args.output else sys.stdout
try:
out.write("-- Generated by adif2sql.py. Do not edit by hand.\n")
out.write(f"-- ADIF version {version}, {args.format} exports, from {source}\n")
out.write("-- Columns are all TEXT: these are enumerations, and ADIF's own\n")
out.write("-- exports carry version-dependent extra columns. Cast at query time.\n")
for name, enum_name, table in picked:
header, rows = sniff_rows(zf, name)
if not header:
sys.stderr.write(f"skipping empty {name}\n")
continue
header, rows, dropped = prune_columns(header, rows, args.keep_all_columns)
note = f" (-{len(dropped)} cols)" if dropped else ""
sys.stderr.write(f"{enum_name:44} -> {table:32} {len(rows):5d} rows{note}\n")
emit_table(table, header, rows, out)
# Provenance, so audit.py stops complaining about undocumented tables.
out.write(
"\nINSERT OR REPLACE INTO ref_sources "
"(dataset, source_name, source_url, license, retrieved, notes) VALUES\n"
f" ({q(table)}, {q('ADIF ' + version + ' ' + enum_name)}, "
f"{q(source)}, 'ADIF specification, openly published', "
f"{q(RETRIEVED)}, "
f"{q('Generated by adif2sql.py. Dropped columns: ' + (', '.join(c for c, _ in dropped) or 'none'))});\n")
finally:
if args.output:
out.close()
return 0
if __name__ == "__main__":
sys.exit(main())
+485
View File
@@ -0,0 +1,485 @@
#!/usr/bin/env python3
"""
audit.py -- consistency checks for the Hammy reference bundle.
Usage:
python3 audit.py hammy-ref.sqlite # run every check
python3 audit.py hammy-ref.sqlite --quiet # only failures
python3 audit.py hammy-ref.sqlite --indexes # emit CREATE INDEX DDL
Exits non-zero if any check fails, so it drops straight into CI.
The checks fall into three groups:
structural SQLite's own integrity and declared foreign keys.
relational Joins that SHOULD hold but are not declared as foreign keys,
mostly because the ADIF tables are generated with TEXT columns.
Orphans here mean two datasets disagree.
domain Amateur-radio specific invariants. Band edges that cross, gaps in
the beacon cycle, prefixes that resolve to nothing. These are the
ones that catch a bad hand-entered row.
"""
import argparse
import sqlite3
import sys
FAILURES = []
WARNINGS = []
def fail(check, detail):
FAILURES.append((check, detail))
def warn(check, detail):
WARNINGS.append((check, detail))
def tables(db):
return [r[0] for r in db.execute(
"SELECT name FROM sqlite_master WHERE type='table' "
"AND name NOT LIKE 'sqlite_%' ORDER BY name")]
def columns(db, table):
return [r[1] for r in db.execute(f"PRAGMA table_info({table})")]
def indexed_columns(db, table):
"""Columns usable as the LEADING column of some index or the primary key.
In a composite key (prefix, entity_id) only 'prefix' is reachable; a query
filtering on entity_id alone still scans. PRAGMA table_info's pk field is
the 1-based position within the key, so pk == 1 is the leading column.
"""
covered = set()
for row in db.execute(f"PRAGMA table_info({table})"):
if row[5] == 1:
covered.add(row[1])
for idx in db.execute(f"PRAGMA index_list({table})"):
info = list(db.execute(f"PRAGMA index_info({idx[1]})"))
if info:
covered.add(info[0][2])
return covered
# ---------------------------------------------------------------------------
# Structural
# ---------------------------------------------------------------------------
def check_structural(db, verbose):
result = db.execute("PRAGMA integrity_check").fetchone()[0]
if result != "ok":
fail("integrity_check", result)
elif verbose:
print(" integrity_check ok")
violations = db.execute("PRAGMA foreign_key_check").fetchall()
if violations:
for v in violations[:10]:
fail("foreign_key_check", f"{v[0]} rowid {v[1]} -> {v[2]}")
elif verbose:
print(" foreign_key_check ok")
# ---------------------------------------------------------------------------
# Relational: undeclared joins that should still hold
# ---------------------------------------------------------------------------
# (child table, child column, parent table, parent column, description)
RELATIONS = [
("dxcc_prefixes", "entity_id", "dxcc_entities", "id",
"every prefix resolves to an entity"),
("band_segments", "band_id", "bands", "id",
"every segment belongs to a band"),
("band_segments", "class_id", "license_classes", "id",
"every segment's licence class exists"),
("ncdxf_beacons", "dxcc_id", "dxcc_entities", "id",
"beacon entities resolve"),
("ncdxf_frequencies", "band_id", "bands", "id",
"beacon frequencies map to a band"),
# ADIF tables are all TEXT, so these need a CAST to join against integers.
("adif_subdivisions", "dxcc_entity_code", "adif_dxcc", "entity_code",
"subdivisions point at a real ADIF entity"),
]
def check_relations(db, verbose):
present = set(tables(db))
for child, ccol, parent, pcol, desc in RELATIONS:
if child not in present or parent not in present:
continue
if ccol not in columns(db, child) or pcol not in columns(db, parent):
warn(f"{child}.{ccol}", f"column missing, skipped ({desc})")
continue
# TRIM both sides: the ADIF exports pad some cells.
q = (f"SELECT COUNT(*) FROM {child} c "
f"WHERE c.{ccol} IS NOT NULL AND TRIM(c.{ccol}) <> '' "
f"AND NOT EXISTS (SELECT 1 FROM {parent} p "
f" WHERE TRIM(p.{pcol}) = TRIM(c.{ccol}))")
orphans = db.execute(q).fetchone()[0]
if orphans:
sample = db.execute(
f"SELECT DISTINCT c.{ccol} FROM {child} c "
f"WHERE NOT EXISTS (SELECT 1 FROM {parent} p "
f"WHERE TRIM(p.{pcol}) = TRIM(c.{ccol})) "
f"AND TRIM(c.{ccol}) <> '' LIMIT 5").fetchall()
vals = ", ".join(repr(s[0]) for s in sample)
fail(f"{child}.{ccol} -> {parent}.{pcol}",
f"{orphans} orphan rows ({desc}); e.g. {vals}")
elif verbose:
print(f" {child}.{ccol} -> {parent}.{pcol}".ljust(66) + "ok")
# ---------------------------------------------------------------------------
# Domain invariants
# ---------------------------------------------------------------------------
def check_domain(db, verbose):
present = set(tables(db))
def q1(sql, *args):
return db.execute(sql, args).fetchone()[0]
if "band_segments" in present:
n = q1("SELECT COUNT(*) FROM band_segments WHERE low_hz >= high_hz")
if n:
fail("band_segments", f"{n} rows where low_hz >= high_hz")
elif verbose:
print(" band_segments edges ordered".ljust(66) + "ok")
# A segment outside its own band's extent is almost always a typo.
rows = db.execute("""
SELECT b.name, s.low_hz, s.high_hz, b.edge_low_hz, b.edge_high_hz
FROM band_segments s JOIN bands b ON b.id = s.band_id
WHERE s.low_hz < b.edge_low_hz OR s.high_hz > b.edge_high_hz""").fetchall()
if rows:
for r in rows[:5]:
fail("band_segments", f"{r[0]} segment {r[1]}-{r[2]} outside band {r[3]}-{r[4]}")
elif verbose:
print(" band_segments within band edges".ljust(66) + "ok")
# Two segments for the same country+region+class+mode should not overlap.
# iaru_region must be part of the key: the Region 1 and Region 3
# allocation rows share country='' and legitimately cover the same
# frequencies.
rows = db.execute("""
SELECT a.country, a.iaru_region, a.class_id, a.modes,
a.low_hz, a.high_hz, b.low_hz, b.high_hz
FROM band_segments a JOIN band_segments b
ON a.id < b.id AND a.country = b.country
AND IFNULL(a.iaru_region,-1) = IFNULL(b.iaru_region,-1)
AND IFNULL(a.class_id,-1) = IFNULL(b.class_id,-1)
AND a.modes = b.modes
AND a.low_hz < b.high_hz AND b.low_hz < a.high_hz""").fetchall()
if rows:
for r in rows[:5]:
warn("band_segments", f"{r[0] or 'IARU R' + str(r[1])} class {r[2]} {r[3]}: "
f"{r[4]}-{r[5]} overlaps {r[6]}-{r[7]}")
elif verbose:
print(" band_segments no overlaps".ljust(66) + "ok")
# Privilege nesting. In most licensing regimes a higher class holds a
# superset of a lower one, so a segment that a junior class has and a
# senior class does not is nearly always a missing row rather than a
# real rule. This is exactly the shape of bug that reads as plausible
# output - "Extra: not permitted on 2m" looks like data, not an error.
#
# A warning, not a failure: the superset assumption is true of the US
# and most countries, but it is an assumption, and a contributed band
# plan could legitimately break it.
rows = db.execute("""
SELECT DISTINCT hi.code, lo.code, b.name, s.low_hz, s.high_hz
FROM band_segments s
JOIN license_classes lo ON lo.id = s.class_id
JOIN license_classes hi ON hi.country = lo.country AND hi.rank > lo.rank
JOIN bands b ON b.id = s.band_id
WHERE s.country = lo.country
AND NOT EXISTS (
SELECT 1 FROM band_segments t
WHERE t.country = s.country
AND t.class_id = hi.id
AND t.low_hz <= s.low_hz
AND s.high_hz <= t.high_hz)
ORDER BY b.name, hi.rank DESC""").fetchall()
if rows:
for r in rows[:8]:
warn("band_segments",
f"{r[0]} lacks {r[2]} {r[3]/1e6:.3f}-{r[4]/1e6:.3f} MHz "
f"that {r[1]} holds - missing row?")
if len(rows) > 8:
warn("band_segments", f"...and {len(rows) - 8} more nesting gaps")
elif verbose:
print(" band_segments privileges nest by rank".ljust(66) + "ok")
if "ncdxf_beacons" in present:
slots = [r[0] for r in db.execute("SELECT slot_index FROM ncdxf_beacons ORDER BY slot_index")]
if slots != list(range(18)):
fail("ncdxf_beacons", f"expected slots 0..17, got {len(slots)}: {slots}")
elif verbose:
print(" ncdxf_beacons slots 0..17 complete".ljust(66) + "ok")
if "dxcc_prefixes" in present:
# Longest-prefix matching breaks if a prefix is empty or has whitespace.
n = q1("SELECT COUNT(*) FROM dxcc_prefixes WHERE prefix IS NULL OR TRIM(prefix) <> prefix OR prefix = ''")
if n:
fail("dxcc_prefixes", f"{n} prefixes empty or with surrounding whitespace")
elif verbose:
print(" dxcc_prefixes clean".ljust(66) + "ok")
# Zone overrides that merely restate the entity default are noise.
n = q1("""SELECT COUNT(*) FROM dxcc_prefixes p JOIN dxcc_entities e ON e.id = p.entity_id
WHERE p.cq_zone = e.cq_zone AND p.itu_zone = e.itu_zone""")
if n:
warn("dxcc_prefixes", f"{n} rows whose zone override equals the entity default")
if "morse" in present:
n = q1("SELECT COUNT(*) FROM (SELECT code FROM morse GROUP BY code HAVING COUNT(*) > 1)")
if n:
fail("morse", f"{n} duplicate codes - decoding would be ambiguous")
elif verbose:
print(" morse codes unique".ljust(66) + "ok")
if "ref_sources" in present:
# Every populated table should say where it came from.
documented = {r[0] for r in db.execute("SELECT dataset FROM ref_sources")}
undocumented = []
for t in tables(db):
if t.startswith(("ref_", "sqlite_")):
continue
if q1(f"SELECT COUNT(*) FROM {t}") == 0:
continue
if t not in documented and not any(d in t or t in d for d in documented):
undocumented.append(t)
if undocumented:
warn("ref_sources", f"no provenance row for: {', '.join(undocumented)}")
elif verbose:
print(" ref_sources covers every table".ljust(66) + "ok")
# ---------------------------------------------------------------------------
# Dead weight
# ---------------------------------------------------------------------------
def check_dead_columns(db, verbose, group=True):
"""Constant or empty columns.
A constant column is not automatically a bug: adif_award.import_only is
'Import-only' on every row because every award in that enumeration is, and
adif_subdivisions_secondary.dxcc_entity_code is '6' because Alaska is the
only entity with secondary subdivisions. Empty columns are always dead
weight. Both are reported, but repeated findings across many tables collapse
into one line so the signal is not buried.
"""
empties = []
constants = []
for t in tables(db):
total = db.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0]
if total < 2:
continue
for c in columns(db, t):
distinct = db.execute(
f'SELECT COUNT(DISTINCT IFNULL("{c}", char(0))) FROM "{t}"').fetchone()[0]
if distinct != 1:
continue
val = db.execute(f'SELECT "{c}" FROM "{t}" LIMIT 1').fetchone()[0]
if val is None or str(val).strip() == "":
empties.append((t, c, total))
else:
constants.append((t, c, val, total))
if group:
# Collapse by column name: the same finding across 20 ADIF tables is one
# fact about the export format, not 20 problems.
by_col = {}
for t, c, total in empties:
by_col.setdefault(c, []).append(t)
for c, ts in sorted(by_col.items()):
if len(ts) > 2:
warn(f"*.{c}", f"entirely empty in {len(ts)} tables - drop it "
f"({', '.join(ts[:3])}, ...)")
else:
for t in ts:
warn(f"{t}.{c}", "entirely empty - drop it")
by_col = {}
for t, c, val, total in constants:
by_col.setdefault((c, str(val)), []).append(t)
for (c, val), ts in sorted(by_col.items()):
if len(ts) > 2:
warn(f"*.{c}", f"constant {val!r} in {len(ts)} tables - redundant "
f"({', '.join(ts[:3])}, ...)")
else:
for t in ts:
warn(f"{t}.{c}", f"constant {val!r} - check it is meaningful")
else:
for t, c, total in empties:
warn(f"{t}.{c}", f"entirely empty across {total} rows - drop it")
for t, c, val, total in constants:
warn(f"{t}.{c}", f"constant {val!r} across {total} rows - redundant")
def check_duplicates(db, verbose):
"""Exact duplicate rows, which in a reference table are always a mistake."""
for t in tables(db):
cols = columns(db, t)
if not cols:
continue
collist = ", ".join(f'"{c}"' for c in cols)
n = db.execute(
f"SELECT COUNT(*) FROM (SELECT {collist}, COUNT(*) AS n "
f'FROM "{t}" GROUP BY {collist} HAVING n > 1)').fetchone()[0]
if n:
warn(t, f"{n} groups of exactly duplicated rows")
elif verbose:
print(f" {t} no duplicate rows".ljust(66) + "ok")
# ---------------------------------------------------------------------------
# Index generation
# ---------------------------------------------------------------------------
# Columns worth indexing, by suffix or exact name.
#
# An index only helps a query that filters with equality or a range on its
# leading column. It does NOT help "? GLOB prefix || '*'", because the indexed
# column is on the wrong side of the comparison - SQLite still scans every row
# and evaluates the GLOB. Worse, an index can make such a query slower by
# tempting the planner into a nested loop.
#
# The fix for callsign lookup is on the query side, not here: generate the
# candidate prefixes from the callsign and do equality seeks, longest first.
# See docs in the repo. That turns a 7000-row scan into a handful of index
# seeks, measured at roughly 40x on the current bundle.
INDEX_HINTS = ("code", "_code", "_id", "name", "prefix", "callsign", "band",
"mode", "abbr", "symbol", "letter", "character", "grid",
"country", "continent", "cq_zone", "itu_zone", "dataset")
def emit_indexes(db, out):
out.write("-- Generated by audit.py --indexes. Runs last in the build.\n")
out.write("-- The bundle is read-only, so indexes cost file size and nothing else.\n\n")
# Indexes the heuristic cannot infer but the bot's hot paths need.
ESSENTIAL = [
("dxcc_prefixes", "prefix",
"candidate-prefix equality lookup; leading PK column, but stated "
"explicitly because everything depends on it"),
("dxcc_prefixes", "entity_id", "joins back to dxcc_entities"),
]
made = 0
present = set(tables(db))
for t, c, why in ESSENTIAL:
if t in present and c in columns(db, t):
out.write(f"-- {why}\n")
out.write(f'CREATE INDEX IF NOT EXISTS idx_{t}_{c} ON "{t}" ("{c}");\n')
made += 1
out.write("\n")
for t in tables(db):
if t.startswith("ref_"):
continue
total = db.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0]
if total < 50: # a scan of 50 rows is free
continue
covered = indexed_columns(db, t)
for c in columns(db, t):
if c in covered or (t, c) in {(a, b) for a, b, _ in ESSENTIAL}:
continue
lc = c.lower()
if not (lc in INDEX_HINTS or any(lc.endswith(h) for h in INDEX_HINTS)):
continue
# A column with almost no distinct values is not worth an index.
distinct = db.execute(f'SELECT COUNT(DISTINCT "{c}") FROM "{t}"').fetchone()[0]
if distinct < 2 or distinct < total / 100:
continue
out.write(f'CREATE INDEX IF NOT EXISTS idx_{t}_{lc} ON "{t}" ("{c}");\n')
made += 1
out.write("\n")
out.write("ANALYZE;\n")
sys.stderr.write(f"{made} indexes\n")
# ---------------------------------------------------------------------------
def main():
ap = argparse.ArgumentParser(description="Audit the Hammy reference bundle")
ap.add_argument("db", help="path to the bundle")
ap.add_argument("--quiet", action="store_true", help="only report problems")
ap.add_argument("--ungrouped", action="store_true",
help="report every constant/empty column separately instead of "
"collapsing repeated findings")
ap.add_argument("--indexes", action="store_true",
help="emit CREATE INDEX DDL to stdout instead of auditing")
ap.add_argument("-o", "--output", help="write index DDL here")
args = ap.parse_args()
db = sqlite3.connect(f"file:{args.db}?mode=ro", uri=True)
db.execute("PRAGMA foreign_keys = ON")
if args.indexes:
out = open(args.output, "w") if args.output else sys.stdout
try:
emit_indexes(db, out)
finally:
if args.output:
out.close()
return 0
verbose = not args.quiet
if verbose:
print("structural")
check_structural(db, verbose)
if verbose:
print("\nrelational")
check_relations(db, verbose)
if verbose:
print("\ndomain")
check_domain(db, verbose)
if verbose:
print("\ndead weight and duplicates")
check_dead_columns(db, verbose, group=not args.ungrouped)
check_duplicates(db, False)
print()
for check, detail in WARNINGS:
print(f"WARN {check}: {detail}")
for check, detail in FAILURES:
print(f"FAIL {check}: {detail}")
print(f"\n{len(FAILURES)} failures, {len(WARNINGS)} warnings")
return 1 if FAILURES else 0
if __name__ == "__main__":
sys.exit(main())
+421
View File
@@ -0,0 +1,421 @@
#!/usr/bin/env python3
"""
cty2sql.py -- convert AD1C cty.dat (ham radio DXCC country file) into SQL
INSERT statements for the dxcc_entities / dxcc_prefixes tables.
Usage:
python3 cty2sql.py cty.dat > dxcc.sql
python3 cty2sql.py cty.dat -o dxcc.sql --schema --transaction
Notes on cty.dat that this script deals with for you:
* cty.dat has NO DXCC entity numbers. The ADIF entity code (1 = Canada,
291 = United States, ...) is supplied by the DXCC_IDS table below, keyed
on the record's primary prefix. Override or extend it with --dxcc-map
(a JSON file of {"primary_prefix": entity_id}).
* cty.dat longitudes are POSITIVE WEST. This script flips them to the
conventional positive-east form by default (Ottawa -> -75.0). Use
--longitude as-is to keep the raw cty.dat sign.
* cty.dat UTC offsets are also positive-west (Canada 5.0, Japan -9.0) and
are emitted unchanged by default, matching the target schema. Use
--utc-offset standard to flip them into real UTC offsets (Japan +9.0).
* Entities whose primary prefix starts with "*" are WAE/CQ-only entities
(Sicily, Shetland Is., European Turkey, ...). They are not DXCC entities
and have no entity code, so they are skipped unless --include-wae is
given (which requires you to supply ids for them via --dxcc-map).
* Prefix modifiers are stripped: (cq) [itu] <lat/lon> {cont} ~offset~.
A prefix written "=CALL" is a full callsign match and is emitted with
exact = 1.
No third-party dependencies. Python 3.8+.
"""
import argparse
import json
import os
import re
import sys
from collections import OrderedDict
# --------------------------------------------------------------------------
# cty.dat primary prefix -> ADIF DXCC entity code (ADIF 3.1.6 enumeration).
# Deleted entities are not included. Regenerate/extend with --dxcc-map.
# --------------------------------------------------------------------------
DXCC_IDS = {
"VE": 1, "YA": 3, "3B6": 4, "OH0": 5, "KL": 6, "ZA": 7, "KH8": 9, "FT/z": 10, "VU4": 11,
"VP2E": 12, "CE9": 13, "EK": 14, "UA9": 15, "ZL9": 16, "YV0": 17, "4J": 18, "KH1": 20,
"EA6": 21, "T8": 22, "3Y/b": 24, "EU": 27, "EA8": 29, "T31": 31, "EA9": 32, "VQ9": 33,
"ZL7": 34, "VK9X": 35, "FO/c": 36, "TI9": 37, "VK9C": 38, "SV9": 40, "FT/w": 41, "KP5": 43,
"SV5": 45, "9M6": 46, "CE0Y": 47, "T32": 48, "3C": 49, "XE": 50, "E3": 51, "ES": 52,
"ET": 53, "UA": 54, "PY0F": 56, "C6": 60, "R1FJ": 61, "8P": 62, "FY": 63, "VP9": 64,
"VP2V": 65, "V3": 66, "ZF": 69, "CM": 70, "HC8": 71, "HI": 72, "YS": 74, "4L": 75,
"TG": 76, "J3": 77, "HH": 78, "FG": 79, "HR": 80, "6Y": 82, "FM": 84, "YN": 86, "HP": 88,
"VP5": 89, "9Y": 90, "P4": 91, "V2": 94, "J7": 95, "VP2M": 96, "J6": 97, "J8": 98,
"FT/g": 99, "LU": 100, "KH2": 103, "CP": 104, "KG4": 105, "GU": 106, "3X": 107, "PY": 108,
"J5": 109, "KH6": 110, "VK0H": 111, "CE": 112, "GD": 114, "HK": 116, "4U1I": 117,
"JX": 118, "HC": 120, "GJ": 122, "KH3": 123, "FT/j": 124, "CE0Z": 125, "UA2": 126,
"8R": 129, "UN": 130, "FT/x": 131, "ZP": 132, "ZL8": 133, "EX": 135, "OA": 136, "HL": 137,
"KH7K": 138, "PZ": 140, "VP8": 141, "VU7": 142, "XW": 143, "CX": 144, "YL": 145, "LY": 146,
"VK9L": 147, "YV": 148, "CU": 149, "VK": 150, "XX9": 152, "VK0M": 153, "C2": 157,
"YJ": 158, "8Q": 159, "A3": 160, "HK0/m": 161, "FK": 162, "P2": 163, "3B8": 165,
"KH0": 166, "OJ0": 167, "V7": 168, "FH": 169, "ZL": 170, "VK9M": 171, "VP6": 172,
"V6": 173, "KH4": 174, "FO": 175, "3D2": 176, "JD/m": 177, "ER": 179, "SV/a": 180,
"C9": 181, "KP1": 182, "H4": 185, "5U": 187, "E6": 188, "VK9N": 189, "5W": 190,
"E5/n": 191, "JD/o": 192, "3C0": 195, "KH5": 197, "3Y/p": 199, "ZS8": 201, "KP4": 202,
"C3": 203, "XF4": 204, "ZD8": 205, "OE": 206, "3B9": 207, "ON": 209, "CY0": 211, "LZ": 212,
"FS": 213, "TK": 214, "5B": 215, "HK0/a": 216, "CE0X": 217, "S9": 219, "OZ": 221,
"OY": 222, "G": 223, "OH": 224, "IS": 225, "F": 227, "DL": 230, "T5": 232, "ZB": 233,
"E5/s": 234, "VP8/g": 235, "SV": 236, "OX": 237, "VP8/o": 238, "HA": 239, "VP8/s": 240,
"VP8/h": 241, "TF": 242, "EI": 245, "1A": 246, "1S": 247, "I": 248, "V4": 249, "ZD7": 250,
"HB0": 251, "CY9": 252, "PY0S": 253, "LX": 254, "CT3": 256, "9H": 257, "JW": 259,
"3A": 260, "EY": 262, "PA": 263, "GI": 265, "LA": 266, "SP": 269, "ZK3": 270, "CT": 272,
"PY0T": 273, "ZD9": 274, "YO": 275, "FT/t": 276, "FP": 277, "T7": 278, "GM": 279,
"EZ": 280, "EA": 281, "T2": 282, "ZC4": 283, "SM": 284, "KP2": 285, "5X": 286, "HB": 287,
"UR": 288, "4U1U": 289, "K": 291, "UK": 292, "3W": 293, "GW": 294, "HV": 295, "YU": 296,
"KH9": 297, "FW": 298, "9M2": 299, "T30": 301, "S0": 302, "VK9W": 303, "A9": 304,
"S2": 305, "A5": 306, "TI": 308, "XZ": 309, "XU": 312, "4S": 315, "BY": 318, "VR": 321,
"VU": 324, "YB": 327, "EP": 330, "YI": 333, "4X": 336, "JA": 339, "JY": 342, "P5": 344,
"V8": 345, "9K": 348, "OD": 354, "JT": 363, "9N": 369, "A4": 370, "AP": 372, "DU": 375,
"A7": 376, "HZ": 378, "S7": 379, "9V": 381, "J2": 382, "YK": 384, "BV": 386, "HS": 387,
"TA": 390, "A6": 391, "7X": 400, "D2": 401, "A2": 402, "9U": 404, "TJ": 406, "TL": 408,
"D4": 409, "TT": 410, "D6": 411, "TN": 412, "9Q": 414, "TY": 416, "TR": 420, "C5": 422,
"9G": 424, "TU": 428, "5Z": 430, "7P": 432, "EL": 434, "5A": 436, "5R": 438, "7Q": 440,
"TZ": 442, "5T": 444, "CN": 446, "5N": 450, "Z2": 452, "FR": 453, "9X": 454, "6W": 456,
"9L": 458, "3D2/r": 460, "ZS": 462, "V5": 464, "ST": 466, "3DA": 468, "5H": 470, "3V": 474,
"SU": 478, "XT": 480, "9J": 482, "5V": 483, "3D2/c": 489, "T33": 490, "7O": 492, "9A": 497,
"S5": 499, "E7": 501, "Z3": 502, "OK": 503, "OM": 504, "BV9P": 505, "BS7": 506, "H40": 507,
"FO/a": 508, "FO/m": 509, "E4": 510, "4W": 511, "FK/c": 512, "VP6/d": 513, "4O": 514,
"KH8/s": 515, "FJ": 516, "PJ2": 517, "PJ7": 518, "PJ5": 519, "PJ4": 520, "Z8": 521,
"Z6": 522
}
# --------------------------------------------------------------------------
# Parsing
# --------------------------------------------------------------------------
# Modifiers that may be attached to a prefix inside a cty.dat prefix list.
# These are CAPTURED rather than discarded: about 75% of the prefix tokens in a
# current cty.dat carry a zone override, and they exist precisely because the
# record default is wrong for that prefix. VK4[55] is Queensland at ITU 55, not
# Australia's default 59; drop the modifier and every Queensland call gets the
# wrong zone.
MOD_CQ = re.compile(r"\(\s*(\d+)\s*\)")
MOD_ITU = re.compile(r"\[\s*(\d+)\s*\]")
MOD_LATLON = re.compile(r"<\s*([-\d.]+)\s*/\s*([-\d.]+)\s*>")
MOD_CONT = re.compile(r"\{([^}]*)\}")
MOD_OFFSET = re.compile(r"~([^~]*)~")
MODIFIER_RE = re.compile(r"""
\(\s*\d+\s*\)
| \[\s*\d+\s*\]
| <[^>]*>
| \{[^}]*\}
| ~[^~]*~
""", re.VERBOSE)
def split_modifiers(token):
"""Return (bare_prefix, overrides dict) for one prefix-list token."""
ov = {"cq_zone": None, "itu_zone": None, "latitude": None,
"longitude": None, "continent": None, "utc_offset": None}
m = MOD_CQ.search(token)
if m:
ov["cq_zone"] = int(m.group(1))
m = MOD_ITU.search(token)
if m:
ov["itu_zone"] = int(m.group(1))
m = MOD_LATLON.search(token)
if m:
ov["latitude"] = float(m.group(1))
ov["longitude"] = float(m.group(2)) # still positive-west here
m = MOD_CONT.search(token)
if m:
ov["continent"] = m.group(1).strip().upper() or None
m = MOD_OFFSET.search(token)
if m:
try:
ov["utc_offset"] = float(m.group(1))
except ValueError:
ov["utc_offset"] = None
return MODIFIER_RE.sub("", token).strip(), ov
class Entity:
__slots__ = ("name", "cq_zone", "itu_zone", "continent", "latitude",
"longitude", "utc_offset", "primary", "wae", "prefixes",
"entity_id", "line_no")
def __init__(self, **kw):
for k in self.__slots__:
setattr(self, k, kw.get(k))
def parse_cty(text):
"""Yield Entity objects from the contents of a cty.dat file."""
text = text.replace("\r\n", "\n").replace("\r", "\n")
# Records are terminated by ';'. Track line numbers for error messages.
pos = 0
line_no = 1
for chunk in text.split(";"):
start_line = line_no + chunk[:len(chunk) - len(chunk.lstrip("\n"))].count("\n")
line_no += chunk.count("\n")
record = chunk.strip()
if not record:
continue
head, _, body = record.partition("\n")
fields = [f.strip() for f in head.split(":")]
if len(fields) < 8:
raise ValueError(
"line %d: expected 8 colon-separated header fields, got %d: %r"
% (start_line, len(fields), head))
primary = fields[7]
wae = primary.startswith("*")
ent = Entity(
name=fields[0],
cq_zone=int(fields[1]),
itu_zone=int(fields[2]),
continent=fields[3].upper(),
latitude=float(fields[4]),
longitude=float(fields[5]),
utc_offset=float(fields[6]),
primary=primary.lstrip("*"),
wae=wae,
prefixes=[],
entity_id=None,
line_no=start_line,
)
seen = set()
for token in body.replace("\n", "").split(","):
token = token.strip()
if not token:
continue
bare, ov = split_modifiers(token)
exact = bare.startswith("=")
bare = bare.lstrip("=").strip().upper()
if not bare or bare in seen:
continue
seen.add(bare)
ent.prefixes.append((bare, 1 if exact else 0, ov))
yield ent
# --------------------------------------------------------------------------
# SQL emission
# --------------------------------------------------------------------------
SCHEMA = """\
CREATE TABLE IF NOT EXISTS dxcc_entities (
id INTEGER PRIMARY KEY,
name VARCHAR(64) NOT NULL,
continent CHAR(2) NOT NULL,
cq_zone INTEGER NOT NULL,
itu_zone INTEGER NOT NULL,
latitude DECIMAL(6,2) NOT NULL,
longitude DECIMAL(7,2) NOT NULL,
utc_offset DECIMAL(4,2) NOT NULL
);
CREATE TABLE IF NOT EXISTS dxcc_prefixes (
prefix VARCHAR(16) NOT NULL,
entity_id INTEGER NOT NULL REFERENCES dxcc_entities (id),
exact INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (prefix, entity_id)
);
CREATE INDEX IF NOT EXISTS idx_dxcc_prefixes_entity ON dxcc_prefixes (entity_id);
"""
def q(value):
"""Quote a string for SQL, doubling embedded single quotes."""
return "'" + value.replace("'", "''") + "'"
def emit_entities(entities, out):
rows = []
for e in entities:
rows.append((
"%d," % e.entity_id,
q(e.name) + ",",
q(e.primary) + ",",
q(e.continent) + ",",
"%d," % e.cq_zone,
"%d," % e.itu_zone,
"%.2f," % e.latitude,
"%.2f," % e.longitude,
"%.1f" % e.utc_offset,
))
widths = [max(len(r[i]) for r in rows) for i in range(9)]
# Numeric columns look better right-aligned, text columns left-aligned.
align = ["<", "<", "<", "<", "<", "<", ">", ">", ">"]
out.write("INSERT INTO dxcc_entities "
"(id, name, primary_prefix, continent, cq_zone, itu_zone, "
"latitude, longitude, utc_offset)"
" VALUES\n")
for n, row in enumerate(rows):
cells = [format(cell, "%s%d" % (align[i], widths[i]))
for i, cell in enumerate(row)]
line = " (" + " ".join(cells).rstrip() + ")"
out.write(line + (",\n" if n < len(rows) - 1 else ";\n"))
def emit_prefixes(entities, out, per_line=3):
out.write("INSERT INTO dxcc_prefixes "
"(prefix, entity_id, exact, cq_zone, itu_zone) VALUES\n")
def n(v):
return "NULL" if v is None else str(v)
tuples_by_entity = []
for e in entities:
if e.prefixes:
tuples_by_entity.append(
["(%s, %d, %d, %s, %s)" % (q(p), e.entity_id, x,
n(ov["cq_zone"]), n(ov["itu_zone"]))
for p, x, ov in e.prefixes])
last = len(tuples_by_entity) - 1
for i, group in enumerate(tuples_by_entity):
for j in range(0, len(group), per_line):
slice_ = group[j:j + per_line]
is_final = (i == last) and (j + per_line >= len(group))
out.write(" " + ", ".join(slice_) + (";\n" if is_final else ",\n"))
# --------------------------------------------------------------------------
# Main
# --------------------------------------------------------------------------
def main(argv=None):
ap = argparse.ArgumentParser(
description="Convert a cty.dat DXCC country file into SQL INSERT statements.")
ap.add_argument("cty", help="path to cty.dat")
ap.add_argument("-o", "--output", help="write SQL here (default: stdout)")
ap.add_argument("--dxcc-map", metavar="JSON",
help='JSON file of {"primary_prefix": entity_id} merged over '
"the built-in table")
ap.add_argument("--longitude", choices=["east-positive", "as-is"],
default="east-positive",
help="cty.dat stores longitude positive-west; 'east-positive' "
"(default) flips it to the usual convention")
ap.add_argument("--utc-offset", choices=["cty", "standard"], default="standard",
help="'cty' (default) keeps cty.dat's positive-west offsets; "
"'standard' flips them to real UTC offsets")
ap.add_argument("--include-wae", action="store_true",
help="include WAE/CQ-only entities (needs ids via --dxcc-map)")
ap.add_argument("--exclude-exact", action="store_true",
help="skip '=CALLSIGN' full-callsign entries entirely")
ap.add_argument("--schema", action="store_true",
help="emit CREATE TABLE statements first")
ap.add_argument("--truncate", action="store_true",
help="emit DELETE FROM statements before the inserts")
ap.add_argument("--transaction", action="store_true",
help="wrap the output in BEGIN / COMMIT")
ap.add_argument("--per-line", type=int, default=5, metavar="N",
help="prefix tuples per output line (default 5)")
ap.add_argument("--strict", action="store_true",
help="exit non-zero if any entity has no known DXCC id")
args = ap.parse_args(argv)
ids = dict(DXCC_IDS)
if args.dxcc_map:
with open(args.dxcc_map, encoding="utf-8") as fh:
ids.update({k: int(v) for k, v in json.load(fh).items()})
with open(args.cty, encoding="utf-8", errors="replace") as fh:
raw = fh.read()
entities = []
skipped_wae = []
unmapped = []
for ent in parse_cty(raw):
if ent.wae and not args.include_wae:
skipped_wae.append(ent)
continue
ent.entity_id = ids.get(ent.primary)
if ent.entity_id is None:
unmapped.append(ent)
continue
if args.longitude == "east-positive":
ent.longitude = -ent.longitude or 0.0 # avoid "-0.00"
if args.utc_offset == "standard":
ent.utc_offset = -ent.utc_offset or 0.0
if args.exclude_exact:
ent.prefixes = [(p, x, ov) for p, x, ov in ent.prefixes if not x]
entities.append(ent)
entities.sort(key=lambda e: e.entity_id)
# A prefix must resolve to one entity; cty.dat should not collide, but say
# so loudly if a hand-edited file does.
owner = OrderedDict()
for e in entities:
kept = []
for p, x, ov in e.prefixes:
if p in owner:
sys.stderr.write(
"warning: prefix %s claimed by both %s and %s; keeping %s\n"
% (p, owner[p], e.name, owner[p]))
continue
owner[p] = e.name
kept.append((p, x, ov))
e.prefixes = kept
for e in skipped_wae:
sys.stderr.write("note: skipping WAE/CQ-only entity %s (%s)\n"
% (e.name, e.primary))
for e in unmapped:
sys.stderr.write("warning: no DXCC id for %s (primary prefix %s, line %d)\n"
% (e.name, e.primary, e.line_no))
if not entities:
sys.stderr.write("error: no entities to write\n")
return 2
out = open(args.output, "w", encoding="utf-8") if args.output else sys.stdout
try:
src = os.path.basename(args.cty)
out.write("-- " + "-" * 73 + "\n")
out.write("-- DXCC entities and prefixes generated from %s by cty2sql.py.\n" % src)
out.write("-- %d entities, %d prefixes. Longitude is %s; utc_offset uses the %s.\n"
% (len(entities), sum(len(e.prefixes) for e in entities),
"positive east" if args.longitude == "east-positive"
else "positive west (raw cty.dat)",
"cty.dat sign (positive west)" if args.utc_offset == "cty"
else "standard UTC sign"))
out.write("-- " + "-" * 73 + "\n")
if args.schema:
out.write(SCHEMA + "\n")
if args.transaction:
out.write("BEGIN;\n")
if args.truncate:
out.write("DELETE FROM dxcc_prefixes;\nDELETE FROM dxcc_entities;\n")
emit_entities(entities, out)
emit_prefixes(entities, out, per_line=max(1, args.per_line))
if args.transaction:
out.write("COMMIT;\n")
finally:
if args.output:
out.close()
sys.stderr.write("wrote %d entities and %d prefixes\n"
% (len(entities), sum(len(e.prefixes) for e in entities)))
return 1 if (args.strict and unmapped) else 0
if __name__ == "__main__":
sys.exit(main())
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
#
# Build the Hammy reference bundle from the numbered SQL sources.
#
# The plain `for f in *.sql; do sqlite3 db < $f; done` loop keeps going after a
# failure, so one bad file leaves a half-populated database that looks fine
# until something queries the missing rows. -bail plus set -e stops at the first
# error instead.
set -euo pipefail
DB="${1:-hammy-ref.sqlite}"
# -batch ignores ~/.sqliterc, so output formatting does not depend on whatever
# .mode the developer has configured.
SQLITE=(sqlite3 -bail -batch)
# Order matters: 05-dxcc.sql must load before 06-eng-beacons.sql, because
# ncdxf_beacons.dxcc_id has a foreign key into dxcc_entities.
SOURCES=(*.sql)
if [ -e "$DB" ]; then
echo "removing existing $DB"
rm -f "$DB"
fi
for f in "${SOURCES[@]}"; do
printf ' %-24s' "$f"
"${SQLITE[@]}" "$DB" < "$f"
echo "ok"
done
echo
echo "integrity"
fk=$("${SQLITE[@]}" "$DB" 'PRAGMA foreign_key_check;')
if [ -n "$fk" ]; then
echo " FOREIGN KEY VIOLATIONS:"
echo "$fk" | sed 's/^/ /'
exit 1
fi
echo " foreign keys ok"
echo " integrity $("${SQLITE[@]}" "$DB" 'PRAGMA integrity_check;')"
echo
echo "row counts"
# pragma_table_info returns one row per COLUMN, so counting it gives column
# counts. Real row counts need a query per table; generate them and pipe back in.
"${SQLITE[@]}" -noheader "$DB" "
SELECT 'SELECT '' ' || name || ''' , COUNT(*) FROM ' || name || ';'
FROM sqlite_master WHERE type='table' ORDER BY name;
" | "${SQLITE[@]}" -noheader -separator ' ' "$DB" | awk '{printf " %-34s %8s\n", $1, $2}'
echo
echo "compacting"
"${SQLITE[@]}" "$DB" 'VACUUM;'
ls -lh "$DB"
+40
View File
@@ -0,0 +1,40 @@
#include <concord/discord.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <hammy/command.h>
#include <hammy/commands.h>
#include <hammy/job.h>
#include <hammy/refdb.h>
#include <hammy/utils.h>
#define HAMMY_ABBR_ECHO_MAX 16
#define HAMMY_ABBR_CODE_MAX 3072
#define HAMMY_ABBR_TRUNCATED " ... (truncated)"
void hammy_cmd_abbr(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb) {
// Get the qcode argument from the job
const char* abbr = hammy_job_get_arg(job, "abbreviation");
if (!abbr) {
hammy_job_respond(job, client, "Error", "No Abbreviation provided for Abbreviation conversion.", true);
return;
}
char body[HAMMY_ABBR_CODE_MAX + sizeof(HAMMY_ABBR_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* str = NULL;
const char* context = NULL;
if (!hammy_refdb_get_abbr(refdb, abbr, &str, &context)) {
hammy_job_respond(job, client, "Abbreviation Not Found!", "Failed to find the Abbreviation specified. Please check your query!", true);
return;
}
hammy_to_uppercase((char*)abbr);
snprintf(body, sizeof(body), "Abbreviation: `%s`\nMeaning: `%s`\nUsage Context: `%s`", abbr, (str ? str : "Not Specified"), (context ? context : "Not Specified"));
hammy_job_respond(job, client, "Abbreviation Conversion", body, false);
}
+191
View File
@@ -0,0 +1,191 @@
#include <concord/discord.h>
#include <inttypes.h>
#include <math.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <hammy/command.h>
#include <hammy/commands.h>
#include <hammy/geo.h>
#include <hammy/job.h>
#include <hammy/refdb.h>
#include <hammy/utils.h>
#define HAMMY_DXCC_BODY_MAX 2048
#define HAMMY_DXCC_TITLE_MAX 128
// Formats a signed decimal degree as "38.90 N" / "77.04 W".
static void hammy_dxcc_fmt_coord(double value, bool isLatitude, char* out, size_t cap) {
char hemi = isLatitude ? (value >= 0.0 ? 'N' : 'S')
: (value >= 0.0 ? 'E' : 'W');
snprintf(out, cap, "%.2f %c", fabs(value), hemi);
}
// Formats a UTC offset as "UTC+9", "UTC-5", "UTC+5:30". cty.dat carries half
// hour offsets for a handful of entities, so the fractional case is real.
static void hammy_dxcc_fmt_offset(double offset, char* out, size_t cap) {
int totalMinutes = (int)lround(offset * 60.0);
char sign = totalMinutes < 0 ? '-' : '+';
if (totalMinutes < 0) { totalMinutes = -totalMinutes; }
int hours = totalMinutes / 60;
int minutes = totalMinutes % 60;
if (minutes) {
snprintf(out, cap, "UTC%c%d:%02d", sign, hours, minutes);
} else {
snprintf(out, cap, "UTC%c%d", sign, hours);
}
}
// Wall-clock time at the entity. cty.dat gives STANDARD offsets with no notion
// of summer time, so this can be an hour out for part of the year. Said so in
// the output rather than quietly presenting it as exact.
static void hammy_dxcc_fmt_local_time(double offset, char* out, size_t cap) {
time_t now = time(NULL);
if (now == (time_t)-1) {
snprintf(out, cap, "unknown");
return;
}
time_t shifted = now + (time_t)lround(offset * 3600.0);
struct tm tmBuf;
// gmtime_r rather than gmtime: instant commands run on the gateway thread
// and deferred ones on a worker, and gmtime's static buffer is shared.
if (!gmtime_r(&shifted, &tmBuf)) {
snprintf(out, cap, "unknown");
return;
}
snprintf(out, cap, "%02d:%02d", tmBuf.tm_hour, tmBuf.tm_min);
}
// Instant command: runs on the gateway thread, sends a fresh response.
void hammy_cmd_dxcc(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb) {
if (!refdb) {
hammy_job_respond(job, client, "Error", "Reference data is unavailable. Please try again later.", true);
return;
}
const char* callsign = hammy_job_get_arg(job, "callsign");
if (!callsign || !*callsign) {
hammy_job_respond(job, client, "Error", "No callsign provided for lookup.", true);
return;
}
hammy_dxcc_t entity;
if (!hammy_refdb_dxcc(refdb, callsign, &entity)) {
char body[HAMMY_DXCC_BODY_MAX];
snprintf(body, sizeof(body),
"No DXCC entity matches `%s`.\n\n"
"Prefixes are matched longest-first, so this usually means a typo "
"or a prefix that has never been allocated.", callsign);
hammy_job_respond(job, client, "Entity Not Found!", body, true);
return;
}
char gridStr[HAMMY_GRID_MAX] = "unknown";
bool haveEntityGrid = hammy_grid_from_latlon(entity.latitude, entity.longitude, 6,
gridStr, sizeof(gridStr));
char latStr[24];
char lonStr[24];
hammy_dxcc_fmt_coord(entity.latitude, true, latStr, sizeof(latStr));
hammy_dxcc_fmt_coord(entity.longitude, false, lonStr, sizeof(lonStr));
char offsetStr[24];
char timeStr[16];
hammy_dxcc_fmt_offset(entity.utcOffset, offsetStr, sizeof(offsetStr));
hammy_dxcc_fmt_local_time(entity.utcOffset, timeStr, sizeof(timeStr));
char title[HAMMY_DXCC_TITLE_MAX];
char upperCallsign[HAMMY_CALLSIGN_MAX];
snprintf(upperCallsign, sizeof(upperCallsign), "%s", callsign);
hammy_to_uppercase(upperCallsign);
snprintf(title, sizeof(title), "%s - %s", upperCallsign, entity.name);
char body[HAMMY_DXCC_BODY_MAX];
int len = 0;
len += snprintf(body + len, sizeof(body) - (size_t)len,
"Entity: `%s` (DXCC `%d`)\n"
"Continent: `%s` CQ zone: `%d` ITU zone: `%d`\n",
entity.name, entity.entityId, entity.continent,
entity.cqZone, entity.ituZone);
if (haveEntityGrid) {
len += snprintf(body + len, sizeof(body) - (size_t)len,
"Location: `%s` (%s, %s)\n", gridStr, latStr, lonStr);
} else {
len += snprintf(body + len, sizeof(body) - (size_t)len,
"Location: %s, %s\n", latStr, lonStr);
}
len += snprintf(body + len, sizeof(body) - (size_t)len,
"Local time: `%s` (`%s`, standard time)\n", timeStr, offsetStr);
// Saying WHICH rule matched matters more than it looks. An exact hit means
// cty.dat carries a whole-callsign override, and a short prefix hit on a
// long callsign is a hint that the entity guess may be loose.
if (entity.exact) {
len += snprintf(body + len, sizeof(body) - (size_t)len,
"Matched: exact callsign rule `%s`\n", entity.matchedPrefix);
} else {
len += snprintf(body + len, sizeof(body) - (size_t)len,
"Matched: prefix `%s`\n", entity.matchedPrefix);
}
// Optional second argument: the asker's own locator, which turns this from
// trivia into something you can point an antenna with.
const char* fromGrid = hammy_job_get_arg(job, "grid");
if (fromGrid && *fromGrid) {
double fromLat = 0.0;
double fromLon = 0.0;
if (!hammy_grid_to_latlon(fromGrid, &fromLat, &fromLon)) {
len += snprintf(body + len, sizeof(body) - (size_t)len,
"\nCould not read `%s` as a Maidenhead locator. "
"Try something like `JN76` or `JN76gb`.\n", fromGrid);
} else {
double distKm = 0.0;
double bearing = 0.0;
hammy_great_circle(fromLat, fromLon, entity.latitude, entity.longitude,
&distKm, &bearing);
double longKm = hammy_long_path_km(distKm);
double longBearing = hammy_reciprocal_bearing(bearing);
char fromNorm[HAMMY_GRID_MAX];
if (!hammy_grid_from_latlon(fromLat, fromLon, 6, fromNorm, sizeof(fromNorm))) {
snprintf(fromNorm, sizeof(fromNorm), "%s", fromGrid);
}
len += snprintf(body + len, sizeof(body) - (size_t)len,
"\n**From %s**\n"
"Short path: `%.0f km` (`%.0f mi`) bearing `%.0f` (%s)\n"
"Long path: `%.0f km` bearing `%.0f` (%s)\n",
fromNorm,
distKm, distKm * 0.621371, bearing, hammy_compass_point(bearing),
longKm, longBearing, hammy_compass_point(longBearing));
}
}
// The entity coordinates are a nominal center for the whole entity, not the
// station's own position. For the US that is a point in Washington DC, so a
// bearing to a California station will be well off. Worth admitting.
len += snprintf(body + len, sizeof(body) - (size_t)len,
"\n-# Coordinates are the entity's nominal center, not the "
"station's actual location.");
(void)len;
hammy_job_respond(job, client, title, body, false);
}
+208
View File
@@ -0,0 +1,208 @@
#include <concord/discord.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdarg.h>
#include <hammy/command.h>
#include <hammy/commands.h>
#include <hammy/job.h>
#include <hammy/refdb.h>
#define HAMMY_FREQ_BODY_MAX 3600
// Small append-only string builder. Every write is bounds-checked, so a country
// with thirty licence classes overflows into "truncated" rather than the stack.
typedef struct {
char* buf;
size_t cap;
size_t len;
bool overflow;
} hammy_sb_t;
static void sb_addf(hammy_sb_t* sb, const char* fmt, ...) {
if (sb->overflow || sb->len + 1 >= sb->cap) {
sb->overflow = true;
return;
}
va_list ap;
va_start(ap, fmt);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wformat-nonliteral"
int n = vsnprintf(sb->buf + sb->len, sb->cap - sb->len, fmt, ap);
#pragma clang diagnostic pop
va_end(ap);
if (n < 0) {
sb->overflow = true;
return;
}
if ((size_t)n >= sb->cap - sb->len) {
sb->len = sb->cap - 1;
sb->overflow = true;
return;
}
sb->len += (size_t)n;
}
// Hz -> MHz with three decimals, which is the resolution every band edge in the
// bundle actually uses. Integer maths so no rounding surprises at an edge.
static void fmt_mhz(int64_t hz, char* out, size_t cap) {
int64_t whole = hz / 1000000;
int64_t frac = (hz % 1000000) / 1000;
snprintf(out, cap, "%" PRId64 ".%03" PRId64, whole, frac);
}
// 'CW,DATA' reads better as 'CW, DATA' in an embed.
static void fmt_modes(const char* modes, char* out, size_t cap) {
size_t j = 0;
for (size_t i = 0; modes[i] && j + 2 < cap; i++) {
out[j++] = modes[i];
if (modes[i] == ',' && j + 1 < cap) { out[j++] = ' '; }
}
out[j] = '\0';
}
// Instant command: runs on the gateway thread, sends a fresh response.
void hammy_cmd_freq(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb) {
if (!refdb) {
hammy_job_respond(job, client, "Error", "Reference data is unavailable. Please try again later.", true);
return;
}
const char* arg = hammy_job_get_arg(job, "frequency");
if (!arg || !*arg) {
hammy_job_respond(job, client, "Error", "No frequency provided for lookup.", true);
return;
}
// Parses "14.150", "14150 kHz", "146.52 MHz". Integer maths throughout: a
// bare (int64_t)cast of a float truncates BEFORE the multiply and turns
// 14.150 into 14.000, which silently reports the wrong segment.
int64_t freqHz = 0;
if (!hammy_freq_parse(arg, &freqHz)) {
hammy_job_respond(job, client, "Error", "I could not read that frequency. Try something like `14.150`, `7025 kHz` or `146.52 MHz`.", true);
return;
}
const char* cc = hammy_job_get_arg(job, "country"); // NULL -> refdb uses US
hammy_freq_t r;
if (!hammy_refdb_freq(refdb, freqHz, cc, &r)) {
hammy_job_respond(job, client, "Error", "Something went wrong looking that up.", true);
return;
}
char freqStr[32];
fmt_mhz(r.freqHz, freqStr, sizeof(freqStr));
char body[HAMMY_FREQ_BODY_MAX];
hammy_sb_t sb = { .buf = body, .cap = sizeof(body), .len = 0, .overflow = false };
body[0] = '\0';
// Not a valid ham band
if (!r.inBand) {
char lo[32], hi[32], dist[32];
fmt_mhz(r.nearestLowHz, lo, sizeof(lo));
fmt_mhz(r.nearestHighHz, hi, sizeof(hi));
fmt_mhz(r.nearestDistanceHz, dist, sizeof(dist));
sb_addf(&sb, "**%s MHz** is not in a valid amateur band.\n\n", freqStr);
sb_addf(&sb, "Nearest is **%s** (%s - %s MHz), %s MHz away.", r.nearestBand, lo, hi, dist);
hammy_job_respond(job, client, "Not an Amateur Band!", body, false);
return;
}
char bandLo[32], bandHi[32];
fmt_mhz(r.bandLowHz, bandLo, sizeof(bandLo));
fmt_mhz(r.bandHighHz, bandHi, sizeof(bandHi));
char title[128];
snprintf(title, sizeof(title), "Band **%s** (%s - %s MHz)\n", r.band, bandLo, bandHi);
// Privileges
if (!r.countryKnown) {
// The common case - only US data is seeded so far. Say what is missing
// and what does exist, rather than showing an empty table.
char codes[HAMMY_FREQ_COUNTRIES_MAX][HAMMY_COUNTRY_MAX];
size_t n = hammy_refdb_countries(refdb, codes, HAMMY_FREQ_COUNTRIES_MAX);
sb_addf(&sb, "\nNo licence data for **%s** in this bundle yet.\n", r.country);
if (n) {
sb_addf(&sb, "Currently available: ");
for (size_t i = 0; i < n; i++) {
sb_addf(&sb, "%s`%s`", i ? ", " : "", codes[i]);
}
sb_addf(&sb, "\n");
}
sb_addf(&sb, "Band plans are contributed by operators - if you know your regulator's allocations, please help fill this in.\n");
} else if (r.nPrivs == 0) {
sb_addf(&sb, "\nNo license classes are recorded for **%s**.\n", r.country);
} else {
sb_addf(&sb, "\n**Privileges in %s**\n", r.country);
for (size_t i = 0; i < r.nPrivs; i++) {
const hammy_freq_priv_t* p = &r.privs[i];
if (!p->permitted) {
sb_addf(&sb, "`%-14s` not permitted here\n", p->name);
continue;
}
char lo[32], hi[32], modes[80];
fmt_mhz(p->segLowHz, lo, sizeof(lo));
fmt_mhz(p->segHighHz, hi, sizeof(hi));
fmt_modes(p->modes, modes, sizeof(modes));
sb_addf(&sb, "`%-14s` %s (%s - %s", p->name, modes, lo, hi);
if (p->maxPowerW > 0) {
sb_addf(&sb, ", max %d W", p->maxPowerW);
}
sb_addf(&sb, ")\n");
if (p->notes[0]) {
sb_addf(&sb, " *%s*\n", p->notes);
}
}
}
// IARU
if (r.nIaru) {
sb_addf(&sb, "\n**IARU Allocation**\n");
for (size_t i = 0; i < r.nIaru; i++) {
char lo[32], hi[32];
fmt_mhz(r.iaru[i].lowHz, lo, sizeof(lo));
fmt_mhz(r.iaru[i].highHz, hi, sizeof(hi));
sb_addf(&sb, "Region %d: %s - %s MHz\n", r.iaru[i].region, lo, hi);
}
}
// Boundary warnings
if (r.atBandEdge) {
sb_addf(&sb, "\n> This is exactly the band edge. A signal of any width centerd here extends outside the band.\n");
} else if (r.atSegmentEdge) {
sb_addf(&sb, "\n> This is exactly a segment boundary, so a signal centerd here straddles both sides.\n");
}
if (sb.overflow) {
snprintf(body + sizeof(body) - 20, 20, "\n... truncated");
}
hammy_job_respond(job, client, title, body, false);
}
+142
View File
@@ -0,0 +1,142 @@
#include <concord/discord.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <hammy/command.h>
#include <hammy/commands.h>
#include <hammy/job.h>
#include <hammy/refdb.h>
// 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,
// user-controlled VLA off the stack: a command option runs to 6000 characters,
// and every one of them can expand to eight.
#define HAMMY_MORSE_ECHO_MAX 512
#define HAMMY_MORSE_CODE_MAX 3072
#define HAMMY_MORSE_TRUNCATED " ... (truncated)"
// Appends one token, space-separated from whatever is already in the buffer and
// optionally preceded by a word-gap slash. All or nothing: returns false and
// leaves the buffer untouched when the whole thing would not fit, so the caller
// can stop without a half-written character on the end.
//
// len counts bytes excluding the terminator, and the buffer stays terminated
// throughout - which is what the strcat-onto-uninitialised-stack version this
// replaces got wrong: strcat needs a terminated destination to start from, and
// an uninitialised one made the reply random bytes that Discord rejected as
// invalid JSON.
static bool morse_append(char* buf, size_t cap, size_t* len, bool gap, const char* token) {
size_t n = strlen(token);
size_t need = (*len > 0 ? 1u : 0u) + (gap ? 2u : 0u) + n;
if (*len + need + 1 > cap) { return false; }
if (*len > 0) { buf[(*len)++] = ' '; }
if (gap) {
buf[(*len)++] = '/';
buf[(*len)++] = ' ';
}
memcpy(buf + *len, token, n);
*len += n;
buf[*len] = '\0';
return true;
}
static bool morse_is_gap(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
}
// How many bytes of s can be kept without exceeding max OR splitting a UTF-8
// sequence. Half a sequence would make the JSON body invalid and cost the whole
// reply, so the cut walks back to the nearest lead byte.
//
// Only the echoed input needs this - the generated code is all ASCII.
static size_t morse_utf8_trim(const char* s, size_t max) {
size_t n = strlen(s);
if (n <= max) { return n; }
// Continuation bytes are 10xxxxxx. Dropping back past them lands either on
// ASCII or on the lead byte of the sequence being cut, and that lead byte
// is then excluded too, since the kept range is [0, n).
n = max;
while (n > 0 && ((unsigned char)s[n] & 0xC0) == 0x80) { n--; }
return n;
}
// Instant command: runs on the gateway thread, sends a fresh response.
void hammy_cmd_morse(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb) {
// Get the text argument from the job
const char* text = hammy_job_get_arg(job, "text");
if (!text) {
hammy_job_respond(job, client, "Text Missing!", "No Text provided for Morse code conversion.", true);
return;
}
// Echoed back trimmed rather than whole: sizing a buffer from the option
// would put an unbounded, user-controlled allocation on the stack.
char echo[HAMMY_MORSE_ECHO_MAX + sizeof(HAMMY_MORSE_TRUNCATED)];
size_t echoLen = morse_utf8_trim(text, HAMMY_MORSE_ECHO_MAX);
memcpy(echo, text, echoLen);
echo[echoLen] = '\0';
if (text[echoLen] != '\0') {
memcpy(echo + echoLen, HAMMY_MORSE_TRUNCATED, sizeof(HAMMY_MORSE_TRUNCATED));
}
char morse[HAMMY_MORSE_CODE_MAX + sizeof(HAMMY_MORSE_TRUNCATED)];
morse[0] = '\0';
// Room held back so the truncation note always fits.
const size_t cap = sizeof(morse) - (sizeof(HAMMY_MORSE_TRUNCATED) - 1);
size_t len = 0;
bool pendingGap = false;
bool truncated = false;
for (const char* p = text; *p; p++) {
// The morse table has no row for whitespace, and rendering it as an
// unknown character would lose the word boundaries. Held rather than
// emitted on the spot so leading, trailing and repeated spaces do not
// stack up slashes.
if (morse_is_gap(*p)) {
pendingGap = (len > 0);
continue;
}
const char* code = NULL;
if (!hammy_refdb_get_morse(refdb, *p, &code)) {
code = "?"; // Handle unknown characters
}
if (!morse_append(morse, cap, &len, pendingGap, code)) {
truncated = true;
break;
}
pendingGap = false;
}
// All whitespace, or an empty string: Discord refuses an empty body, so
// there is nothing to send back.
if (len == 0) {
hammy_job_respond(job, client, "Error", "Nothing to convert.", true);
return;
}
if (truncated) {
memcpy(morse + len, HAMMY_MORSE_TRUNCATED, sizeof(HAMMY_MORSE_TRUNCATED));
}
// Reply with the converted Morse code, alongside the text it came from.
char body[sizeof(echo) + sizeof(morse) + 32];
snprintf(body, sizeof(body), "Original text: `%s`\nMorse: `%s`", echo, morse);
hammy_job_respond(job, client, "Morse Code Conversion", body, false);
}
+144
View File
@@ -0,0 +1,144 @@
#include <concord/discord.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <hammy/command.h>
#include <hammy/commands.h>
#include <hammy/job.h>
#include <hammy/refdb.h>
#include <hammy/utils.h>
// 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,
// user-controlled VLA off the stack: a command option runs to 6000 characters,
// and every one of them can expand to a whole word.
#define HAMMY_PHONETIC_CODE_MAX 1536
#define HAMMY_PHONETIC_PRONUNCIATION_MAX 1536
#define HAMMY_PHONETIC_TRUNCATED " ... (truncated)"
// Appends one token, space-separated from whatever is already in the buffer and
// optionally preceded by a word-gap slash. All or nothing: returns false and
// leaves the buffer untouched when the whole thing would not fit, so the caller
// can stop without a half-written word on the end.
//
// len counts bytes excluding the terminator, and the buffer stays terminated
// throughout - which is what the strcat-onto-uninitialised-stack version this
// replaces got wrong: strcat needs a terminated destination to start from, and
// an uninitialised one made the reply random bytes that Discord rejected as
// invalid JSON.
static bool phonetic_append(char* buf, size_t cap, size_t* len, bool gap, const char* token) {
size_t n = strlen(token);
size_t need = (*len > 0 ? 1u : 0u) + (gap ? 2u : 0u) + n;
if (*len + need + 1 > cap) { return false; }
if (*len > 0) { buf[(*len)++] = ' '; }
if (gap) {
buf[(*len)++] = '/';
buf[(*len)++] = ' ';
}
memcpy(buf + *len, token, n);
*len += n;
buf[*len] = '\0';
return true;
}
static bool phonetic_is_gap(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
}
// Instant command: runs on the gateway thread, sends a fresh response.
void hammy_cmd_phonetic(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb) {
// Get the text argument from the job
const char* text = hammy_job_get_arg(job, "text");
if (!text) {
hammy_job_respond(job, client, "Text Missing!", "No Text provided for Phonetic conversion.", true);
return;
}
if (strlen(text) >= 128) {
hammy_job_respond(job, client, "Text Too Long!", "Text provided is too long! Max. 128 characters.", true);
return;
}
bool showPronunciation = strlen(text) < 12;
char phonetic[HAMMY_PHONETIC_CODE_MAX + sizeof(HAMMY_PHONETIC_TRUNCATED)];
char pronunciation[HAMMY_PHONETIC_PRONUNCIATION_MAX + sizeof(HAMMY_PHONETIC_TRUNCATED)];
phonetic[0] = '\0';
pronunciation[0] = '\0';
// Room held back so the truncation note always fits.
const size_t phoneticCap = sizeof(phonetic) - (sizeof(HAMMY_PHONETIC_TRUNCATED) - 1);
const size_t pronunciationCap = sizeof(pronunciation) - (sizeof(HAMMY_PHONETIC_TRUNCATED) - 1);
size_t phoneticLen = 0;
size_t pronunciationLen = 0;
bool pendingGap = false;
bool truncated = false;
for (const char* p = text; *p; p++) {
// The phonetic table has no row for whitespace, and rendering it as an
// unknown character would lose the word boundaries. Held rather than
// emitted on the spot so leading, trailing and repeated spaces do not
// stack up slashes.
if (phonetic_is_gap(*p)) {
pendingGap = (phoneticLen > 0);
continue;
}
const char* code = NULL;
const char* codePronunciation = NULL;
if (!hammy_refdb_get_phonetic(refdb, *p, &code, &codePronunciation)) {
// Both halves get a placeholder: leaving the pronunciation at NULL
// would hand strlen() a null pointer on the next append.
code = "?"; // Handle unknown characters
codePronunciation = "?";
}
// The two halves are read side by side, so they have to stay on the same
// character. A word that fits in one buffer but not the other is rolled
// back out of the first rather than left to skew the columns.
size_t phoneticMark = phoneticLen;
if (!phonetic_append(phonetic, phoneticCap, &phoneticLen, pendingGap, code) ||
!phonetic_append(pronunciation, pronunciationCap, &pronunciationLen, pendingGap, codePronunciation)) {
phoneticLen = phoneticMark;
phonetic[phoneticLen] = '\0';
truncated = true;
break;
}
pendingGap = false;
}
// All whitespace, or an empty string: Discord refuses an empty body, so
// there is nothing to send back.
if (phoneticLen == 0) {
hammy_job_respond(job, client, "Error", "Nothing to convert.", true);
return;
}
if (truncated) {
memcpy(phonetic + phoneticLen, HAMMY_PHONETIC_TRUNCATED, sizeof(HAMMY_PHONETIC_TRUNCATED));
memcpy(pronunciation + pronunciationLen, HAMMY_PHONETIC_TRUNCATED, sizeof(HAMMY_PHONETIC_TRUNCATED));
}
hammy_to_uppercase((char*)text);
// Reply with the phonetic words and how each of them is spoken.
char body[sizeof(phonetic) + sizeof(pronunciation) + 48];
if (showPronunciation) {
snprintf(body, sizeof(body), "Text: `%s`\nPhonetics: `%s`\nPronunciation: `%s`", text, phonetic, pronunciation);
} else {
snprintf(body, sizeof(body), "Text: `(truncated)`\nPhonetics: `%s`", phonetic);
}
hammy_job_respond(job, client, "Text to Phonetics Conversion", body, false);
}
+33
View File
@@ -0,0 +1,33 @@
#include <concord/discord.h>
#include <inttypes.h>
#include <stdio.h>
#include <hammy/command.h>
#include <hammy/commands.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, hammy_refdb_t* refdb) {
(void)refdb;
// 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),
"Gateway: `%d ms`\nHandled in: `%" PRId64 " ms`",
gatewayMs, handledMs);
hammy_job_respond(job, client, "Pong!", body, false);
}
+40
View File
@@ -0,0 +1,40 @@
#include <concord/discord.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <hammy/command.h>
#include <hammy/commands.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((char*)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);
}
+328
View File
@@ -0,0 +1,328 @@
#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>
#include <hammy/embeds.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,
};
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 = (char*)"I don't know that command!"
};
code = discord_create_interaction_response(client, event->id, event->token, &params, 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, bot->refdb);
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, "Bot is Busy", "I am a bit busy right now! Please try again later.", true);
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, "Bot is Offline", "Hammy is not accepting commands right now. Please try again shortly.", true);
hammy_job_destroy(&job);
break;
}
}
hammy_bot_t* hammy_bot_create() {
hammy_bot_t* bot = (hammy_bot_t*)calloc(1, sizeof(*bot));
if (!bot) {
return NULL;
}
// Set defaults
bot->client = NULL;
bot->commandsRegistered = false;
bot->commands = vector_create(sizeof(hammy_command_t));
// Check if vector allocation errored
if (!bot->commands) {
free(bot);
return NULL;
}
// Open a reference to sqlite
bot->refdb = hammy_refdb_open("hammy-ref.sqlite"); // TODO: Probably don't hardcode this?
if (!bot->refdb) {
log_error("[bot] Failed to open ref to sqlite. Commands requiring it will be unavailable!"); // TODO: Consider making this a hard-fail
}
return bot;
}
bool hammy_bot_add_command(hammy_bot_t* bot, const hammy_command_t* command) {
if (!bot || !bot->commands || !command || !command->name) { return false; }
// 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;
}
log_info("[bot] Loaded command '%s'.", command->name);
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 true;
}
bool hammy_bot_deregister_all_commands(hammy_bot_t* bot) {
// TODO: This leaks and I don't know why. Figure it out.
if (!bot || !bot->client) { return false; }
struct discord_application_commands cmds = {0};
struct discord_ret_application_commands ret = { .sync = &cmds };
if (discord_get_global_application_commands(bot->client, bot->appId, &ret) == CCORD_OK) {
for (int i = 0; i < cmds.size; i++) {
discord_delete_global_application_command(bot->client, bot->appId, cmds.array[i].id, NULL);
}
} else {
log_error("[bot] Failed to deregister global commands!");
return false;
}
const char* devGuild = getenv("HAMMY_DEV_GUILD");
u64snowflake guildId = devGuild ? (u64snowflake)strtoull(devGuild, NULL, 10) : 0;
if (guildId) {
if (discord_get_guild_application_commands(bot->client, bot->appId, guildId, &ret) == CCORD_OK) {
for (int i = 0; i < cmds.size; i++) {
discord_delete_guild_application_command(bot->client, bot->appId, guildId, cmds.array[i].id, NULL);
}
} else {
log_error("[bot] Failed to deregister dev guild commands!");
return false;
}
}
bot->commandsRegistered = false;
return true;
}
bool hammy_bot_register_commands(hammy_bot_t* bot) {
if (!bot || !bot->client) { return false; }
if (bot->commandsRegistered) { return true; } // Already registered
if (!bot->appId) {
log_error("[bot] No application ID set. Cannot register commands. This should be set in the on_ready() callback.");
return false;
}
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, &params, 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, &params, 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; }
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;
}
if (b->refdb) {
hammy_refdb_close(&b->refdb);
}
free(*bot);
*bot = NULL;
return true;
}
+118
View File
@@ -0,0 +1,118 @@
#include <stddef.h>
#include <string.h>
#include <hammy/command.h>
#include <hammy/commands.h> // Table
// 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.
// 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[] = {
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = (char*)"text",
.description = (char*)"Text to convert to Morse code", .required = true },
};
static struct discord_application_command_options morse_opts_struct = {
.size = 1,
.array = morse_opts
};
static struct discord_application_command_option freq_opts[] = {
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = (char*)"frequency",
.description = (char*)"Frequency in MHz", .required = true },
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = (char*)"country",
.description = (char*)"Country code to look up (e.g. US) - Defaults to US if none specified.", .required = false },
};
static struct discord_application_command_options freq_opts_struct = {
.size = 2,
.array = freq_opts
};
static struct discord_application_command_option q_opts[] = {
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = (char*)"q-code",
.description = (char*)"Q-Code to convert to Question/Answer", .required = true },
};
static struct discord_application_command_options q_opts_struct = {
.size = 1,
.array = q_opts
};
static struct discord_application_command_option abbr_opts[] = {
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = (char*)"abbreviation",
.description = (char*)"Abbreviation to convert to Meaning", .required = true },
};
static struct discord_application_command_options abbr_opts_struct = {
.size = 1,
.array = abbr_opts
};
static struct discord_application_command_option phonetic_opts[] = {
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = (char*)"text",
.description = (char*)"Text to convert to Phonetics", .required = true },
};
static struct discord_application_command_options phonetic_opts_struct = {
.size = 1,
.array = phonetic_opts
};
static struct discord_application_command_option dxcc_opts[] = {
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = (char*)"callsign",
.description = (char*)"Callsign or prefix to look up", .required = true },
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = (char*)"grid",
.description = (char*)"Your Maidenhead locator, for distance and bearing", .required = false },
};
static struct discord_application_command_options dxcc_opts_struct = { .size = 2, .array = dxcc_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.
//
// Field order: name, description, options, handler, instant.
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 },
{ "q", "Convert Q-Code to the corresponding question and answer", &q_opts_struct, &hammy_cmd_q, true },
{ "phonetic", "Convert Text to Phonetics", &phonetic_opts_struct, &hammy_cmd_phonetic, true },
{ "dxcc", "Look up the DXCC entity for a callsign", &dxcc_opts_struct, &hammy_cmd_dxcc, true },
{ "abbr", "Convert Abbreviation to Meaning and Context", &abbr_opts_struct, &hammy_cmd_abbr, true }
// Tier 0 commands go here as they land. All pure computation, so instant:
// { "grid", "Maidenhead locator conversions", &grid_opts, &hammy_cmd_grid, true },
// { "morse", "Text to and from Morse code", &morse_opts, &hammy_cmd_morse, true },
// { "band", "What band is a frequency in", &band_opts, &hammy_cmd_band, true },
//
// Anything touching the backend API, a database or the network is NOT
// instant:
// { "call", "Look up a callsign", &call_opts, &hammy_cmd_call, false },
};
const hammy_command_t* hammy_command_builtins(size_t* outCount) {
if (outCount)
*outCount = sizeof(hammy_builtin_commands) / sizeof(hammy_builtin_commands[0]);
return hammy_builtin_commands;
}
const hammy_command_t* hammy_command_find(const hammy_command_t* commands, size_t count,
const char* name) {
if (!commands || !name) return NULL;
for (size_t i = 0; i < count; i++) {
if (commands[i].name && strcmp(commands[i].name, name) == 0)
return &commands[i];
}
return NULL;
}
bool hammy_command_is_instant(const hammy_command_t* command) {
return command && command->instant;
}
+197
View File
@@ -0,0 +1,197 @@
#include <ctype.h>
#include <math.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <hammy/geo.h>
// M_PI is POSIX, not C99, so it is absent under -std=c99 without _GNU_SOURCE.
// Defining it here keeps the build strict and portable.
#define HAMMY_PI 3.14159265358979323846
// IUGG mean Earth radius. Any of the common radii agree to well within the
// error a 6-character grid square already carries.
#define HAMMY_EARTH_RADIUS_KM 6371.0088
#define HAMMY_EARTH_CIRCUM_KM (2.0 * HAMMY_PI * HAMMY_EARTH_RADIUS_KM)
static double deg_to_rad(double deg) { return deg * HAMMY_PI / 180.0; }
static double rad_to_deg(double rad) { return rad * 180.0 / HAMMY_PI; }
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wbad-function-cast" // suppresses the dumb floor() cast to int warning
bool hammy_grid_from_latlon(double lat, double lon, int precision, char* out, size_t cap) {
if (!out || cap < 3) { return false; }
if (lat < -90.0 || lat > 90.0 || lon < -180.0 || lon > 180.0) { return false; }
if (precision != 2 && precision != 4 && precision != 6 && precision != 8) { return false; }
if (cap < (size_t)precision + 1) { return false; }
// Shift into the all-positive space the locator system is defined over.
double lonA = lon + 180.0;
double latA = lat + 90.0;
// Clamp the poles and the antimeridian so floor() cannot walk off the end
// of the field letters at exactly +90 / +180.
if (lonA >= 360.0) { lonA = 359.999999; }
if (latA >= 180.0) { latA = 179.999999; }
int i = 0;
int f1 = (int)floor(lonA / 20.0);
int f2 = (int)floor(latA / 10.0);
out[i++] = (char)('A' + f1);
out[i++] = (char)('A' + f2);
double remLon = lonA - f1 * 20.0;
double remLat = latA - f2 * 10.0;
if (precision >= 4) {
int s1 = (int)floor(remLon / 2.0);
int s2 = (int)floor(remLat / 1.0);
out[i++] = (char)('0' + s1);
out[i++] = (char)('0' + s2);
remLon -= s1 * 2.0;
remLat -= s2 * 1.0;
}
if (precision >= 6) {
// A square is 2 deg of longitude by 1 deg of latitude, divided 24 ways.
int ss1 = (int)floor(remLon / (2.0 / 24.0));
int ss2 = (int)floor(remLat / (1.0 / 24.0));
if (ss1 > 23) { ss1 = 23; }
if (ss2 > 23) { ss2 = 23; }
out[i++] = (char)('a' + ss1);
out[i++] = (char)('a' + ss2);
remLon -= ss1 * (2.0 / 24.0);
remLat -= ss2 * (1.0 / 24.0);
}
if (precision >= 8) {
int e1 = (int)floor(remLon / (2.0 / 240.0));
int e2 = (int)floor(remLat / (1.0 / 240.0));
if (e1 > 9) { e1 = 9; }
if (e2 > 9) { e2 = 9; }
out[i++] = (char)('0' + e1);
out[i++] = (char)('0' + e2);
}
out[i] = '\0';
return true;
}
bool hammy_grid_to_latlon(const char* grid, double* outLat, double* outLon) {
if (!grid || !outLat || !outLon) { return false; }
char g[HAMMY_GRID_MAX];
size_t n = 0;
for (size_t i = 0; grid[i]; i++) {
if (isspace((unsigned char)grid[i])) { continue; }
// Reject rather than truncate. Silently clipping "IO91wm12345" to
// "IO91wm12" would hand back a plausible-looking position for input the
// user clearly got wrong.
if (n + 1 >= sizeof(g)) { return false; }
g[n++] = grid[i];
}
g[n] = '\0';
// Locators come in even-length pairs; 2, 4, 6 and 8 are the useful ones.
if (n != 2 && n != 4 && n != 6 && n != 8) { return false; }
int f1 = toupper((unsigned char)g[0]) - 'A';
int f2 = toupper((unsigned char)g[1]) - 'A';
if (f1 < 0 || f1 > 17 || f2 < 0 || f2 > 17) { return false; }
double lon = f1 * 20.0;
double lat = f2 * 10.0;
double lonSize = 20.0;
double latSize = 10.0;
if (n >= 4) {
if (!isdigit((unsigned char)g[2]) || !isdigit((unsigned char)g[3])) { return false; }
lon += (g[2] - '0') * 2.0;
lat += (g[3] - '0') * 1.0;
lonSize = 2.0;
latSize = 1.0;
}
if (n >= 6) {
int s1 = tolower((unsigned char)g[4]) - 'a';
int s2 = tolower((unsigned char)g[5]) - 'a';
if (s1 < 0 || s1 > 23 || s2 < 0 || s2 > 23) { return false; }
lon += s1 * (2.0 / 24.0);
lat += s2 * (1.0 / 24.0);
lonSize = 2.0 / 24.0;
latSize = 1.0 / 24.0;
}
if (n >= 8) {
if (!isdigit((unsigned char)g[6]) || !isdigit((unsigned char)g[7])) { return false; }
lon += (g[6] - '0') * (2.0 / 240.0);
lat += (g[7] - '0') * (1.0 / 240.0);
lonSize = 2.0 / 240.0;
latSize = 1.0 / 240.0;
}
// Report the CENTER of the square, not its south-west corner. Using the
// corner biases every distance by up to half a square, which at 4-character
// precision is about 60 km.
*outLon = lon + lonSize / 2.0 - 180.0;
*outLat = lat + latSize / 2.0 - 90.0;
return true;
}
void hammy_great_circle(double lat1, double lon1, double lat2, double lon2,
double* outDistKm, double* outBearingDeg) {
double p1 = deg_to_rad(lat1);
double p2 = deg_to_rad(lat2);
double dp = p2 - p1;
double dl = deg_to_rad(lon2 - lon1);
if (outDistKm) {
// Haversine. The spherical law of cosines is shorter but loses precision
// for short distances, which is exactly the "how far to the next town"
// case people try first.
double a = sin(dp / 2.0) * sin(dp / 2.0)
+ cos(p1) * cos(p2) * sin(dl / 2.0) * sin(dl / 2.0);
if (a > 1.0) { a = 1.0; }
*outDistKm = 2.0 * HAMMY_EARTH_RADIUS_KM * asin(sqrt(a));
}
if (outBearingDeg) {
double y = sin(dl) * cos(p2);
double x = cos(p1) * sin(p2) - sin(p1) * cos(p2) * cos(dl);
double b = rad_to_deg(atan2(y, x));
*outBearingDeg = fmod(b + 360.0, 360.0);
}
}
double hammy_long_path_km(double shortPathKm) {
return HAMMY_EARTH_CIRCUM_KM - shortPathKm;
}
double hammy_reciprocal_bearing(double bearingDeg) {
return fmod(bearingDeg + 180.0, 360.0);
}
const char* hammy_compass_point(double bearingDeg) {
static const char* POINTS[16] = {
"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
"S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"
};
double b = fmod(bearingDeg + 360.0, 360.0);
int idx = (int)floor((b + 11.25) / 22.5) % 16;
return POINTS[idx];
}
#pragma clang diagnostic pop
+209
View File
@@ -0,0 +1,209 @@
#include <concord/discord.h>
#include <concord/log.h>
#include <inttypes.h>
#include <stdlib.h>
#include <string.h>
#include <hammy/bot.h>
#include <hammy/command.h>
#include <hammy/job.h>
#include <hammy/embeds.h>
// strdup() is POSIX, so we'll keep a local and keep the code portable.
static char* hammy_strdup(const char* src) {
if (!src) { return NULL; }
size_t len = strlen(src) + 1; // +1 for the null terminator
char* dst = (char*)malloc(len);
if (!dst) { return NULL; }
memcpy(dst, src, len);
return dst;
}
// Logs a failed send, and only a failed send. Concord returns CCORD_PENDING for
// an asynchronous request (which is every request made with a NULL ret, i.e.
// all of ours) - the request has been queued, not rejected, so treating it as
// an error warned on every single reply that actually worked.
static void hammy_job_check_send(const hammy_job_t* job, CCORDcode code) {
if (code == CCORD_OK || code == CCORD_PENDING) { return; }
log_warn("[job] Failed to send interaction response for interaction %" PRIu64 ": %s (%d)",
job->id, ccord_strerror(code), code);
}
// Pulls the invoking user out of the event. Guild interactions carry it under
// member->user, DM interactions under user directly.
static u64snowflake hammy_job_extract_user(const struct discord_interaction* event) {
if (event->member && event->member->user) return event->member->user->id;
if (event->user) return event->user->id;
return 0;
}
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->user = hammy_job_extract_user(event);
job->token = hammy_strdup(event->token);
job->queuedAt = (int64_t)discord_timestamp(bot->client);
job->appId = event->application_id ? event->application_id : bot->appId;
if (!job->token) {
goto fail;
}
if (event->data && event->data->name) {
job->command = hammy_strdup(event->data->name);
if (!job->command) {
goto fail;
}
}
// Flatten the top-level options. Subcommand groups nest another options array inside an option
// Not handled yet, and worth revisiting before we need one; TODO
if (event->data && event->data->options && event->data->options->size > 0) {
size_t n = (size_t)event->data->options->size;
job->args = (hammy_arg_t*)calloc(n, sizeof(*job->args));
if (!job->args) {
goto fail;
}
for (size_t i = 0; i < n; i++) {
struct discord_application_command_interaction_data_option* opt = &event->data->options->array[i];
job->args[i].name = hammy_strdup(opt->name);
job->args[i].value = hammy_strdup(opt->value);
// A NULL value is legitimate for a flag-style option; a NULL name
// after a non-NULL source is an allocation failure.
if (opt->name && !job->args[i].name) goto fail;
if (opt->value && !job->args[i].value) goto fail;
job->nArgs++;
}
}
return job;
fail:
hammy_job_destroy(&job);
return NULL;
}
bool hammy_job_destroy(hammy_job_t** job) {
if (!job || !*job) { return false; }
hammy_job_t* j = *job;
for (size_t i = 0; i < j->nArgs; i++) {
free(j->args[i].name);
free(j->args[i].value);
}
free(j->args);
free(j->token);
free(j->command);
free(j);
*job = NULL;
return true;
}
const char* hammy_job_get_arg(const hammy_job_t* job, const char* name) {
if (!job || !name) { return NULL; }
for (size_t i = 0; i < job->nArgs; i++) {
if (job->args[i].name && strcmp(job->args[i].name, name) == 0) {
return job->args[i].value;
}
}
return NULL;
}
int64_t hammy_job_age_ms(const hammy_job_t* job, struct discord* client) {
if (!job || !client) { return 0; }
return (int64_t)discord_timestamp(client) - job->queuedAt;
}
void hammy_job_reply(const hammy_job_t* job, struct discord* client, const char* title, const char* content, bool isError) {
if (!job || !client || !content || !title) { return; }
// Editing, not creating: every caller is on the deferred path, where the
// gateway thread already sent DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE. A second
// create on the same interaction is refused with "Interaction has already
// been acknowledged" (40060). Instant handlers want hammy_job_respond().
struct discord_edit_original_interaction_response params = { 0 };
// Both must outlive the call, so neither can be a compound literal inside
// the branch below.
struct discord_embed embed[1];
struct discord_embeds embeds = { .size = 1, .array = embed };
if (hammy_embeds_customembed(client, embed, NULL, title, content, NULL, 0, isError ? 0xFF0000 : 0x00FF00)) {
params.embeds = &embeds;
} else {
// Only reachable on a bad argument, but a plain-text body still beats
// sending nothing at all.
params.content = (char*)content;
}
hammy_job_check_send(job, discord_edit_original_interaction_response(client, job->appId, job->token, &params, NULL));
}
void hammy_job_respond(const hammy_job_t* job, struct discord* client, const char* title, const char* content, bool isError) {
if (!job || !client || !content || !title) { return; }
struct discord_embed embed[1];
if (hammy_embeds_customembed(client, embed, NULL, title, content, NULL, 0, isError ? 0xFF0000 : 0x00FF00)) {
struct discord_interaction_response params = {
.type = DISCORD_INTERACTION_CHANNEL_MESSAGE_WITH_SOURCE,
.data = &(struct discord_interaction_callback_data){
.embeds = &(struct discord_embeds){
.size = 1,
.array = embed
}
}
};
hammy_job_check_send(job, discord_create_interaction_response(client, job->id, job->token, &params, NULL));
} else {
struct discord_interaction_response params = {
.type = DISCORD_INTERACTION_CHANNEL_MESSAGE_WITH_SOURCE,
.data = &(struct discord_interaction_callback_data){
.content = (char*)content
}
};
hammy_job_check_send(job, discord_create_interaction_response(client, job->id, job->token, &params, NULL));
}
}
void hammy_job_run(hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb) {
if (!job || !client) { return; }
if (!refdb) {
// TODO: Consider making this a hard-fail
log_warn("[job] Ran command '%s' for interaction %" PRIu64 " without a valid refdb! SQLite functions will be unavailable!", 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, "Unknown Command", "I don't know that command!", true);
return;
}
command->handler(job, client, refdb);
}
+195
View File
@@ -0,0 +1,195 @@
#include <concord/discord.h>
#include <concord/log.h>
#include <stdlib.h>
#include <hammy/bot.h>
#include <hammy/job.h>
#include <hammy/pool.h>
#include <hammy/worker.h>
hammy_pool_t* hammy_pool_create(struct discord* client, size_t nWorkers, size_t queueCap) {
if (!client) { return NULL; }
if (nWorkers == 0) { nWorkers = HAMMY_POOL_DEFAULT_WORKERS; }
if (queueCap == 0) { queueCap = HAMMY_POOL_DEFAULT_CAPACITY; }
hammy_pool_t* pool = (hammy_pool_t*)calloc(1, sizeof(*pool));
if (!pool) { return NULL; }
pool->cap = queueCap;
pool->jobs = (hammy_job_t**)calloc(pool->cap, sizeof(*pool->jobs));
if (!pool->jobs) {
goto fail_jobs;
}
pool->workers = (hammy_worker_t*)calloc(nWorkers, sizeof(*pool->workers));
if (!pool->workers) {
goto fail_workers;
}
if (pthread_mutex_init(&pool->lock, NULL) != 0) {
goto fail_mutex;
}
if (pthread_cond_init(&pool->notEmpty, NULL) != 0) {
goto fail_cond;
}
// nWorkers counts STARTED threads, so a partial failure below still joins
// exactly the ones that exist.
for (size_t i = 0; i < nWorkers; i++) {
if (!hammy_worker_start(&pool->workers[i], pool, client, (int)i)) {
log_error("[pool] only %zu of %zu workers started, bailing!", i, nWorkers);
hammy_pool_shutdown(pool);
hammy_pool_destroy(&pool);
return NULL;
}
pool->nWorkers++;
}
log_info("[pool] Started %zu workers, queue cap %zu", pool->nWorkers, pool->cap);
return pool;
// GOTOs
fail_cond:
pthread_mutex_destroy(&pool->lock);
fail_mutex:
free(pool->workers);
fail_workers:
free(pool->jobs);
fail_jobs:
free(pool);
return NULL;
}
hammy_push_result_t hammy_pool_push(hammy_pool_t* pool, hammy_job_t* job) {
if (!pool || !job) { return HAMMY_PUSH_SHUTDOWN; }
pthread_mutex_lock(&pool->lock);
if (pool->shutdown) {
pthread_mutex_unlock(&pool->lock);
return HAMMY_PUSH_SHUTDOWN;
}
if (pool->count == pool->cap) {
pthread_mutex_unlock(&pool->lock);
return HAMMY_PUSH_FULL;
}
pool->jobs[pool->tail] = job;
pool->tail = (pool->tail + 1) % pool->cap;
pool->count++;
// Signal inside the lock, wakeup cost kinda irrlevant compared to HTTP round trips
pthread_cond_signal(&pool->notEmpty);
pthread_mutex_unlock(&pool->lock);
return HAMMY_PUSH_OK;
}
static void hammy_pool_stop(hammy_pool_t* pool, bool drain) {
if (!pool) { return; }
pthread_mutex_lock(&pool->lock);
if (pool->shutdown) {
pthread_mutex_unlock(&pool->lock);
return;
}
pool->shutdown = true;
// 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--;
// 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); }
}
}
// Broadcast (NOT signal), every watier has to see the shutdown flag and exit, not just one.
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, "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);
}
free(dropped);
for (size_t i = 0; i < pool->nWorkers; i++) {
hammy_worker_join(&pool->workers[i]);
}
log_info("[pool] Shut down");
}
void hammy_pool_shutdown(hammy_pool_t* pool) {
hammy_pool_stop(pool, true);
}
void hammy_pool_shutdown_now(hammy_pool_t* pool) {
hammy_pool_stop(pool, false);
}
bool hammy_pool_destroy(hammy_pool_t** pool) {
if (!pool || !*pool) { return false; }
hammy_pool_t* p = *pool;
hammy_pool_shutdown(p); // No-op if already shut down
// Anything still queued after the drain is a but - free rather than leak it.
while (p->count > 0) {
hammy_job_t* job = p->jobs[p->head];
p->head = (p->head + 1) % p->cap;
p->count--;
hammy_job_destroy(&job);
}
pthread_cond_destroy(&p->notEmpty);
pthread_mutex_destroy(&p->lock);
free(p->workers);
free(p->jobs);
free(p);
*pool = NULL;
return true;
}
void hammy_pool_stats(hammy_pool_t* pool, size_t* outQueued, size_t* outBusy) {
if (!pool) { return; }
pthread_mutex_lock(&pool->lock);
if (outBusy) { *outBusy = pool->busy; }
if (outQueued) { *outQueued = pool->count; }
pthread_mutex_unlock(&pool->lock);
}
+850
View File
@@ -0,0 +1,850 @@
#include <concord/log.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <hammy/refdb.h>
// Whole-callsign rules (cty.dat's "=CALL" entries) must match the entire string.
static const char SQL_EXACT[] =
"SELECT e.id, e.name, e.continent,"
" COALESCE(p.cq_zone, e.cq_zone), COALESCE(p.itu_zone, e.itu_zone),"
" e.latitude, e.longitude, e.utc_offset, p.prefix"
" FROM dxcc_prefixes p JOIN dxcc_entities e ON e.id = p.entity_id"
" WHERE p.prefix = ?1 AND p.exact = 1"
" LIMIT 1";
// One candidate at a time, called longest-first by the caller.
static const char SQL_PREFIX[] =
"SELECT e.id, e.name, e.continent,"
" COALESCE(p.cq_zone, e.cq_zone), COALESCE(p.itu_zone, e.itu_zone),"
" e.latitude, e.longitude, e.utc_offset, p.prefix"
" FROM dxcc_prefixes p JOIN dxcc_entities e ON e.id = p.entity_id"
" WHERE p.prefix = ?1 AND p.exact = 0"
" LIMIT 1";
static const char SQL_VERSION[] =
"SELECT value FROM ref_meta WHERE key = 'bundle_version'";
static const char SQL_MORSE[] =
"SELECT code FROM morse WHERE character = UPPER(?1) LIMIT 1";
// Band plus per-class privileges in one pass.
//
// Both LEFT JOINs matter. They are what makes "Technician: not permitted here"
// appear as a row rather than vanishing - a user excluded from a segment needs
// to be told so, not shown a shorter list.
//
// Boundary convention differs between the tables on purpose:
// bands inclusive at both ends. 14.350 is still "20m".
// band_segments half-open [low, high). 14.150 is the START of the phone
// segment, not the end of the CW one. BETWEEN would match
// both and the command would print contradictory modes.
static const char SQL_FREQ_MAIN[] =
"SELECT b.name, b.edge_low_hz, b.edge_high_hz,"
" lc.code, lc.name, lc.rank,"
" s.modes, s.low_hz, s.high_hz, s.max_power_w, s.notes"
" FROM bands b"
" LEFT JOIN license_classes lc"
" ON lc.country = ?2"
" LEFT JOIN band_segments s"
" ON s.band_id = b.id"
" AND s.class_id = lc.id"
" AND s.country = ?2"
" AND s.low_hz <= ?1"
" AND ?1 < s.high_hz"
" WHERE b.edge_low_hz <= ?1 AND ?1 <= b.edge_high_hz"
" ORDER BY lc.rank DESC, s.low_hz";
// Regional allocation, independent of national licensing. country = '' and
// class_id IS NULL mark these rows: they say what the band IS in a region, not
// who may use it.
static const char SQL_FREQ_IARU[] =
"SELECT s.iaru_region, s.low_hz, s.high_hz, s.modes"
" FROM band_segments s"
" WHERE s.country = '' AND s.class_id IS NULL"
" AND s.low_hz <= ?1 AND ?1 < s.high_hz"
" ORDER BY s.iaru_region";
// min() with two arguments is SQLite's scalar min, not the aggregate.
static const char SQL_FREQ_NEAREST[] =
"SELECT name, edge_low_hz, edge_high_hz,"
" min(abs(edge_low_hz - ?1), abs(edge_high_hz - ?1))"
" FROM bands ORDER BY 4 LIMIT 1";
// Is the frequency exactly on a segment boundary for this country?
static const char SQL_FREQ_SEG_EDGE[] =
"SELECT 1 FROM band_segments"
" WHERE country = ?2 AND (low_hz = ?1 OR high_hz = ?1) LIMIT 1";
static const char SQL_COUNTRY_LIST[] =
"SELECT DISTINCT country FROM license_classes"
" WHERE country <> '' ORDER BY country";
// Does the bundle carry licence data for this country at all? Only US is seeded
// so far, so "no data yet" is the common answer and needs saying properly.
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";
// Resolve character to phonetic
static const char SQL_PHONETIC[] =
"SELECT word, pronunciation FROM phonetics WHERE letter = UPPER(?1) LIMIT 1";
// Resolve abbr to meaning
static const char SQL_ABBR[] =
"SELECT meaning, context FROM abbreviations WHERE abbr = UPPER(?1) LIMIT 1";
// Authorizer: the connection may read, and nothing else.
//
// SQLITE_OPEN_READONLY protects the MAIN database file. It does not stop
// "ATTACH DATABASE 'somewhere.db' AS x" - an attached database is a separate
// file with its own flags, so a read-only connection is not automatically a
// read-only process. SQLITE_LIMIT_ATTACHED below closes that, and this
// authorizer closes it again, because defence in depth costs nothing here.
static int hammy_refdb_authorizer(void* unused, int action,
const char* a1, const char* a2,
const char* dbname, const char* trigger) {
(void)unused; (void)a1; (void)a2; (void)dbname; (void)trigger;
switch (action) {
case SQLITE_SELECT:
case SQLITE_READ:
case SQLITE_FUNCTION: // COALESCE, length(), upper() and friends
return SQLITE_OK;
default:
// INSERT, UPDATE, DELETE, ATTACH, DETACH, CREATE/DROP anything,
// PRAGMA, transactions - all refused at prepare time.
return SQLITE_DENY;
}
}
// Every prepared statement is registered in HAMMY_STATEMENTS below, exactly
// once. open() and close() both walk that table, so adding a statement means
// adding one row - not a struct field plus a prepare call plus a finalize call,
// three places that can silently drift apart.
//
// That drift is what produced the stCountryKnown crash: the field and the SQL
// existed, the prepare didn't, and sqlite3_clear_bindings() dereferenced a NULL
// left over from calloc(). Now it's impossible to reach a slot except through
// the table.
// NOTE: the SQL_* constants above are declared `static const char X[]`, not
// `static const char* X`. A pointer variable is not a constant expression, so
// using one in this table's static initializer is a compile error
// ("initializer element is not constant"). An array's address is a link-time
// constant and works. Don't "tidy" them back into pointers.
typedef struct {
const char* name; // for log messages
size_t offset; // into struct hammy_refdb_t
const char* sql;
} hammy_stmt_def_t;
#define HAMMY_STMT(field, sqlConst) \
{ #field, offsetof(struct hammy_refdb_t, field), sqlConst }
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(stPhonetic, SQL_PHONETIC),
HAMMY_STMT(stAbbr, SQL_ABBR),
HAMMY_STMT(stFreqMain, SQL_FREQ_MAIN),
HAMMY_STMT(stFreqIaru, SQL_FREQ_IARU),
HAMMY_STMT(stFreqNearest, SQL_FREQ_NEAREST),
HAMMY_STMT(stFreqSegEdge, SQL_FREQ_SEG_EDGE),
HAMMY_STMT(stCountryKnown, SQL_COUNTRY_KNOWN),
HAMMY_STMT(stCountryList, SQL_COUNTRY_LIST),
};
#define HAMMY_STMT_COUNT (sizeof(HAMMY_STATEMENTS) / sizeof(HAMMY_STATEMENTS[0]))
static sqlite3_stmt** stmt_slot(hammy_refdb_t* db, const hammy_stmt_def_t* def) {
return (sqlite3_stmt**)((char*)db + def->offset);
}
// sqlite3_reset(NULL) is harmless, but sqlite3_clear_bindings(NULL) dereferences
// straight away. Query entry points check their statements before touching them,
// so a half-built refdb returns false instead of taking the process down.
static bool stmts_ready(const char* where, sqlite3_stmt* const* stmts, size_t count) {
for (size_t i = 0; i < count; i++) {
if (!stmts[i]) {
log_error("[refdb] %s called with unprepared statements", where);
return false;
}
}
return true;
}
static bool prepare(sqlite3* h, const char* sql, sqlite3_stmt** out) {
if (sqlite3_prepare_v2(h, sql, -1, out, NULL) != SQLITE_OK) {
log_error("[refdb] prepare failed: %s", sqlite3_errmsg(h));
log_error("[refdb] sql: %s", sql);
// prepare_v2 doesn't reliably clear this on every error path, and a
// garbage pointer would sail past the NULL checks below.
*out = NULL;
return false;
}
return true;
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wjump-misses-init" // suppresses the "jumped initialization of varaible" warning, has no effect here
hammy_refdb_t* hammy_refdb_open(const char* path) {
if (!path) { return NULL; }
hammy_refdb_t* db = (hammy_refdb_t*)calloc(1, sizeof(*db));
if (!db) { return NULL; }
// immutable=1 promises SQLite the fill won't change while it's open. Skips locks and change-counter checks
// Faster and stricter than plain read-only
char uri[1024];
snprintf(uri, sizeof(uri), "file:%s?mode=ro&immutable=1", path);
// NOMUTEX - exactly one thread uses this handle, avoid serialization overhead. Safe only because of the one-per-thread rule.
int flags = SQLITE_OPEN_READONLY | SQLITE_OPEN_NOMUTEX | SQLITE_OPEN_URI;
if (sqlite3_open_v2(uri, &db->handle, flags, NULL) != SQLITE_OK) {
log_error("[refdb] cannot open %s: %s", path, db->handle ? sqlite3_errmsg(db->handle) : "out of memory");
goto fail;
}
// No ATTACH; closes a way a read-only connection could still open writable files.
sqlite3_limit(db->handle, SQLITE_LIMIT_ATTACHED, 0);
// Bound the damage of a pathological query. Turns a runaway into an error instead of a stall. Keeps plenty of headroom for actual lookups.
sqlite3_limit(db->handle, SQLITE_LIMIT_SQL_LENGTH, 8192);
sqlite3_limit(db->handle, SQLITE_LIMIT_EXPR_DEPTH, 100);
sqlite3_limit(db->handle, SQLITE_LIMIT_LIKE_PATTERN_LENGTH, 256);
sqlite3_limit(db->handle, SQLITE_LIMIT_VARIABLE_NUMBER, 32);
// Refuse schema-corrupting tricks (e.g. PRAGMA writable_schema) and stop the schema itself from invoking unvetted functions.
sqlite3_db_config(db->handle, SQLITE_DBCONFIG_DEFENSIVE, 1, NULL);
sqlite3_db_config(db->handle, SQLITE_DBCONFIG_TRUSTED_SCHEMA, 0, NULL);
// The bundle is small enough to just map memory-whole. Avoids a read() per page. Set before authorizer since it denies PRAGMA.
sqlite3_exec(db->handle, "PRAGMA mmap_size = 67108864;", NULL, NULL, NULL);
// TODO: PLEASE chmod 444 OR SOMETHING AND DON'T RUN AS THE SQLITE DB FILE OWNER - THAT'S THE ONLY REAL PROTECTION
sqlite3_set_authorizer(db->handle, &hammy_refdb_authorizer, NULL);
for (size_t i = 0; i < HAMMY_STMT_COUNT; i++) {
const hammy_stmt_def_t* def = &HAMMY_STATEMENTS[i];
if (!prepare(db->handle, def->sql, stmt_slot(db, def))) {
log_error("[refdb] statement '%s' failed to prepare", def->name);
goto fail;
}
}
// Belt and braces. A registry that drifts from the struct would otherwise
// leave a NULL slot that crashes inside SQLite on first use, far from the
// cause. Fail here instead, naming the statement.
for (size_t i = 0; i < HAMMY_STMT_COUNT; i++) {
if (!(*stmt_slot(db, &HAMMY_STATEMENTS[i]))) {
log_error("[refdb] statement '%s' is NULL after prepare", HAMMY_STATEMENTS[i].name);
goto fail;
}
}
sqlite3_stmt* st = NULL;
if (prepare(db->handle, SQL_VERSION, &st)) {
if (sqlite3_step(st) == SQLITE_ROW) {
const unsigned char* v = sqlite3_column_text(st, 0);
if (v) {
snprintf(db->version, sizeof(db->version), "%s", (const char*)v);
}
}
sqlite3_finalize(st);
}
return db;
fail:
hammy_refdb_close(&db);
return NULL;
}
#pragma GCC diagnostic pop
bool hammy_refdb_close(hammy_refdb_t** db) {
if (!db || !(*db)) { return false; }
hammy_refdb_t* d = *db;
// Statements must be finalized before the connection closes, or we're in deep shit
// (sqlite3_close() returns SQLITE_BUSY and the handle leaks)
for (size_t i = 0; i < HAMMY_STMT_COUNT; i++) {
sqlite3_stmt** slot = stmt_slot(d, &HAMMY_STATEMENTS[i]);
if (*slot) {
sqlite3_finalize(*slot);
*slot = NULL;
}
}
if (d->handle) { sqlite3_close(d->handle); }
free(d);
*db = NULL;
return true;
}
const char* hammy_refdb_version(hammy_refdb_t* db) {
return (db && db->version[0]) ? db->version : NULL;
}
// Uppercase, strip whitespace, keep only characters that appear in callsigns (voodoo).
static void normalise(const char* in, char* out, size_t cap) {
size_t j = 0;
for (size_t i = 0; in[i] && j + 1 < cap; i++) {
unsigned char c = (unsigned char)in[i];
if (c >= 'a' && c <= 'z') { c = (unsigned char)(c - 'a' + 'A'); }
if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '/') {
out[j++] = (char)c;
}
}
out[j] = '\0';
}
static void fill(sqlite3_stmt* st, hammy_dxcc_t* out, bool exact) {
out->entityId = sqlite3_column_int(st, 0);
const unsigned char* name = sqlite3_column_text(st, 1);
const unsigned char* cont = sqlite3_column_text(st, 2);
const unsigned char* pfx = sqlite3_column_text(st, 8);
snprintf(out->name, sizeof(out->name), "%s", name ? (const char*)name : "");
snprintf(out->continent, sizeof(out->continent), "%s", cont ? (const char*)cont : "");
snprintf(out->matchedPrefix, sizeof(out->matchedPrefix), "%s", pfx ? (const char*)pfx : "");
out->cqZone = sqlite3_column_int(st, 3);
out->ituZone = sqlite3_column_int(st, 4);
out->latitude = sqlite3_column_double(st, 5);
out->longitude = sqlite3_column_double(st, 6);
out->utcOffset = sqlite3_column_double(st, 7);
out->exact = exact;
}
// Runs one candidate against a prepared statement. Returns true on a hit.
static bool try_one(sqlite3_stmt* st, const char* candidate, size_t len, hammy_dxcc_t* out, bool exact) {
sqlite3_reset(st);
sqlite3_clear_bindings(st);
sqlite3_bind_text(st, 1, candidate, (int)len, SQLITE_STATIC);
bool hit = (sqlite3_step(st) == SQLITE_ROW);
if (hit) { fill(st, out, exact); }
sqlite3_reset(st);
return hit;
}
// Longest-first prefix search over one string.
static bool search_prefixes(hammy_refdb_t* db, const char* call, hammy_dxcc_t* out) {
size_t len = strlen(call);
for (size_t n = len; n > 0; n--) {
if (try_one(db->stPrefix, call, n, out, false)) { return true; }
}
return false;
}
bool hammy_refdb_dxcc(hammy_refdb_t* db, const char* callsign, hammy_dxcc_t* out) {
if (!db || !callsign || !out) { return false; }
sqlite3_stmt* const needed[] = { db->stExact, db->stPrefix };
if (!stmts_ready("dxcc", needed, 2)) { return false; }
char call[HAMMY_CALLSIGN_MAX];
normalise(callsign, call, sizeof(call));
if (!call[0]) { return false; }
memset(out, 0, sizeof(*out));
// 1. Whole-callsign rules win outright. cty.dat carries a couple of thousand
// of these for stations that do not follow their entity's prefix pattern.
if (try_one(db->stExact, call, strlen(call), out, true))
return true;
// 2. Portable designators. "DL/W1AW" is Germany, "W1AW/4" is still the US.
// The rule of thumb is that the SHORTER side of the slash is the location
// indicator, and a purely numeric tail is a call-area change rather than
// an entity change.
//
// This is a heuristic, not a specification - real callsign parsing has
// genuine ambiguities and every logging program handles them slightly
// differently. Good enough for a lookup command; revisit before using it
// to award DXCC credit. TODO
const char* slash = strchr(call, '/');
if (slash) {
char left[HAMMY_CALLSIGN_MAX] = {0};
char right[HAMMY_CALLSIGN_MAX] = {0};
size_t llen = (size_t)(slash - call);
snprintf(left, sizeof(left), "%.*s", (int)llen, call);
snprintf(right, sizeof(right), "%s", slash + 1);
bool right_numeric = true;
for (const char* p = right; *p; p++) {
if (*p < '0' || *p > '9') {
right_numeric = false;
break;
}
}
// A numeric tail keeps the home entity: try the left side only.
if (right_numeric && right[0]) { return search_prefixes(db, left, out); }
// Otherwise the shorter side is the location indicator.
const char* location = (strlen(right) && strlen(right) < llen) ? right : left;
const char* fallback = (location == right) ? left : right;
if (search_prefixes(db, location, out)) { return true; }
return search_prefixes(db, fallback, out);
}
// 3. Plain callsign: longest prefix wins.
return search_prefixes(db, call, out);
}
bool hammy_refdb_get_morse(hammy_refdb_t* db, char c, const char** out) {
if (!db || !out) { return false; }
sqlite3_stmt* const needed[] = { db->stMorse };
if (!stmts_ready("morse", needed, 1)) { return false; }
// The table is ASCII (ITU-R M.1677-1 has no non-Latin extensions), and a
// single byte out of a multi-byte UTF-8 sequence is not valid text to bind,
// so anything with the high bit set is a miss without touching SQLite.
if ((unsigned char)c & 0x80u) { return false; }
// Run the query with the character as a string. SQLite's UPPER() handles case-insensitivity.
// SQLITE_STATIC is safe because the statement is reset before this returns,
// so key never outlives the binding that points at it.
char key[2] = { c, '\0' };
sqlite3_reset(db->stMorse);
sqlite3_clear_bindings(db->stMorse);
sqlite3_bind_text(db->stMorse, 1, key, 1, SQLITE_STATIC);
bool hit = false;
if (sqlite3_step(db->stMorse) == SQLITE_ROW) {
const unsigned char* code = sqlite3_column_text(db->stMorse, 0);
if (code) {
// Copied rather than handed back directly: the column pointer dies
// at the reset below, and the caller has no way to know that.
snprintf(db->morseCode, sizeof(db->morseCode), "%s", (const char*)code);
*out = db->morseCode;
hit = true;
}
}
sqlite3_reset(db->stMorse);
return hit;
}
bool hammy_refdb_get_phonetic(hammy_refdb_t* db, char c, const char** out, const char** outPronunciation) {
if (!db || !out) { return false; }
sqlite3_stmt* const needed[] = { db->stPhonetic };
if (!stmts_ready("phonetic", needed, 1)) { return false; }
// The table is ASCII (ITU-R M.1677-1 has no non-Latin extensions), and a
// single byte out of a multi-byte UTF-8 sequence is not valid text to bind,
// so anything with the high bit set is a miss without touching SQLite.
if ((unsigned char)c & 0x80u) { return false; }
// Run the query with the character as a string. SQLite's UPPER() handles case-insensitivity.
// SQLITE_STATIC is safe because the statement is reset before this returns,
// so key never outlives the binding that points at it.
char key[2] = { c, '\0' };
sqlite3_reset(db->stPhonetic);
sqlite3_clear_bindings(db->stPhonetic);
sqlite3_bind_text(db->stPhonetic, 1, key, 1, SQLITE_STATIC);
bool hit = false;
if (sqlite3_step(db->stPhonetic) == SQLITE_ROW) {
if (sqlite3_column_count(db->stPhonetic) < 2) {
// Error
sqlite3_reset(db->stPhonetic);
return false;
}
const unsigned char* code = sqlite3_column_text(db->stPhonetic, 0);
const unsigned char* codePronunciation = sqlite3_column_text(db->stPhonetic, 1);
if (code && codePronunciation) {
// Copied rather than handed back directly: the column pointer dies
// at the reset below, and the caller has no way to know that.
snprintf(db->phoneticCode, sizeof(db->phoneticCode), "%s", (const char*)code);
*out = db->phoneticCode;
snprintf(db->phoneticCodePronunciation, sizeof(db->phoneticCodePronunciation), "%s", (const char*)codePronunciation);
*outPronunciation = db->phoneticCodePronunciation;
hit = true;
}
}
sqlite3_reset(db->stPhonetic);
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;
}
bool hammy_refdb_get_abbr(hammy_refdb_t* db, const char* code, const char** outStr, const char** outContext) {
if (!db || !code || !outStr || !outContext) { return false; }
sqlite3_stmt* const needed[] = { db->stAbbr };
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->stAbbr);
sqlite3_clear_bindings(db->stAbbr);
sqlite3_bind_text(db->stAbbr, 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->stAbbr) == SQLITE_ROW) {
if (sqlite3_column_count(db->stAbbr) < 2) { // Error
sqlite3_reset(db->stAbbr);
return false;
}
const unsigned char* meaningStr = sqlite3_column_text(db->stAbbr, 0);
const unsigned char* ctxStr = sqlite3_column_text(db->stAbbr, 1);
if (meaningStr) {
snprintf(db->abbrStr, sizeof(db->abbrStr), "%s", (const char*)meaningStr);
*outStr = db->abbrStr;
hit = true;
}
if (ctxStr) {
snprintf(db->abbrCtx, sizeof(db->abbrCtx), "%s", (const char*)ctxStr);
*outContext = db->abbrCtx;
hit = true;
}
}
sqlite3_reset(db->stAbbr);
return hit;
}
// ---------------------------------------------------------------------------
// Frequency parsing
// ---------------------------------------------------------------------------
// Copies a TEXT column into a fixed buffer, tolerating NULL.
static void copy_text(sqlite3_stmt* st, int col, char* dst, size_t cap) {
const unsigned char* v = sqlite3_column_text(st, col);
snprintf(dst, cap, "%s", v ? (const char*)v : "");
}
bool hammy_freq_parse(const char* text, int64_t* outHz) {
if (!text || !outHz) { return false; }
while (*text == ' ' || *text == '\t') { text++; }
// Integer part.
int64_t whole = 0;
bool anyDigit = false;
while (*text >= '0' && *text <= '9') {
if (whole > INT64_MAX / 10 - 9) { return false; } // absurd input
whole = whole * 10 + (*text - '0');
anyDigit = true;
text++;
}
// Fractional part, accumulated as digits rather than a double. Six decimal
// places of MHz is exactly 1 Hz, so anything beyond that is discarded.
int64_t frac = 0;
int fracDigits = 0;
if (*text == '.' || *text == ',') {
text++;
while (*text >= '0' && *text <= '9') {
if (fracDigits < 9) {
frac = frac * 10 + (*text - '0');
fracDigits++;
}
anyDigit = true;
text++;
}
}
if (!anyDigit) { return false; }
while (*text == ' ' || *text == '\t') { text++; }
// Unit. A bare number means MHz, which is what people type.
int64_t mult = 1000000; // Hz per unit
if (*text) {
char unit[8] = {0};
size_t i = 0;
while (text[i] && i + 1 < sizeof(unit) && text[i] != ' ') {
char c = text[i];
unit[i] = (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c;
i++;
}
if (!strcmp(unit, "hz")) { mult = 1; }
else if (!strcmp(unit, "khz") || !strcmp(unit, "k")) { mult = 1000; }
else if (!strcmp(unit, "mhz") || !strcmp(unit, "m")) { mult = 1000000; }
else if (!strcmp(unit, "ghz") || !strcmp(unit, "g")) { mult = 1000000000; }
else { return false; }
}
// Scale the fraction to the unit without ever touching a double.
int64_t scale = 1;
for (int i = 0; i < fracDigits; i++) {
if (scale > INT64_MAX / 10) { return false; }
scale *= 10;
}
if (whole > INT64_MAX / mult) { return false; }
int64_t hz = whole * mult + (frac * mult) / scale;
if (hz <= 0 || hz > 300000000000LL) { return false; } // 1 Hz .. 300 GHz
*outHz = hz;
return true;
}
// ---------------------------------------------------------------------------
// Frequency lookup
// ---------------------------------------------------------------------------
static void normalise_country(const char* in, char* out, size_t cap) {
size_t j = 0;
if (!in || !*in) {
snprintf(out, cap, "US"); // default
return;
}
for (size_t i = 0; in[i] && j + 1 < cap; i++) {
char c = in[i];
if (c >= 'a' && c <= 'z') { c = (char)(c - 'a' + 'A'); }
if (c >= 'A' && c <= 'Z') { out[j++] = c; }
}
out[j] = '\0';
if (!out[0]) { snprintf(out, cap, "US"); }
}
static bool step_bool(sqlite3_stmt* st) {
bool got = (sqlite3_step(st) == SQLITE_ROW);
sqlite3_reset(st);
return got;
}
bool hammy_refdb_freq(hammy_refdb_t* db, int64_t freqHz, const char* country,
hammy_freq_t* out) {
if (!db || !out || freqHz <= 0) { return false; }
sqlite3_stmt* const needed[] = {
db->stFreqMain, db->stFreqIaru, db->stFreqNearest,
db->stFreqSegEdge, db->stCountryKnown
};
if (!stmts_ready("freq", needed, 5)) { return false; }
memset(out, 0, sizeof(*out));
out->freqHz = freqHz;
normalise_country(country, out->country, sizeof(out->country));
// Does the bundle know this country at all? Only US privileges are seeded so
// far, so this is the common path and the message matters.
sqlite3_reset(db->stCountryKnown);
sqlite3_clear_bindings(db->stCountryKnown);
sqlite3_bind_text(db->stCountryKnown, 1, out->country, -1, SQLITE_TRANSIENT);
out->countryKnown = step_bool(db->stCountryKnown);
// Band and privileges.
sqlite3_stmt* st = db->stFreqMain;
sqlite3_reset(st);
sqlite3_clear_bindings(st);
sqlite3_bind_int64(st, 1, freqHz);
sqlite3_bind_text(st, 2, out->country, -1, SQLITE_TRANSIENT);
while (sqlite3_step(st) == SQLITE_ROW) {
if (!out->inBand) {
out->inBand = true;
copy_text(st, 0, out->band, sizeof(out->band));
out->bandLowHz = sqlite3_column_int64(st, 1);
out->bandHighHz = sqlite3_column_int64(st, 2);
out->atBandEdge = (freqHz == out->bandLowHz ||
freqHz == out->bandHighHz);
}
// A NULL class means the country has no licence classes at all; the row
// still carried the band, which is why it is read above first.
if (sqlite3_column_type(st, 3) == SQLITE_NULL) { continue; }
if (out->nPrivs >= HAMMY_FREQ_PRIVS_MAX) { continue; }
hammy_freq_priv_t* p = &out->privs[out->nPrivs++];
copy_text(st, 3, p->code, sizeof(p->code));
copy_text(st, 4, p->name, sizeof(p->name));
p->rank = sqlite3_column_int(st, 5);
p->permitted = (sqlite3_column_type(st, 6) != SQLITE_NULL);
if (p->permitted) {
copy_text(st, 6, p->modes, sizeof(p->modes));
p->segLowHz = sqlite3_column_int64(st, 7);
p->segHighHz = sqlite3_column_int64(st, 8);
p->maxPowerW = sqlite3_column_int(st, 9); // 0 when NULL
copy_text(st, 10, p->notes, sizeof(p->notes));
}
}
sqlite3_reset(st);
// Not in any band: report the nearest one so the user can see how far off
// they are. Usually a typo - 15.000 instead of 14.150.
if (!out->inBand) {
st = db->stFreqNearest;
sqlite3_reset(st);
sqlite3_clear_bindings(st);
sqlite3_bind_int64(st, 1, freqHz);
if (sqlite3_step(st) == SQLITE_ROW) {
copy_text(st, 0, out->nearestBand, sizeof(out->nearestBand));
out->nearestLowHz = sqlite3_column_int64(st, 1);
out->nearestHighHz = sqlite3_column_int64(st, 2);
out->nearestDistanceHz = sqlite3_column_int64(st, 3);
}
sqlite3_reset(st);
return true;
}
// IARU regional allocations. Independent of country, so worth showing even
// when the privilege table is empty.
st = db->stFreqIaru;
sqlite3_reset(st);
sqlite3_clear_bindings(st);
sqlite3_bind_int64(st, 1, freqHz);
while (sqlite3_step(st) == SQLITE_ROW && out->nIaru < HAMMY_FREQ_IARU_MAX) {
hammy_freq_iaru_t* r = &out->iaru[out->nIaru++];
r->region = sqlite3_column_int(st, 0);
r->lowHz = sqlite3_column_int64(st, 1);
r->highHz = sqlite3_column_int64(st, 2);
copy_text(st, 3, r->modes, sizeof(r->modes));
}
sqlite3_reset(st);
// Sitting exactly on a segment boundary is worth flagging: a signal of any
// width centerd there straddles both sides.
st = db->stFreqSegEdge;
sqlite3_reset(st);
sqlite3_clear_bindings(st);
sqlite3_bind_int64(st, 1, freqHz);
sqlite3_bind_text(st, 2, out->country, -1, SQLITE_TRANSIENT);
out->atSegmentEdge = step_bool(st);
return true;
}
size_t hammy_refdb_countries(hammy_refdb_t* db,
char out[][HAMMY_COUNTRY_MAX], size_t cap) {
if (!db || !out || cap == 0 || !db->stCountryList) { return 0; }
size_t n = 0;
sqlite3_reset(db->stCountryList);
while (n < cap && sqlite3_step(db->stCountryList) == SQLITE_ROW) {
copy_text(db->stCountryList, 0, out[n++], HAMMY_COUNTRY_MAX);
}
sqlite3_reset(db->stCountryList);
return n;
}
+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] = (char)toupper((unsigned char)str[i]);
}
}
void hammy_to_lowercase(char* str) {
for (size_t i = 0; str[i] != '\0'; i++) {
str[i] = (char)tolower((unsigned char)str[i]);
}
}
+127
View File
@@ -0,0 +1,127 @@
#include <concord/discord.h>
#include <concord/discord-internal.h>
#include <concord/log.h>
#include <stdlib.h>
#include <hammy/job.h>
#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;
log_info("[worker %d] Started", worker->id);
for (;;) {
pthread_mutex_lock(&pool->lock);
// while instead of if, because pthread_cond_wait() can spuriously wake up like an ass
while (pool->count == 0 && !pool->shutdown) {
pthread_cond_wait(&pool->notEmpty, &pool->lock);
}
if (pool->count == 0 && pool->shutdown) {
pthread_mutex_unlock(&pool->lock);
break;
}
hammy_job_t* job = pool->jobs[pool->head];
pool->head = (pool->head + 1) % pool->cap;
pool->count--;
pool->busy++;
pthread_mutex_unlock(&pool->lock);
// From here, the worker owns the job and is responsible for destroying it.
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);
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);
hammy_job_reply(job, worker->clientCopy, "Command Timeout", "Sorry, your command took too long to process and was dropped. Please try again.", true);
} else {
hammy_job_run(job, worker->clientCopy, worker->refdb);
}
hammy_job_destroy(&job);
pthread_mutex_lock(&pool->lock);
pool->busy--;
pthread_mutex_unlock(&pool->lock);
}
log_info("[worker %d] Exiting", worker->id);
return NULL;
}
bool hammy_worker_start(hammy_worker_t* worker, hammy_pool_t* pool, struct discord* client, int id) {
if (!worker || !pool || !client) { return false; }
worker->pool = pool;
worker->id = id;
worker->clientCopy = NULL;
worker->started = false;
// 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);
return false;
}
// Open a reference to sqlite
worker->refdb = hammy_refdb_open("hammy-ref.sqlite"); // TODO: Probably don't hardcode this?
if (!worker->refdb) {
log_error("[worker %d] Failed to open ref to sqlite. Commands requiring it will be unavailable!", id); // TODO: Consider making this a hard-fail
}
if (pthread_create(&worker->thread, NULL, &hammy_worker_main, worker) != 0) {
log_error("[worker %d] Failed to create thread", id);
hammy_worker_cleanup_clone(worker->clientCopy);
worker->clientCopy = NULL;
return false;
}
worker->started = true;
return true;
}
void hammy_worker_join(hammy_worker_t* worker) {
if (!worker) { return; }
if (worker->started) {
pthread_join(worker->thread, NULL);
worker->started = false;
}
if (worker->clientCopy) {
hammy_worker_cleanup_clone(worker->clientCopy);
worker->clientCopy = NULL;
}
if (worker->refdb) {
hammy_refdb_close(&worker->refdb);
}
}
+92
View File
@@ -0,0 +1,92 @@
#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>
#define HAMMY_CONFIG_PATH "config.json"
static hammy_bot_t* g_bot = NULL;
static void hammy_on_sigint(int sig) {
(void)sig;
// 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);
}
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;
//hammy_bot_deregister_all_commands(bot);
// 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) {
ccord_global_init();
hammy_bot_t* bot = hammy_bot_create();
if (!bot) {
fprintf(stderr, "could not create bot\n");
ccord_global_cleanup();
return EXIT_FAILURE;
}
// 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 ok ? EXIT_SUCCESS : EXIT_FAILURE;
fail:
hammy_bot_destroy(&bot);
ccord_global_cleanup();
return EXIT_FAILURE;
}
+67
View File
@@ -0,0 +1,67 @@
// MIT License
// Copyright (c) 2022 Anotra
// https://github.com/Anotra/anomap
#pragma once
#ifndef ANOMAP_H
#define ANOMAP_H
#include <stddef.h>
#include <stdbool.h>
#define ANOMAP_DECLARE_COMPARE_FUNCTION(function_name, data_type) \
static int \
function_name(const void *a, const void *b) { \
if (*(data_type *)a == *(data_type *)b) return 0; \
return *(data_type *)a > *(data_type *)b ? 1 : -1; \
}
enum anomap_operation {
anomap_insert = 1 << 0,
anomap_update = 1 << 1,
anomap_upsert = anomap_insert | anomap_update,
anomap_delete = 1 << 2,
anomap_getval = 1 << 3,
};
struct anomap;
struct anomap *anomap_create(size_t key_size, size_t val_size,
int (*cmp)(const void *, const void *));
void anomap_destroy(struct anomap *map);
struct anomap_item_changed {
void *data;
enum anomap_operation op;
void *key;
struct {
void *prev;
void *now;
} val;
};
typedef void anomap_on_item_changed(
struct anomap *map, struct anomap_item_changed *item_changed);
void anomap_set_on_item_changed(
struct anomap *map, anomap_on_item_changed *on_changed, void *data);
size_t anomap_length(struct anomap *map);
void anomap_clear(struct anomap *map);
bool anomap_index_of(struct anomap *map, void *key, size_t *index);
bool anomap_at_index(struct anomap *map, size_t index, void *key, void *val);
enum anomap_operation anomap_do(struct anomap *map,
enum anomap_operation operation,
void *key, void *val);
size_t anomap_copy_range(struct anomap *map,
size_t from_index, size_t to_index,
void *keys, void *vals);
size_t anomap_delete_range(struct anomap *map,
size_t from_index, size_t to_index,
void *keys, void *vals);
#endif // !ANOMAP_H
+256
View File
@@ -0,0 +1,256 @@
/**
* @file application_command.h
* @author Cogmasters
* @brief Application Command public functions and datatypes
* @todo application_id should be cached and used when its input value is `0`
*/
#ifndef DISCORD_APPLICATION_COMMAND_H
#define DISCORD_APPLICATION_COMMAND_H
/** @defgroup DiscordAPIInteractionsApplicationCommand Slash commands
* @ingroup DiscordAPIInteractions
* @brief Receiving and registering slash commands
* @{ */
/**
* @brief Fetch all of the global commands for your application
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the parent application
* @CCORD_ret_obj{ret,application_commands}
* @CCORD_return
*/
CCORDcode discord_get_global_application_commands(
struct discord *client,
u64snowflake application_id,
struct discord_ret_application_commands *ret);
/**
* @brief Create a new global command
* @note New global commands will be available in all guilds after 1 hour
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the parent application
* @param params request parameters
* @CCORD_ret_obj{ret,application_command}
* @CCORD_return
*/
CCORDcode discord_create_global_application_command(
struct discord *client,
u64snowflake application_id,
struct discord_create_global_application_command *params,
struct discord_ret_application_command *ret);
/**
* @brief Fetch a global command for your application
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the parent application
* @param command_id the registered command id
* @CCORD_ret_obj{ret,application_command}
* @CCORD_return
*/
CCORDcode discord_get_global_application_command(
struct discord *client,
u64snowflake application_id,
u64snowflake command_id,
struct discord_ret_application_command *ret);
/**
* @brief Edit a global command
* @note Updates will be available in all guilds after 1 hour
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the parent application
* @param command_id the registered command id
* @param params request parameters
* @CCORD_ret_obj{ret,application_command}
* @CCORD_return
*/
CCORDcode discord_edit_global_application_command(
struct discord *client,
u64snowflake application_id,
u64snowflake command_id,
struct discord_edit_global_application_command *params,
struct discord_ret_application_command *ret);
/**
* @brief Deletes a global command
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the parent application
* @param command_id the registered command id
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_global_application_command(
struct discord *client,
u64snowflake application_id,
u64snowflake command_id,
struct discord_ret *ret);
/**
* @brief Overwrite existing global application commands
* @note Updates will be available in all guilds after 1 hour
* @warning Will overwrite all types of application commands: slash
* commands, user commands, and message commands
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the parent application
* @param params the request parameters, a list of application commands
* @CCORD_ret_obj{ret,application_commands}
* @CCORD_return
*/
CCORDcode discord_bulk_overwrite_global_application_commands(
struct discord *client,
u64snowflake application_id,
struct discord_application_commands *params,
struct discord_ret_application_commands *ret);
/**
* @brief Fetch all of the guild commands of a given guild
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the parent application
* @param guild_id the guild where the commands are located
* @CCORD_ret_obj{ret,application_commands}
* @CCORD_return
*/
CCORDcode discord_get_guild_application_commands(
struct discord *client,
u64snowflake application_id,
u64snowflake guild_id,
struct discord_ret_application_commands *ret);
/**
* @brief Create a new guild command
* @note Commands will be available in the guild immediately
* @note Will overwrite any existing guild command with the same name
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the parent application
* @param guild_id the guild where the command is located
* @param params request parameters
* @CCORD_ret_obj{ret,application_command}
* @CCORD_return
*/
CCORDcode discord_create_guild_application_command(
struct discord *client,
u64snowflake application_id,
u64snowflake guild_id,
struct discord_create_guild_application_command *params,
struct discord_ret_application_command *ret);
/**
* @brief Fetch a guild command for your application
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the parent application
* @param guild_id the guild where the command is located
* @param command_id the registered command id
* @CCORD_ret_obj{ret,application_command}
* @CCORD_return
*/
CCORDcode discord_get_guild_application_command(
struct discord *client,
u64snowflake application_id,
u64snowflake guild_id,
u64snowflake command_id,
struct discord_ret_application_command *ret);
/**
* @brief Edit a guild command
* @note Updates for guild commands will be available immediately
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the parent application
* @param guild_id the guild where the command is located
* @param command_id the registered command id
* @param params request parameters
* @CCORD_ret_obj{ret,application_command}
* @CCORD_return
*/
CCORDcode discord_edit_guild_application_command(
struct discord *client,
u64snowflake application_id,
u64snowflake guild_id,
u64snowflake command_id,
struct discord_edit_guild_application_command *params,
struct discord_ret_application_command *ret);
/**
* @brief Deletes a guild command
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the parent application
* @param guild_id the guild where the command is located
* @param command_id the registered command id
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_guild_application_command(struct discord *client,
u64snowflake application_id,
u64snowflake guild_id,
u64snowflake command_id,
struct discord_ret *ret);
/**
* @brief Overwrite existing guild application commands
* @warning This will overwrite all types of application commands: slash
* commands, user commands, and message commands
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the parent application
* @param guild_id the guild where the commands are located
* @param params the request parameters, a list of application commands
* @CCORD_ret_obj{ret,application_commands}
* @CCORD_return
*/
CCORDcode discord_bulk_overwrite_guild_application_commands(
struct discord *client,
u64snowflake application_id,
u64snowflake guild_id,
struct discord_bulk_overwrite_guild_application_commands *params,
struct discord_ret_application_commands *ret);
/**
* @brief Fetches command permissions for all commands in a given guild
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the parent application
* @param guild_id the guild where the commands are located
* @CCORD_ret_obj{ret,guild_application_command_permissions}
* @CCORD_return
*/
CCORDcode discord_get_guild_application_command_permissions(
struct discord *client,
u64snowflake application_id,
u64snowflake guild_id,
struct discord_ret_guild_application_command_permissions *ret);
/**
* @brief Fetches command permissions for a specific command in a given guild
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the parent application
* @param guild_id the guild where the command is located
* @param command_id the registered command id
* @CCORD_ret_obj{ret,application_command_permissions}
* @CCORD_return
*/
CCORDcode discord_get_application_command_permissions(
struct discord *client,
u64snowflake application_id,
u64snowflake guild_id,
u64snowflake command_id,
struct discord_ret_application_command_permission *ret);
/** @example slash-commands.c
* Demonstrates registering and reacting to slash commands */
/** @example slash-commands2.c
* Demonstrates registering and reacting to slash commands from the console */
/** @} DiscordAPIInteractionsApplicationCommand */
#endif /* DISCORD_APPLICATION_COMMAND_H */
+12
View File
@@ -0,0 +1,12 @@
#ifndef ATTRIBUTES_H
#define ATTRIBUTES_H
#if defined(__MINGW32__) \
|| (defined(__GNUC__) && __GNUC__ > 4 ? true : __GNUC_PATCHLEVEL__ >= 4) \
|| defined(__USE_MINGW_ANSI_STDIO)
#define PRINTF_LIKE(a, b) __attribute__((format(gnu_printf, a, b)))
#else
#define PRINTF_LIKE(a, b)
#endif
#endif /* ATTRIBUTES_H */
+37
View File
@@ -0,0 +1,37 @@
/**
* @file audit_log.h
* @author Cogmasters
* @brief Audit Log public functions and datatypes
*/
#ifndef DISCORD_AUDIT_LOG
#define DISCORD_AUDIT_LOG
/** @defgroup DiscordAPIAuditLog Audit Log
* @ingroup DiscordAPI
* @brief Audit Log's public API supported by Concord
* @{ */
/**
* @brief Get audit log for a given guild
*
* @note Requires the 'VIEW_AUDIT_LOG' permission
* @param client the client created with discord_from_token()
* @param guild_id the guild to retrieve the audit log from
* @param params request parameters
* @CCORD_ret_obj{ret,audit_log}
* @CCORD_return
*/
CCORDcode discord_get_guild_audit_log(
struct discord *client,
u64snowflake guild_id,
struct discord_get_guild_audit_log *params,
struct discord_ret_audit_log *ret);
/** @example audit-log.c
* Demonstrates listening to audit-log events and fetching a specific audit-log
*/
/** @} DiscordAPIAuditLog */
#endif /* DISCORD_AUDIT_LOG */
+99
View File
@@ -0,0 +1,99 @@
/**
* @file auto_moderation.h
* @author Cogmasters
* @brief Auto Moderation public functions and datatypes
*/
#ifndef DISCORD_AUTO_MODERATION_H
#define DISCORD_AUTO_MODERATION_H
/** @defgroup DiscordAPIAutoModeration Auto Moderation
* @ingroup DiscordAPI
* @brief Auto Moderation public API supported by Concord
* @{ */
/**
* @brief Get a list of all rules currently configured for the guild
* @note Requires the `MANAGE_GUILD` permission
*
* @param client the client created with discord_from_token()
* @param guild_id the guild to fetch the rules from
* @CCORD_ret_obj{ret,auto_moderation_rules}
* @CCORD_return
*/
CCORDcode discord_list_auto_moderation_rules_for_guild(
struct discord *client,
u64snowflake guild_id,
struct discord_ret_auto_moderation_rules *ret);
/**
* @brief Get a single rule
* @note Requires the `MANAGE_GUILD` permission
*
* @param client the client created with discord_from_token()
* @param guild_id the guild to fetch the rule from
* @param auto_moderation_rule_id the rule to be fetched
* @CCORD_ret_obj{ret,auto_moderation_rule}
* @CCORD_return
*/
CCORDcode discord_get_auto_moderation_rule(
struct discord *client,
u64snowflake guild_id,
u64snowflake auto_moderation_rule_id,
struct discord_ret_auto_moderation_rule *ret);
/**
* @brief Create a new rule
* @note Requires the `MANAGE_GUILD` permission
*
* @param client the client created with discord_from_token()
* @param guild_id the guild to create the rule in
* @param params request parameters
* @CCORD_ret_obj{ret,auto_moderation_rule}
* @CCORD_return
*/
CCORDcode discord_create_auto_moderation_rule(
struct discord *client,
u64snowflake guild_id,
struct discord_create_auto_moderation_rule *params,
struct discord_ret_auto_moderation_rule *ret);
/**
* @brief Modify an existing rule
* @note Requires the `MANAGE_GUILD` permission
*
* @param client the client created with discord_from_token()
* @param guild_id the guild where the rule to be modified is at
* @param auto_moderation_rule_id the rule to be modified
* @param params request parameters
* @CCORD_ret_obj{ret,auto_moderation_rule}
* @CCORD_return
*/
CCORDcode discord_modify_auto_moderation_rule(
struct discord *client,
u64snowflake guild_id,
u64snowflake auto_moderation_rule_id,
struct discord_modify_auto_moderation_rule *params,
struct discord_ret_auto_moderation_rule *ret);
/**
* @brief Delete a rule
* @note Requires the `MANAGE_GUILD` permission
*
* @param client the client created with discord_from_token()
* @param guild_id the guild where the rule to be deleted is at
* @param auto_moderation_rule_id the rule to be deleted
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_auto_moderation_rule(
struct discord *client,
u64snowflake guild_id,
u64snowflake auto_moderation_rule_id,
struct discord_delete_auto_moderation_rule *params,
struct discord_ret *ret);
/** @} DiscordAPIAutoModeration */
#endif /* DISCORD_AUTO_MODERATION_H */
+232
View File
@@ -0,0 +1,232 @@
/* Copyright 2022 Cogmasters */
/*
* C-Ware License
*
* Copyright (c) 2022, C-Ware
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Redistributions of modified source code must append a copyright notice in
* the form of 'Copyright <YEAR> <NAME>' to each modified source file's
* copyright notice, and the standalone license file if one exists.
*
* A 'redistribution' can be constituted as any version of the original source
* code material that is intended to comprise some other derivative work of
* this code. A fork created for the purpose of contributing to any version of
* the source does not constitute a truly 'derivative work' and does not require
* listing.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/* Modified by Lucas Müller <[email protected]>, 19 Sept 2022
* - __carray_init() should initialize its `size` value
*
* Modified by Lucas Müller <lucas@muller.codes>, 15 Feb 2022
* - CARRAY_RESIZE() has a fallback value (+1)
*
* Modified by Lucas Müller <lucas@muller.codes>, 06 Feb 2022
* - __carray_init() accept initial length
*
* Modified by Lucas Müller <lucas@muller.codes>, 02 Feb 2022
* - remove free(carray) at __carrray_free()
*
* Modified by Lucas Müller <lucas@muller.codes>, 01 Feb 2022
* - change CARRAY_INITIAL_SIZE from 5 to 4
* - change CARRAY_RESIZE to doubling arrays to reduce realloc calls
* - remove calloc() from __carray_init(), expect user to allocate it
* - remove pseudo-return from __carray_init()
*
* Modified by Lucas Müller <lucas@muller.codes>, 27 Jan 2022
* - rename contents -> array
* - rename logical_size -> size
* - rename physical_size -> realsize
*/
#ifndef CWARE_ARRAY_H
#define CWARE_ARRAY_H
#ifndef CARRAY_INITIAL_SIZE
#define CARRAY_INITIAL_SIZE 4
#endif
#ifndef CARRAY_RESIZE
#define CARRAY_RESIZE(current_size) \
1 + current_size * 2
#endif
/* carray_init */
#ifndef CARRAY_STACKFUL
#define __carray_init(carray, length, _type, _compare, _free) \
do { \
(carray)->realsize = length; \
(carray)->size = 0; \
(carray)->array = calloc(length, sizeof(_type)); \
} while (0)
#define carray_init(carray, settings) \
__carray_init(carray, CARRAY_INITIAL_SIZE, settings)
#else
#define carray_init(carray, length, block) \
do { \
carray.realsize = length; \
carray.size = 0; \
carray.array = block; \
} while (0)
#endif
/* carray_insert */
#ifndef CARRAY_STACKFUL
#define __carray_insert_handle_full(carray, index, value) \
(carray)->realsize = CARRAY_RESIZE((carray)->realsize); \
(carray)->array = realloc((carray)->array, sizeof(*(carray)->array) * (size_t) (carray)->realsize)
#else
#define __carray_insert_handle_full(carray, index, value) \
fprintf(stderr, "carray_insert: attempt to insert value '%s' into full array '%s'. (%s:%i)\n", #value, #carray, __FILE__, __LINE__); \
exit(EXIT_FAILURE)
#endif
#define carray_insert(carray, index, value) \
if((carray)->size == (carray)->realsize) { \
__carray_insert_handle_full(carray, index, value); \
} \
\
if(index < 0 || index > (carray)->size) { \
fprintf(stderr, "carray_insert: attempt to insert at index %i, out of bounds of array '%s'. (%s:%i)\n", index, #carray, __FILE__, __LINE__); \
exit(EXIT_FAILURE); \
} \
\
memmove((carray)->array + index + 1, (carray)->array + index, sizeof(*(carray)->array) * (size_t) ((carray)->size - index)); \
(carray)->array[index] = value; \
(carray)->size++
/* carray_pop */
#define carray_pop(carray, index, location) \
location; \
\
if(index < 0 || index >= (carray)->size) { \
fprintf(stderr, "carray_pop: attempt to pop index %i, out of bounds of array '%s'. (%s:%i)\n", index, #carray, __FILE__, __LINE__); \
exit(EXIT_FAILURE); \
} \
\
(carray)->size--; \
(location) = (carray)->array[(index)]; \
memmove((carray)->array + index, (carray)->array + index + 1, sizeof(*(carray)->array) * (size_t) ((carray)->size - index))
/* carray_remove */
#define __carray_remove(carray, value, _type, _compare, _free) \
do { \
int __CARRAY_ITER_INDEX = 0; \
\
for(__CARRAY_ITER_INDEX = 0; __CARRAY_ITER_INDEX < (carray)->size; __CARRAY_ITER_INDEX++) { \
_type __CARRAY_OPERAND_A = (carray)->array[__CARRAY_ITER_INDEX]; \
_type __CARRAY_OPERAND_B = value; \
\
if((_compare) == 0) \
continue; \
\
_free; \
(carray)->size--; \
memmove((carray)->array + __CARRAY_ITER_INDEX, \
(carray)->array + __CARRAY_ITER_INDEX + 1, \
sizeof(*(carray)->array) * (size_t) ((carray)->size - __CARRAY_ITER_INDEX)); \
\
__CARRAY_ITER_INDEX = -1; \
break; \
} \
\
if(__CARRAY_ITER_INDEX != -1) { \
fprintf(stderr, "carray_remove: attempt to remove value '%s' that is not in array '%s'. (%s:%i)\n", #value, #carray, __FILE__, __LINE__); \
exit(EXIT_FAILURE); \
} \
} while(0)
#define carray_remove(carray, value, settings) \
__carray_remove(carray, value, settings)
/* carray_find */
#define __carray_find(carray, value, location, _type, _compare, _free) \
-1; \
\
do { \
int __CARRAY_ITER_INDEX = 0; \
location = -1; \
\
for(__CARRAY_ITER_INDEX = 0; __CARRAY_ITER_INDEX < (carray)->size; __CARRAY_ITER_INDEX++) { \
_type __CARRAY_OPERAND_A = (carray)->array[__CARRAY_ITER_INDEX]; \
_type __CARRAY_OPERAND_B = value; \
\
if((_compare) == 0) \
continue; \
\
location = __CARRAY_ITER_INDEX; \
\
break; \
} \
} while(0)
#define carray_find(carray, value, location, settings) \
__carray_find(carray, value, location, settings)
#ifndef CARRAY_STACKFUL
#define __carray_free_array(carray) free((carray)->array);
#else
#define __carray_free_array(carray)
#endif
/* carray_free */
#define __carray_free(carray, _type, _compare, _free) \
do { \
int __CARRAY_ITER_INDEX = 0; \
\
for(__CARRAY_ITER_INDEX = 0; __CARRAY_ITER_INDEX < (carray)->size; __CARRAY_ITER_INDEX++) { \
_type __CARRAY_OPERAND_A = (carray)->array[__CARRAY_ITER_INDEX]; \
(void) __CARRAY_OPERAND_A; \
\
_free; \
} \
\
__carray_free_array(carray); \
} while(0)
#define carray_free(carray, settings) \
__carray_free(carray, settings)
/* carray_append */
#ifndef CARRAY_STACKFUL
#define __carray_append_handle_full(carray, value) \
(carray)->realsize = CARRAY_RESIZE((carray)->realsize); \
(carray)->array = realloc((carray)->array, sizeof(*(carray)->array) * (size_t) (carray)->realsize)
#else
#define __carray_append_handle_full(carray, value) \
fprintf(stderr, "carray_append: attempt to append value '%s' into full array '%s'. (%s:%i)\n", #value, #carray, __FILE__, __LINE__); \
exit(EXIT_FAILURE)
#endif
#define carray_append(carray, value) \
if((carray)->size == (carray)->realsize) { \
__carray_append_handle_full(carray, value); \
} \
\
(carray)->array[(carray)->size] = value; \
(carray)->size++;
#endif
+670
View File
@@ -0,0 +1,670 @@
/**
* @file channel.h
* @author Cogmasters
* @brief Channel public functions and datatypes
*/
#ifndef DISCORD_CHANNEL_H
#define DISCORD_CHANNEL_H
/* forward declaration */
struct discord_ret_users;
/**/
/** @defgroup DiscordAPIChannel Channel
* @ingroup DiscordAPI
* @brief Channel's public API supported by Concord
* @{ */
/**
* @brief Get channel from given id
* @note If the channel is a thread, a thread member object is included in the
* returned result
*
* @param client the client created with discord_from_token()
* @param channel_id the channel to be retrieved
* @CCORD_ret_obj{ret,channel}
* @CCORD_return
*/
CCORDcode discord_get_channel(struct discord *client,
u64snowflake channel_id,
struct discord_ret_channel *ret);
/**
* @brief Update a channel's settings
*
* @param client the client created with discord_from_token()
* @param channel_id the channel to be modified
* @param params request parameters
* @CCORD_ret_obj{ret,channel}
* @CCORD_return
*/
CCORDcode discord_modify_channel(struct discord *client,
u64snowflake channel_id,
struct discord_modify_channel *params,
struct discord_ret_channel *ret);
/**
* @brief Delete a channel, or close a private message
* @note Requires the MANAGE_CHANNELS permission for the guild, or
* MANAGE_THREADS if the channel is a thread
* @note Deleting a category does not delete its child channels; they will have
* their parent_id removed and a `Channel Update Gateway` event will
* fire for each of them
* @note Fires a `Channel Delete` event (or `Thread Delete` if the channel
* was a thread)
*
* @param client the client created with discord_from_token()
* @param channel_id the channel to be deleted
* @param params request parameters
* @CCORD_ret_obj{ret,channel}
* @CCORD_return
*/
CCORDcode discord_delete_channel(struct discord *client,
u64snowflake channel_id,
struct discord_delete_channel *params,
struct discord_ret_channel *ret);
/**
* @brief Get messages for a given channel
* @note If operating on a guild channel, this endpoint requires the
* VIEW_CHANNEL permission to be present on the current user
* @note If the current user is missing the READ_MESSAGE_HISTORY permission
* in the channel then this will return no messages (since they cannot
* read the message history)
* @note The before, after, and around keys are mutually exclusive, only one
* may be passed at a time
*
* @param client the client created with discord_from_token()
* @param channel_id the channel to get messages from
* @param params request parameters
* @CCORD_ret_obj{ret,messages}
* @CCORD_return
*/
CCORDcode discord_get_channel_messages(
struct discord *client,
u64snowflake channel_id,
struct discord_get_channel_messages *params,
struct discord_ret_messages *ret);
/**
* @brief Get a specific message in the channel
* @note If operating on a guild channel, this endpoint requires the
* 'READ_MESSAGE_HISTORY' permission to be present on the current user
* @param client the client created with discord_from_token()
* @param channel_id the channel where the message resides
* @param message_id the message itself
* @CCORD_ret_obj{ret,message}
* @CCORD_return
*/
CCORDcode discord_get_channel_message(struct discord *client,
u64snowflake channel_id,
u64snowflake message_id,
struct discord_ret_message *ret);
/**
* @brief Post a message to a guild text or DM channel
* @note Fires a `Message Create` event
*
* @param client the client created with discord_from_token()
* @param channel_id the channel to send the message at
* @param params request parameters
* @CCORD_ret_obj{ret,message}
* @CCORD_return
*/
CCORDcode discord_create_message(struct discord *client,
u64snowflake channel_id,
struct discord_create_message *params,
struct discord_ret_message *ret);
/**
* @brief Crosspost a message in a News Channel to following channels
* @note This endpoint requires the 'SEND_MESSAGES' permission, if the current
* user sent the message, or additionally the 'MANAGE_MESSAGES'
* permission, for all other messages, to be present for the current
* user
*
* @param client the client created with discord_from_token()
* @param channel_id the news channel that will crosspost
* @param message_id the message that will crospost
* @CCORD_ret_obj{ret,message}
* @CCORD_return
*/
CCORDcode discord_crosspost_message(struct discord *client,
u64snowflake channel_id,
u64snowflake message_id,
struct discord_ret_message *ret);
/**
* @brief Create a reaction for the message
*
* @param client the client created with discord_from_token()
* @param channel_id the channel that the message belongs to
* @param message_id the message to receive a reaction
* @param emoji_id the emoji id (leave as 0 if not a custom emoji)
* @param emoji_name the emoji name
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_create_reaction(struct discord *client,
u64snowflake channel_id,
u64snowflake message_id,
u64snowflake emoji_id,
const char emoji_name[],
struct discord_ret *ret);
/**
* @brief Delete a reaction the current user has made for the message
*
* @param client the client created with discord_from_token()
* @param channel_id the channel that the message belongs to
* @param message_id the message to have a reaction deleted
* @param emoji_id the emoji id (leave as 0 if not a custom emoji)
* @param emoji_name the emoji name
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_own_reaction(struct discord *client,
u64snowflake channel_id,
u64snowflake message_id,
u64snowflake emoji_id,
const char emoji_name[],
struct discord_ret *ret);
/**
* @brief Deletes another user's reaction
*
* @param client the client created with discord_from_token()
* @param channel_id the channel that the message belongs to
* @param message_id the message to have a reaction deleted
* @param user_id the user the reaction belongs to
* @param emoji_id the emoji id (leave as 0 if not a custom emoji)
* @param emoji_name the emoji name
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_user_reaction(struct discord *client,
u64snowflake channel_id,
u64snowflake message_id,
u64snowflake user_id,
u64snowflake emoji_id,
const char emoji_name[],
struct discord_ret *ret);
/**
* @brief Get a list of users that reacted with given emoji
*
* @param client the client created with discord_from_token()
* @param channel_id the channel that the message belongs to
* @param message_id the message reacted to
* @param emoji_id the emoji id (leave as 0 if not a custom emoji)
* @param emoji_name the emoji name
* @param params request parameters
* @CCORD_ret_obj{ret,users}
* @CCORD_return
*/
CCORDcode discord_get_reactions(struct discord *client,
u64snowflake channel_id,
u64snowflake message_id,
u64snowflake emoji_id,
const char emoji_name[],
struct discord_get_reactions *params,
struct discord_ret_users *ret);
/**
* @brief Deletes all reactions from message
*
* @param client the client created with discord_from_token()
* @param channel_id the channel that the message belongs to
* @param message_id the message that will be purged of reactions
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_all_reactions(struct discord *client,
u64snowflake channel_id,
u64snowflake message_id,
struct discord_ret *ret);
/**
* @brief Deletes all the reactions for a given emoji on message
*
* @param client the client created with discord_from_token()
* @param channel_id the channel that the message belongs to
* @param message_id the message that will be purged of reactions from
* particular emoji
* @param emoji_id the emoji id (leave as 0 if not a custom emoji)
* @param emoji_name the emoji name
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_all_reactions_for_emoji(struct discord *client,
u64snowflake channel_id,
u64snowflake message_id,
u64snowflake emoji_id,
const char emoji_name[],
struct discord_ret *ret);
/**
* @brief Edit a previously sent message
*
* @param client the client created with discord_from_token()
* @param channel_id the channel that the message belongs to
* @param message_id the message that will be purged of reactions from
* particular emoji
* @param params request parameters
* @CCORD_ret_obj{ret,message}
* @CCORD_return
*/
CCORDcode discord_edit_message(struct discord *client,
u64snowflake channel_id,
u64snowflake message_id,
struct discord_edit_message *params,
struct discord_ret_message *ret);
/**
* @brief Delete a message
*
* @param client the client created with discord_from_token()
* @param channel_id the channel that the message belongs to
* @param message_id the message that will be purged of reactions from
* particular emoji
* @param params request parameters
* @CCORD_return
*/
CCORDcode discord_delete_message(struct discord *client,
u64snowflake channel_id,
u64snowflake message_id,
struct discord_delete_message *params,
struct discord_ret *ret);
/**
* @brief Delete multiple messages in a single request
*
* @param client the client created with discord_from_token()
* @param channel_id the channel that the message belongs to
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_bulk_delete_messages(
struct discord *client,
u64snowflake channel_id,
struct discord_bulk_delete_messages *params,
struct discord_ret *ret);
/**
* @brief Edit the channel permission overwrites for a user or role in a
* channel
*
* @param client the client created with discord_from_token()
* @param channel_id the channel that the message belongs to
* @param overwrite_id
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_edit_channel_permissions(
struct discord *client,
u64snowflake channel_id,
u64snowflake overwrite_id,
struct discord_edit_channel_permissions *params,
struct discord_ret *ret);
/**
* @brief Get invites (with invite metadata) for the channel
*
* @param client the client created with discord_from_token()
* @param channel_id the channel that the message belongs to
* @CCORD_ret_obj{ret,invites}
* @CCORD_return
*/
CCORDcode discord_get_channel_invites(struct discord *client,
u64snowflake channel_id,
struct discord_ret_invites *ret);
/**
* @brief Create a new invite for the channel
*
* @param client the client created with discord_from_token()
* @param channel_id the channel that the message belongs to
* @param params request parameters
* @CCORD_ret_obj{ret,invite}
* @CCORD_return
*/
CCORDcode discord_create_channel_invite(
struct discord *client,
u64snowflake channel_id,
struct discord_create_channel_invite *params,
struct discord_ret_invite *ret);
/**
* @brief Delete a channel permission overwrite for a user or role in a
* channel
*
* @param client the client created with discord_from_token()
* @param channel_id the channel to the permission deleted
* @param overwrite_id the id of the overwritten permission
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_channel_permission(
struct discord *client,
u64snowflake channel_id,
u64snowflake overwrite_id,
struct discord_delete_channel_permission *params,
struct discord_ret *ret);
/**
* @brief Post a typing indicator for the specified channel
*
* @param client the client created with discord_from_token()
* @param channel_id the channel to post the typing indicator to
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_trigger_typing_indicator(struct discord *client,
u64snowflake channel_id,
struct discord_ret *ret);
/**
* @brief Follow a News Channel to send messages to a target channel
* @note Requires MANAGE_WEBHOOKS permission in the target channel
* MANAGE_WEBHOOKS permission in the target channel
*
* @param client the client created with discord_from_token()
* @param channel_id the channel to be followed
* @CCORD_ret_obj{ret,followed_channel}
* @CCORD_return
*/
CCORDcode discord_follow_news_channel(
struct discord *client,
u64snowflake channel_id,
struct discord_follow_news_channel *params,
struct discord_ret_followed_channel *ret);
/**
* @brief Get all pinned messages in the channel
*
* @param client the client created with discord_from_token()
* @param channel_id the channel where the get pinned messages from
* @CCORD_ret_obj{ret,messages}
* @CCORD_return
*/
CCORDcode discord_get_pinned_messages(struct discord *client,
u64snowflake channel_id,
struct discord_ret_messages *ret);
/**
* @brief Pin a message to a channel
*
* @param client the client created with discord_from_token()
* @param channel_id channel to pin the message on
* @param message_id message to be pinned
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_pin_message(struct discord *client,
u64snowflake channel_id,
u64snowflake message_id,
struct discord_pin_message *params,
struct discord_ret *ret);
/**
* @brief Unpin a message from a channel
*
* @param client the client created with discord_from_token()
* @param channel_id channel for the message to be unpinned
* @param message_id message to be unpinned
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_unpin_message(struct discord *client,
u64snowflake channel_id,
u64snowflake message_id,
struct discord_unpin_message *params,
struct discord_ret *ret);
/**
* @brief Adds a recipient to a Group DM using their access token
*
* @param client the client created with discord_from_token()
* @param channel_id group to add the user in
* @param user_id user to be added
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_group_dm_add_recipient(
struct discord *client,
u64snowflake channel_id,
u64snowflake user_id,
struct discord_group_dm_add_recipient *params,
struct discord_ret *ret);
/**
* @brief Removes a recipient from a Group DM
*
* @param client the client created with discord_from_token()
* @param channel_id channel for the user to be removed from
* @param user_id user to be removed
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_group_dm_remove_recipient(struct discord *client,
u64snowflake channel_id,
u64snowflake user_id,
struct discord_ret *ret);
/**
* @brief Creates a new thread from an existing message
* @note Fires a `Thread Create` event
*
* @param client the client created with discord_from_token()
* @param channel_id channel to start a thread on
* @param message_id message to start a thread from
* @param params request parameters
* @CCORD_ret_obj{ret,channel}
* @CCORD_return
*/
CCORDcode discord_start_thread_with_message(
struct discord *client,
u64snowflake channel_id,
u64snowflake message_id,
struct discord_start_thread_with_message *params,
struct discord_ret_channel *ret);
/**
* @brief Creates a new thread that is not connected to an existing message
* @note Fires a `Thread Create` event
*
* @param client the client created with discord_from_token()
* @param channel_id channel to start a thread on
* @param params request parameters
* @CCORD_ret_obj{ret,channel}
* @CCORD_return
*/
CCORDcode discord_start_thread_without_message(
struct discord *client,
u64snowflake channel_id,
struct discord_start_thread_without_message *params,
struct discord_ret_channel *ret);
/**
* @brief Adds the current user to an un-archived thread
* @note Fires a `Thread Members Update` event
*
* @param client the client created with discord_from_token()
* @param channel_id the thread to be joined
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_join_thread(struct discord *client,
u64snowflake channel_id,
struct discord_ret *ret);
/**
* @brief Adds another member to an un-archived thread
* @note Fires a `Thread Members Update` event
*
* @param client the client created with discord_from_token()
* @param channel_id the thread to be joined
* @param user_id user to be added to thread
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_add_thread_member(struct discord *client,
u64snowflake channel_id,
u64snowflake user_id,
struct discord_ret *ret);
/**
* @brief Removes the current user from a un-archived thread
* @note Fires a `Thread Members Update` event
*
* @param client the client created with discord_from_token()
* @param channel_id the thread to be removed from
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_leave_thread(struct discord *client,
u64snowflake channel_id,
struct discord_ret *ret);
/**
* @brief Removes another member from a un-archived thread
* @note Fires a `Thread Members Update` event
* @note Requires `MANAGE_THREADS` permission
*
* @param client the client created with discord_from_token()
* @param channel_id the thread to be removed from
* @param user_id user to be removed
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_remove_thread_member(struct discord *client,
u64snowflake channel_id,
u64snowflake user_id,
struct discord_ret *ret);
/**
* @brief Get members from a given thread channel
* @note Fires a `Thread Members Update` event
* @note Requires `MANAGE_THREADS` permission
*
* @param client the client created with discord_from_token()
* @param channel_id the thread to be joined
* @CCORD_ret_obj{ret,thread_members}
* @CCORD_return
*/
CCORDcode discord_list_thread_members(struct discord *client,
u64snowflake channel_id,
struct discord_ret_thread_members *ret);
/**
* @brief Get public archived threads in a given channel
*
* @param client the client created with discord_from_token()
* @param channel_id the channel to be searched for threads
* @param before return threads before this timestamp
* @param limit maximum number of threads to return
* @CCORD_ret_obj{ret,thread_response_body}
* @CCORD_return
*/
CCORDcode discord_list_public_archived_threads(
struct discord *client,
u64snowflake channel_id,
u64unix_ms before,
int limit,
struct discord_ret_thread_response_body *ret);
/**
* @brief Get private archived threads in a given channel
*
* @param client the client created with discord_from_token()
* @param channel_id the channel to be searched for threads
* @param before return threads before this timestamp
* @param limit maximum number of threads to return
* @CCORD_ret_obj{ret,thread_response_body}
* @CCORD_return
*/
CCORDcode discord_list_private_archived_threads(
struct discord *client,
u64snowflake channel_id,
u64unix_ms before,
int limit,
struct discord_ret_thread_response_body *ret);
/**
* @brief Get private archived threads that current user has joined
*
* @param client the client created with discord_from_token()
* @param channel_id the channel to be searched for threads
* @param before return threads before this timestamp
* @param limit maximum number of threads to return
* @CCORD_ret_obj{ret,thread_response_body}
* @CCORD_return
*/
CCORDcode discord_list_joined_private_archived_threads(
struct discord *client,
u64snowflake channel_id,
u64unix_ms before,
int limit,
struct discord_ret_thread_response_body *ret);
/** @defgroup DiscordAPIChannelHelper Helper functions
* @brief Custom helper functions
* @{ */
/**
* @brief Get a guild's channel from its given numerical position
*
* @param client the client created with discord_from_token()
* @param guild_id guild the channel belongs to
* @param type the channel type where to take position reference from
* @CCORD_ret_obj{ret,channel}
* @CCORD_return
*/
CCORDcode discord_get_channel_at_pos(struct discord *client,
u64snowflake guild_id,
enum discord_channel_types type,
int position,
struct discord_ret_channel *ret);
/**
* @brief Append to an overwrite list
* @note the list should be freed with `discord_overwrite_list_free()` after
* its no longer being used
*
* @param permission_overwrites list to be appended to
* @param id role or user id
* @param type either 0 (role) or 1 (member)
* @param allow permission bit set
* @param deny permission bit set
*/
void discord_overwrite_append(struct discord_overwrites *permission_overwrites,
u64snowflake id,
int type,
u64bitmask allow,
u64bitmask deny);
/** @} DiscordAPIChannelHelper */
/** @example channel.c
* Demonstrates a couple use cases of the Channel API */
/** @example embed.c
* Demonstrates embed manipulation */
/** @example fetch-messages.c
* Demonstrates fetching user messages */
/** @example manual-dm.c
* Demonstrates sending DMs with your client */
/** @example pin.c
* Demonstrates pinning messages */
/** @example reaction.c
* Demonstrates a couple use cases of the Channel reactions API */
/** @} DiscordAPIChannel */
#endif /* DISCORD_CHANNEL_H */
+510
View File
@@ -0,0 +1,510 @@
/* Copyright 2022 Cogmasters */
/*
* C-Ware License
*
* Copyright (c) 2022, C-Ware
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Redistributions of modified source code must append a copyright notice in
* the form of 'Copyright <YEAR> <NAME>' to each modified source file's
* copyright notice, and the standalone license file if one exists.
*
* A 'redistribution' can be constituted as any version of the original source
* code material that is intended to comprise some other derivative work of
* this code. A fork created for the purpose of contributing to any version of
* the source does not constitute a truly 'derivative work' and does not require
* listing.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/* Modified by Lucas Müller ([email protected]), 16 May 2022
* - add __chash_init() and __chash_free() as a non-malloc option */
#ifndef CWARE_LIBCHASH_H
#define CWARE_LIBCHASH_H
#define CWARE_LIBCHASH_VERSION "x.0.0"
/* How big heap-allocated hashtables are by default */
#ifndef CHASH_INITIAL_SIZE
#define CHASH_INITIAL_SIZE 10
#elif CHASH_INITIAL_SIZE <= 0
"chash_init: default length must be greater than 0"
#endif
/* Calculates the next size of the hashtable. */
#ifndef CHASH_RESIZE
#define CHASH_RESIZE(size) \
((size) * 1.3)
#endif
/* The threshold that, when passed, will cause a resize */
#ifndef CHASH_LOAD_THRESHOLD
#define CHASH_LOAD_THRESHOLD 0.8
#endif
/* The type that is used for counters; useful for aligning hashtable
* length and capacity fields so type casting warnings do not appear */
#ifndef CHASH_COUNTER_TYPE
#define CHASH_COUNTER_TYPE int
#endif
/* The name of the key field */
#ifndef CHASH_KEY_FIELD
#define CHASH_KEY_FIELD key
#endif
/* The name of the value field */
#ifndef CHASH_VALUE_FIELD
#define CHASH_VALUE_FIELD value
#endif
/* The name of the state field */
#ifndef CHASH_STATE_FIELD
#define CHASH_STATE_FIELD state
#endif
/* The name of the buckets field */
#ifndef CHASH_BUCKETS_FIELD
#define CHASH_BUCKETS_FIELD buckets
#endif
/* The name of the length field */
#ifndef CHASH_LENGTH_FIELD
#define CHASH_LENGTH_FIELD length
#endif
/* The name of the capacity field */
#ifndef CHASH_CAPACITY_FIELD
#define CHASH_CAPACITY_FIELD capacity
#endif
/* State enums */
#define CHASH_UNFILLED 0
#define CHASH_FILLED 1
#define CHASH_TOMBSTONE 2
/* Built-ins */
#define chash_string_hash(key, hash) \
5031; \
do { \
int __CHASH_HINDEX = 0; \
\
for(__CHASH_HINDEX = 0; (key)[__CHASH_HINDEX] != '\0'; \
__CHASH_HINDEX++) { \
(hash) = (((hash) << 1) + (hash)) + (key)[__CHASH_HINDEX]; \
} \
} while(0)
#define chash_string_compare(cmp_a, cmp_b) \
(strcmp((cmp_a), (cmp_b)) == 0)
#define chash_default_init(bucket, _key, _value) \
(bucket).CHASH_KEY_FIELD = (_key); \
(bucket).CHASH_VALUE_FIELD = _value
/* utility macros */
#define __chash_abs(x) \
((x) < 0 ? (x) * - 1 : (x))
#define __chash_hash(mod, _key, namespace) \
__CHASH_HASH = namespace ## _HASH((_key), __CHASH_HASH); \
__CHASH_HASH = __CHASH_HASH % (mod); \
__CHASH_HASH = __chash_abs(__CHASH_HASH);
#define __chash_probe(hashtable, _key, namespace) \
while(__CHASH_INDEX < (hashtable)->CHASH_CAPACITY_FIELD) { \
if((hashtable)->CHASH_BUCKETS_FIELD[__CHASH_HASH].CHASH_STATE_FIELD == \
CHASH_UNFILLED) \
break; \
\
if((namespace ## _COMPARE((_key), \
(hashtable)->CHASH_BUCKETS_FIELD[__CHASH_HASH].CHASH_KEY_FIELD)) == 1) { \
\
__CHASH_INDEX = -1; \
break; \
} \
\
__CHASH_HASH = (__CHASH_HASH + 1) % (hashtable)->CHASH_CAPACITY_FIELD; \
__CHASH_INDEX++; \
} \
#define __chash_probe_to_unfilled(mod, _key, buffer, namespace) \
while(1) { \
if(buffer[__CHASH_HASH].CHASH_STATE_FIELD != CHASH_FILLED) \
break; \
\
if((namespace ## _COMPARE((_key), buffer[__CHASH_HASH].CHASH_KEY_FIELD)) \
== 1) \
break; \
\
__CHASH_HASH = (__CHASH_HASH + 1) % mod; \
} \
#define __chash_resize(hashtable, namespace) \
do { \
CHASH_COUNTER_TYPE __CHASH_INDEX = 0; \
namespace ## _BUCKET *__CHASH_BUCKETS = NULL; \
CHASH_COUNTER_TYPE __CHASH_NEXT_SIZE = (CHASH_COUNTER_TYPE) \
CHASH_RESIZE((hashtable)->CHASH_CAPACITY_FIELD); \
\
if((namespace ## _HEAP) == 0) { \
if((hashtable)->CHASH_LENGTH_FIELD != \
(hashtable)->CHASH_CAPACITY_FIELD) { \
break; \
} \
\
fprintf(stderr, "__chash_resize: hashtable is full. could not resize" \
" (%s:%i)\n", __FILE__, __LINE__); \
abort(); \
} \
\
if((double) (hashtable)->CHASH_LENGTH_FIELD / \
(double) (hashtable)->CHASH_CAPACITY_FIELD < CHASH_LOAD_THRESHOLD) \
break; \
\
__CHASH_BUCKETS = malloc((size_t) (__CHASH_NEXT_SIZE \
* ((CHASH_COUNTER_TYPE) \
sizeof(namespace ## _BUCKET)))); \
memset(__CHASH_BUCKETS, 0, ((size_t) (__CHASH_NEXT_SIZE \
* ((CHASH_COUNTER_TYPE) \
sizeof(namespace ## _BUCKET))))); \
\
for(__CHASH_INDEX = 0; __CHASH_INDEX < (hashtable)->CHASH_CAPACITY_FIELD; \
__CHASH_INDEX++) { \
namespace ## _BUCKET __CHASH_NEW_KEY_BUCKET; \
memset(&__CHASH_NEW_KEY_BUCKET, 0, sizeof(namespace ## _BUCKET)); \
namespace ## _INIT(__CHASH_NEW_KEY_BUCKET, \
(hashtable)->CHASH_BUCKETS_FIELD[__CHASH_INDEX].CHASH_KEY_FIELD, \
(hashtable)->CHASH_BUCKETS_FIELD[__CHASH_INDEX].CHASH_VALUE_FIELD); \
\
if((hashtable)->CHASH_BUCKETS_FIELD[__CHASH_INDEX].CHASH_STATE_FIELD \
!= CHASH_FILLED) \
continue; \
\
__chash_hash(__CHASH_NEXT_SIZE, __CHASH_NEW_KEY_BUCKET.CHASH_KEY_FIELD, \
namespace); \
__chash_probe_to_unfilled(__CHASH_NEXT_SIZE, \
(hashtable)->CHASH_BUCKETS_FIELD[__CHASH_INDEX].CHASH_KEY_FIELD, \
__CHASH_BUCKETS, namespace) \
\
__CHASH_BUCKETS[__CHASH_HASH] = __CHASH_NEW_KEY_BUCKET; \
__CHASH_BUCKETS[__CHASH_HASH].CHASH_STATE_FIELD = CHASH_FILLED; \
__CHASH_HASH = 0; \
} \
\
free((hashtable)->CHASH_BUCKETS_FIELD); \
(hashtable)->CHASH_BUCKETS_FIELD = __CHASH_BUCKETS; \
(hashtable)->CHASH_CAPACITY_FIELD = __CHASH_NEXT_SIZE; \
__CHASH_HASH = 0; \
} while(0)
#define __chash_assert_nonnull(func, ptr) \
do { \
if((ptr) == NULL) { \
fprintf(stderr, #func ": " #ptr " cannot be null (%s:%i)\n", \
__FILE__, __LINE__); \
abort(); \
} \
} while(0)
/* operations */
#define __chash_init(hashtable, namespace) \
(hashtable)->CHASH_LENGTH_FIELD = 0; \
(hashtable)->CHASH_CAPACITY_FIELD = CHASH_INITIAL_SIZE; \
(hashtable)->CHASH_BUCKETS_FIELD = malloc(CHASH_INITIAL_SIZE \
* sizeof(*((hashtable)->CHASH_BUCKETS_FIELD))); \
memset((hashtable)->CHASH_BUCKETS_FIELD, 0, \
sizeof(*((hashtable)->CHASH_BUCKETS_FIELD)) * CHASH_INITIAL_SIZE)
#define chash_init(hashtable, namespace) \
NULL; \
\
(hashtable) = malloc(sizeof((*(hashtable)))); \
__chash_init(hashtable, namespace)
#define chash_init_stack(hashtable, buffer, _length, namespace) \
(*(hashtable)); \
\
if((_length) <= 0) { \
fprintf(stderr, "chash_init_stack: hashtable cannot have a maximum " \
"length of 0 or less (%s:%i)\n", __FILE__, __LINE__); \
abort(); \
} \
\
__chash_assert_nonnull(chash_init_stack, buffer); \
\
(hashtable)->CHASH_LENGTH_FIELD = 0; \
(hashtable)->CHASH_CAPACITY_FIELD = _length; \
(hashtable)->CHASH_BUCKETS_FIELD = buffer
#define chash_assign(hashtable, _key, _value, namespace) \
do { \
long __CHASH_HASH = 0; \
namespace ## _BUCKET __CHASH_KEY_BUCKET; \
memset(&__CHASH_KEY_BUCKET, 0, sizeof(namespace ## _BUCKET)); \
namespace ## _INIT(__CHASH_KEY_BUCKET, _key, _value); \
\
__chash_assert_nonnull(chash_assign, hashtable); \
__chash_assert_nonnull(chash_assign, (hashtable)->CHASH_BUCKETS_FIELD); \
__chash_resize(hashtable, namespace); \
__chash_hash((hashtable)->CHASH_CAPACITY_FIELD, _key, namespace); \
__chash_probe_to_unfilled((hashtable)->CHASH_CAPACITY_FIELD, \
(_key), (hashtable)->CHASH_BUCKETS_FIELD, namespace) \
\
if((hashtable)->CHASH_BUCKETS_FIELD[__CHASH_HASH].CHASH_STATE_FIELD == \
CHASH_FILLED) { \
namespace ## _FREE_VALUE( \
(hashtable)->CHASH_BUCKETS_FIELD[__CHASH_HASH].CHASH_VALUE_FIELD); \
} else { \
(hashtable)->CHASH_LENGTH_FIELD++; \
} \
\
(hashtable)->CHASH_BUCKETS_FIELD[__CHASH_HASH] = __CHASH_KEY_BUCKET; \
(hashtable)->CHASH_BUCKETS_FIELD[__CHASH_HASH].CHASH_STATE_FIELD = \
CHASH_FILLED; \
} while(0)
#define chash_lookup(hashtable, _key, storage, namespace) \
storage; \
\
do { \
int __CHASH_INDEX = 0; \
long __CHASH_HASH = 0; \
namespace ## _BUCKET __CHASH_KEY_BUCKET; \
memset(&__CHASH_KEY_BUCKET, 0, sizeof(namespace ## _BUCKET)); \
namespace ## _INIT(__CHASH_KEY_BUCKET, _key, \
__CHASH_KEY_BUCKET.CHASH_VALUE_FIELD); \
\
(void) __CHASH_KEY_BUCKET; \
\
__chash_assert_nonnull(chash_lookup, hashtable); \
__chash_assert_nonnull(chash_lookup, (hashtable)->CHASH_BUCKETS_FIELD); \
__chash_hash((hashtable)->CHASH_CAPACITY_FIELD, _key, namespace); \
__chash_probe(hashtable, _key, namespace) \
\
if(((hashtable)->CHASH_BUCKETS_FIELD[__CHASH_HASH].CHASH_STATE_FIELD != \
CHASH_FILLED) || __CHASH_INDEX != -1) { \
fprintf(stderr, "chash_lookup: failed to find key in hashtable (%s:%i)" \
"\n", __FILE__, __LINE__); \
abort(); \
} \
\
storage = (hashtable)->CHASH_BUCKETS_FIELD[__CHASH_HASH].CHASH_VALUE_FIELD; \
} while(0)
#define chash_delete(hashtable, _key, namespace) \
do { \
int __CHASH_INDEX = 0; \
long __CHASH_HASH = 0; \
\
__chash_assert_nonnull(chash_delete, hashtable); \
__chash_assert_nonnull(chash_delete, (hashtable)->CHASH_BUCKETS_FIELD); \
__chash_hash((hashtable)->CHASH_CAPACITY_FIELD, _key, namespace); \
__chash_probe(hashtable, _key, namespace) \
\
if(((hashtable)->CHASH_BUCKETS_FIELD[__CHASH_HASH].CHASH_STATE_FIELD != \
CHASH_FILLED) || __CHASH_INDEX != -1) { \
fprintf(stderr, "chash_delete: failed to find key in hashtable (%s:%i)" \
"\n", __FILE__, __LINE__); \
abort(); \
} \
\
namespace ## _FREE_KEY((hashtable)->CHASH_BUCKETS_FIELD[__CHASH_HASH] \
.CHASH_KEY_FIELD); \
namespace ## _FREE_VALUE( \
(hashtable)->CHASH_BUCKETS_FIELD[__CHASH_HASH].CHASH_VALUE_FIELD); \
(hashtable)->CHASH_BUCKETS_FIELD[__CHASH_HASH].CHASH_STATE_FIELD = \
CHASH_TOMBSTONE; \
(hashtable)->CHASH_LENGTH_FIELD--; \
} while(0)
#define chash_contains(hashtable, _key, storage, namespace) \
1; \
\
do { \
int __CHASH_INDEX = 0; \
long __CHASH_HASH = 0; \
\
__chash_assert_nonnull(chash_contents, hashtable); \
__chash_assert_nonnull(chash_contents, (hashtable)->CHASH_BUCKETS_FIELD); \
__chash_hash((hashtable)->CHASH_CAPACITY_FIELD, _key, namespace); \
__chash_probe(hashtable, _key, namespace) \
\
if(((hashtable)->CHASH_BUCKETS_FIELD[__CHASH_HASH].CHASH_STATE_FIELD != \
CHASH_FILLED) || __CHASH_INDEX != -1) { \
storage = 0; \
} \
} while(0)
#define chash_lookup_bucket(hashtable, _key, storage, namespace) \
storage; \
\
do { \
CHASH_COUNTER_TYPE __CHASH_INDEX = 0; \
long __CHASH_HASH = 0; \
namespace ## _BUCKET __CHASH_KEY_BUCKET; \
memset(&__CHASH_KEY_BUCKET, 0, sizeof(namespace ## _BUCKET)); \
namespace ## _INIT(__CHASH_KEY_BUCKET, _key, \
__CHASH_KEY_BUCKET.CHASH_VALUE_FIELD); \
\
(void) __CHASH_KEY_BUCKET; \
\
__chash_assert_nonnull(chash_lookup_bucket, hashtable); \
__chash_assert_nonnull(chash_lookup_bucket, \
(hashtable)->CHASH_BUCKETS_FIELD); \
__chash_hash((hashtable)->CHASH_CAPACITY_FIELD, _key, namespace); \
__chash_probe(hashtable, _key, namespace) \
\
if(((hashtable)->CHASH_BUCKETS_FIELD[__CHASH_HASH].CHASH_STATE_FIELD != \
CHASH_FILLED) || __CHASH_INDEX != -1) { \
fprintf(stderr, "chash_lookup_bucket: failed to find key in hashtable" \
"(%s:%i) \n", __FILE__, __LINE__); \
abort(); \
} \
\
storage = ((hashtable)->CHASH_BUCKETS_FIELD + __CHASH_HASH); \
} while(0)
#define __chash_free(hashtable, namespace) \
do { \
__chash_assert_nonnull(__chash_free, hashtable); \
__chash_assert_nonnull(__chash_free, (hashtable)->CHASH_BUCKETS_FIELD); \
(hashtable)->CHASH_CAPACITY_FIELD--; \
\
while((hashtable)->CHASH_CAPACITY_FIELD != -1) { \
if((hashtable)->CHASH_BUCKETS_FIELD[(hashtable)->CHASH_CAPACITY_FIELD] \
.CHASH_STATE_FIELD != CHASH_FILLED) { \
(hashtable)->CHASH_CAPACITY_FIELD--; \
continue; \
} \
\
namespace ##_FREE_KEY( \
(hashtable)->CHASH_BUCKETS_FIELD[(hashtable)->CHASH_CAPACITY_FIELD] \
.CHASH_KEY_FIELD); \
namespace ##_FREE_VALUE( \
(hashtable)->CHASH_BUCKETS_FIELD[(hashtable)->CHASH_CAPACITY_FIELD] \
.CHASH_VALUE_FIELD); \
(hashtable)->CHASH_CAPACITY_FIELD--; \
(hashtable)->CHASH_LENGTH_FIELD--; \
} \
\
if((namespace ## _HEAP) == 1) { \
free((hashtable)->CHASH_BUCKETS_FIELD); \
} \
} while(0)
#define chash_free(hashtable, namespace) \
do { \
__chash_assert_nonnull(chash_free, hashtable); \
__chash_assert_nonnull(chash_free, (hashtable)->CHASH_BUCKETS_FIELD); \
(hashtable)->CHASH_CAPACITY_FIELD--; \
\
while((hashtable)->CHASH_CAPACITY_FIELD != -1) { \
if((hashtable)->CHASH_BUCKETS_FIELD[(hashtable)->CHASH_CAPACITY_FIELD] \
.CHASH_STATE_FIELD != CHASH_FILLED) { \
(hashtable)->CHASH_CAPACITY_FIELD--; \
continue; \
} \
\
namespace ##_FREE_KEY( \
(hashtable)->CHASH_BUCKETS_FIELD[(hashtable)->CHASH_CAPACITY_FIELD] \
.CHASH_KEY_FIELD); \
namespace ##_FREE_VALUE( \
(hashtable)->CHASH_BUCKETS_FIELD[(hashtable)->CHASH_CAPACITY_FIELD] \
.CHASH_VALUE_FIELD); \
(hashtable)->CHASH_CAPACITY_FIELD--; \
(hashtable)->CHASH_LENGTH_FIELD--; \
} \
\
if((namespace ## _HEAP) == 1) { \
free((hashtable)->CHASH_BUCKETS_FIELD); \
free((hashtable)); \
} \
} while(0)
#define chash_is_full(hashtable, namespace) \
(((hashtable)->CHASH_LENGTH_FIELD) == ((hashtable)->CHASH_CAPACITY_FIELD))
/* Iterator logic */
#define chash_iter(hashtable, index, _key, _value) \
for((index) = 0, (_key) = (hashtable)->CHASH_BUCKETS_FIELD[index]. \
CHASH_KEY_FIELD, \
(_value) = (hashtable)->CHASH_BUCKETS_FIELD[index].CHASH_VALUE_FIELD; \
(index) < (hashtable)->CHASH_CAPACITY_FIELD; \
(index) = ((index) < (hashtable)->CHASH_CAPACITY_FIELD) \
? ((index) + 1) : index, \
(_key) = (hashtable)->CHASH_BUCKETS_FIELD[index].CHASH_KEY_FIELD, \
(_value) = (hashtable)->CHASH_BUCKETS_FIELD[index].CHASH_VALUE_FIELD, \
(index) = (hashtable)->CHASH_CAPACITY_FIELD)
#define chash_skip(hashtable, index) \
if((hashtable)->CHASH_BUCKETS_FIELD[index]. \
CHASH_STATE_FIELD != CHASH_FILLED) \
continue;
#endif
+464
View File
@@ -0,0 +1,464 @@
/* Clocks (v1)
* Portable Snippets - https://github.com/nemequ/portable-snippets
* Created by Evan Nemerson <evan@nemerson.com>
*
* To the extent possible under law, the authors have waived all
* copyright and related or neighboring rights to this code. For
* details, see the Creative Commons Zero 1.0 Universal license at
* https://creativecommons.org/publicdomain/zero/1.0/
*/
#if !defined(PSNIP_CLOCK_H)
#define PSNIP_CLOCK_H
/* For maximum portability include the exact-int module from
portable snippets. */
#if !defined(psnip_uint64_t) || !defined(psnip_int32_t) || \
!defined(psnip_uint32_t) || !defined(psnip_int32_t)
# include <stdint.h>
# if !defined(psnip_int64_t)
# define psnip_int64_t int64_t
# endif
# if !defined(psnip_uint64_t)
# define psnip_uint64_t uint64_t
# endif
# if !defined(psnip_int32_t)
# define psnip_int32_t int32_t
# endif
# if !defined(psnip_uint32_t)
# define psnip_uint32_t uint32_t
# endif
#endif
#if !defined(PSNIP_CLOCK_STATIC_INLINE)
# if defined(__GNUC__)
# define PSNIP_CLOCK__COMPILER_ATTRIBUTES __attribute__((__unused__))
# else
# define PSNIP_CLOCK__COMPILER_ATTRIBUTES
# endif
# define PSNIP_CLOCK__FUNCTION PSNIP_CLOCK__COMPILER_ATTRIBUTES static
#endif
enum PsnipClockType {
/* This clock provides the current time, in units since 1970-01-01
* 00:00:00 UTC not including leap seconds. In other words, UNIX
* time. Keep in mind that this clock doesn't account for leap
* seconds, and can go backwards (think NTP adjustments). */
PSNIP_CLOCK_TYPE_WALL = 1,
/* The CPU time is a clock which increases only when the current
* process is active (i.e., it doesn't increment while blocking on
* I/O). */
PSNIP_CLOCK_TYPE_CPU = 2,
/* Monotonic time is always running (unlike CPU time), but it only
ever moves forward unless you reboot the system. Things like NTP
adjustments have no effect on this clock. */
PSNIP_CLOCK_TYPE_MONOTONIC = 3
};
struct PsnipClockTimespec {
psnip_uint64_t seconds;
psnip_uint64_t nanoseconds;
};
/* Methods we support: */
#define PSNIP_CLOCK_METHOD_CLOCK_GETTIME 1
#define PSNIP_CLOCK_METHOD_TIME 2
#define PSNIP_CLOCK_METHOD_GETTIMEOFDAY 3
#define PSNIP_CLOCK_METHOD_QUERYPERFORMANCECOUNTER 4
#define PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME 5
#define PSNIP_CLOCK_METHOD_CLOCK 6
#define PSNIP_CLOCK_METHOD_GETPROCESSTIMES 7
#define PSNIP_CLOCK_METHOD_GETRUSAGE 8
#define PSNIP_CLOCK_METHOD_GETSYSTEMTIMEPRECISEASFILETIME 9
#define PSNIP_CLOCK_METHOD_GETTICKCOUNT64 10
#include <assert.h>
#if defined(HEDLEY_UNREACHABLE)
# define PSNIP_CLOCK_UNREACHABLE() HEDLEY_UNREACHABLE()
#else
# define PSNIP_CLOCK_UNREACHABLE() assert(0)
#endif
/* Choose an implementation */
/* #undef PSNIP_CLOCK_WALL_METHOD */
/* #undef PSNIP_CLOCK_CPU_METHOD */
/* #undef PSNIP_CLOCK_MONOTONIC_METHOD */
/* We want to be able to detect the libc implementation, so we include
<limits.h> (<features.h> isn't available everywhere). */
#if defined(__unix__) || defined(__unix) || defined(__linux__)
# include <limits.h>
# include <unistd.h>
#endif
#if defined(_POSIX_TIMERS) && (_POSIX_TIMERS > 0)
/* glibc 2.17+ and FreeBSD are known to work without librt. If you
* know of others please let us know so we can add them. */
# if \
(defined(__GLIBC__) && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 17))) || \
(defined(__FreeBSD__)) || \
!defined(PSNIP_CLOCK_NO_LIBRT)
/* Even though glibc unconditionally sets _POSIX_TIMERS, it doesn't
actually declare the relevant APIs unless _POSIX_C_SOURCE >=
199309L, and if you compile in standard C mode (e.g., c11 instead
of gnu11) _POSIX_C_SOURCE will be unset by default. */
# if _POSIX_C_SOURCE >= 199309L
# define PSNIP_CLOCK_HAVE_CLOCK_GETTIME
# endif
# endif
#endif
#if defined(_WIN32)
# if !defined(PSNIP_CLOCK_CPU_METHOD)
# define PSNIP_CLOCK_CPU_METHOD PSNIP_CLOCK_METHOD_GETPROCESSTIMES
# endif
# if !defined(PSNIP_CLOCK_MONOTONIC_METHOD)
# define PSNIP_CLOCK_MONOTONIC_METHOD PSNIP_CLOCK_METHOD_QUERYPERFORMANCECOUNTER
# endif
#endif
#if defined(__MACH__) && !defined(__gnu_hurd__)
# if !defined(PSNIP_CLOCK_MONOTONIC_METHOD)
# define PSNIP_CLOCK_MONOTONIC_METHOD PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME
# endif
#endif
#if defined(PSNIP_CLOCK_HAVE_CLOCK_GETTIME)
# include <time.h>
# if !defined(PSNIP_CLOCK_WALL_METHOD)
# if defined(CLOCK_REALTIME_PRECISE)
# define PSNIP_CLOCK_WALL_METHOD PSNIP_CLOCK_METHOD_CLOCK_GETTIME
# define PSNIP_CLOCK_CLOCK_GETTIME_WALL CLOCK_REALTIME_PRECISE
# elif !defined(__sun)
# define PSNIP_CLOCK_WALL_METHOD PSNIP_CLOCK_METHOD_CLOCK_GETTIME
# define PSNIP_CLOCK_CLOCK_GETTIME_WALL CLOCK_REALTIME
# endif
# endif
# if !defined(PSNIP_CLOCK_CPU_METHOD)
# if defined(_POSIX_CPUTIME) || defined(CLOCK_PROCESS_CPUTIME_ID)
# define PSNIP_CLOCK_CPU_METHOD PSNIP_CLOCK_METHOD_CLOCK_GETTIME
# define PSNIP_CLOCK_CLOCK_GETTIME_CPU CLOCK_PROCESS_CPUTIME_ID
# elif defined(CLOCK_VIRTUAL)
# define PSNIP_CLOCK_CPU_METHOD PSNIP_CLOCK_METHOD_CLOCK_GETTIME
# define PSNIP_CLOCK_CLOCK_GETTIME_CPU CLOCK_VIRTUAL
# endif
# endif
# if !defined(PSNIP_CLOCK_MONOTONIC_METHOD)
# if defined(_POSIX_MONOTONIC_CLOCK) || defined(CLOCK_MONOTONIC)
# define PSNIP_CLOCK_MONOTONIC_METHOD PSNIP_CLOCK_METHOD_CLOCK_GETTIME
# define PSNIP_CLOCK_CLOCK_GETTIME_MONOTONIC CLOCK_MONOTONIC
# endif
# endif
#endif
#if defined(_POSIX_VERSION) && (_POSIX_VERSION >= 200112L)
# if !defined(PSNIP_CLOCK_WALL_METHOD)
# define PSNIP_CLOCK_WALL_METHOD PSNIP_CLOCK_METHOD_GETTIMEOFDAY
# endif
#endif
#if !defined(PSNIP_CLOCK_WALL_METHOD)
# define PSNIP_CLOCK_WALL_METHOD PSNIP_CLOCK_METHOD_TIME
#endif
#if !defined(PSNIP_CLOCK_CPU_METHOD)
# define PSNIP_CLOCK_CPU_METHOD PSNIP_CLOCK_METHOD_CLOCK
#endif
/* Primarily here for testing. */
#if !defined(PSNIP_CLOCK_MONOTONIC_METHOD) && defined(PSNIP_CLOCK_REQUIRE_MONOTONIC)
# error No monotonic clock found.
#endif
/* Implementations */
#if \
(defined(PSNIP_CLOCK_CPU_METHOD) && (PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME)) || \
(defined(PSNIP_CLOCK_WALL_METHOD) && (PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME)) || \
(defined(PSNIP_CLOCK_MONOTONIC_METHOD) && (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME)) || \
(defined(PSNIP_CLOCK_CPU_METHOD) && (PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_CLOCK)) || \
(defined(PSNIP_CLOCK_WALL_METHOD) && (PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_CLOCK)) || \
(defined(PSNIP_CLOCK_MONOTONIC_METHOD) && (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_CLOCK)) || \
(defined(PSNIP_CLOCK_CPU_METHOD) && (PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_TIME)) || \
(defined(PSNIP_CLOCK_WALL_METHOD) && (PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_TIME)) || \
(defined(PSNIP_CLOCK_MONOTONIC_METHOD) && (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_TIME))
# include <time.h>
#endif
#if \
(defined(PSNIP_CLOCK_CPU_METHOD) && (PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_GETTIMEOFDAY)) || \
(defined(PSNIP_CLOCK_WALL_METHOD) && (PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_GETTIMEOFDAY)) || \
(defined(PSNIP_CLOCK_MONOTONIC_METHOD) && (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_GETTIMEOFDAY))
# include <sys/time.h>
#endif
#if \
(defined(PSNIP_CLOCK_CPU_METHOD) && (PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_GETPROCESSTIMES)) || \
(defined(PSNIP_CLOCK_WALL_METHOD) && (PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_GETPROCESSTIMES)) || \
(defined(PSNIP_CLOCK_MONOTONIC_METHOD) && (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_GETPROCESSTIMES)) || \
(defined(PSNIP_CLOCK_CPU_METHOD) && (PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_GETTICKCOUNT64)) || \
(defined(PSNIP_CLOCK_WALL_METHOD) && (PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_GETTICKCOUNT64)) || \
(defined(PSNIP_CLOCK_MONOTONIC_METHOD) && (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_GETTICKCOUNT64))
# include <windows.h>
#endif
#if \
(defined(PSNIP_CLOCK_CPU_METHOD) && (PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_GETRUSAGE)) || \
(defined(PSNIP_CLOCK_WALL_METHOD) && (PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_GETRUSAGE)) || \
(defined(PSNIP_CLOCK_MONOTONIC_METHOD) && (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_GETRUSAGE))
# include <sys/time.h>
# include <sys/resource.h>
#endif
#if \
(defined(PSNIP_CLOCK_CPU_METHOD) && (PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME)) || \
(defined(PSNIP_CLOCK_WALL_METHOD) && (PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME)) || \
(defined(PSNIP_CLOCK_MONOTONIC_METHOD) && (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME))
# include <CoreServices/CoreServices.h>
# include <mach/mach.h>
# include <mach/mach_time.h>
#endif
/*** Implementations ***/
#define PSNIP_CLOCK_NSEC_PER_SEC ((psnip_uint32_t) (1000000000ULL))
#if \
(defined(PSNIP_CLOCK_CPU_METHOD) && (PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME)) || \
(defined(PSNIP_CLOCK_WALL_METHOD) && (PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME)) || \
(defined(PSNIP_CLOCK_MONOTONIC_METHOD) && (PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME))
PSNIP_CLOCK__FUNCTION psnip_uint32_t
psnip_clock__clock_getres (clockid_t clk_id) {
struct timespec res;
int r;
r = clock_getres(clk_id, &res);
if (r != 0)
return 0;
return (psnip_uint32_t) (PSNIP_CLOCK_NSEC_PER_SEC / res.tv_nsec);
}
PSNIP_CLOCK__FUNCTION int
psnip_clock__clock_gettime (clockid_t clk_id, struct PsnipClockTimespec* res) {
struct timespec ts;
if (clock_gettime(clk_id, &ts) != 0)
return -10;
res->seconds = (psnip_uint64_t) (ts.tv_sec);
res->nanoseconds = (psnip_uint64_t) (ts.tv_nsec);
return 0;
}
#endif
PSNIP_CLOCK__FUNCTION psnip_uint32_t
psnip_clock_wall_get_precision (void) {
#if !defined(PSNIP_CLOCK_WALL_METHOD)
return 0;
#elif defined(PSNIP_CLOCK_WALL_METHOD) && PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME
return psnip_clock__clock_getres(PSNIP_CLOCK_CLOCK_GETTIME_WALL);
#elif defined(PSNIP_CLOCK_WALL_METHOD) && PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_GETTIMEOFDAY
return 1000000;
#elif defined(PSNIP_CLOCK_WALL_METHOD) && PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_TIME
return 1;
#else
return 0;
#endif
}
PSNIP_CLOCK__FUNCTION int
psnip_clock_wall_get_time (struct PsnipClockTimespec* res) {
(void) res;
#if !defined(PSNIP_CLOCK_WALL_METHOD)
return -2;
#elif defined(PSNIP_CLOCK_WALL_METHOD) && PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME
return psnip_clock__clock_gettime(PSNIP_CLOCK_CLOCK_GETTIME_WALL, res);
#elif defined(PSNIP_CLOCK_WALL_METHOD) && PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_TIME
res->seconds = (uint64_t) time(NULL);
res->nanoseconds = 0;
#elif defined(PSNIP_CLOCK_WALL_METHOD) && PSNIP_CLOCK_WALL_METHOD == PSNIP_CLOCK_METHOD_GETTIMEOFDAY
struct timeval tv;
if (gettimeofday(&tv, NULL) != 0)
return -6;
res->seconds = tv.tv_sec;
res->nanoseconds = tv.tv_usec * 1000;
#else
return -2;
#endif
return 0;
}
PSNIP_CLOCK__FUNCTION psnip_uint32_t
psnip_clock_cpu_get_precision (void) {
#if !defined(PSNIP_CLOCK_CPU_METHOD)
return 0;
#elif defined(PSNIP_CLOCK_CPU_METHOD) && PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME
return psnip_clock__clock_getres(PSNIP_CLOCK_CLOCK_GETTIME_CPU);
#elif defined(PSNIP_CLOCK_CPU_METHOD) && PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_CLOCK
return CLOCKS_PER_SEC;
#elif defined(PSNIP_CLOCK_CPU_METHOD) && PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_GETPROCESSTIMES
return PSNIP_CLOCK_NSEC_PER_SEC / 100;
#else
return 0;
#endif
}
PSNIP_CLOCK__FUNCTION int
psnip_clock_cpu_get_time (struct PsnipClockTimespec* res) {
#if !defined(PSNIP_CLOCK_CPU_METHOD)
(void) res;
return -2;
#elif defined(PSNIP_CLOCK_CPU_METHOD) && PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME
return psnip_clock__clock_gettime(PSNIP_CLOCK_CLOCK_GETTIME_CPU, res);
#elif defined(PSNIP_CLOCK_CPU_METHOD) && PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_CLOCK
clock_t t = clock();
if (t == ((clock_t) -1))
return -5;
res->seconds = t / CLOCKS_PER_SEC;
res->nanoseconds = (t % CLOCKS_PER_SEC) * (PSNIP_CLOCK_NSEC_PER_SEC / CLOCKS_PER_SEC);
#elif defined(PSNIP_CLOCK_CPU_METHOD) && PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_GETPROCESSTIMES
FILETIME CreationTime, ExitTime, KernelTime, UserTime;
LARGE_INTEGER date, adjust;
if (!GetProcessTimes(GetCurrentProcess(), &CreationTime, &ExitTime, &KernelTime, &UserTime))
return -7;
/* http://www.frenk.com/2009/12/convert-filetime-to-unix-timestamp/ */
date.HighPart = UserTime.dwHighDateTime;
date.LowPart = UserTime.dwLowDateTime;
adjust.QuadPart = 11644473600000 * 10000;
date.QuadPart -= adjust.QuadPart;
res->seconds = date.QuadPart / 10000000;
res->nanoseconds = (date.QuadPart % 10000000) * (PSNIP_CLOCK_NSEC_PER_SEC / 100);
#elif PSNIP_CLOCK_CPU_METHOD == PSNIP_CLOCK_METHOD_GETRUSAGE
struct rusage usage;
if (getrusage(RUSAGE_SELF, &usage) != 0)
return -8;
res->seconds = usage.ru_utime.tv_sec;
res->nanoseconds = tv.tv_usec * 1000;
#else
(void) res;
return -2;
#endif
return 0;
}
PSNIP_CLOCK__FUNCTION psnip_uint32_t
psnip_clock_monotonic_get_precision (void) {
#if !defined(PSNIP_CLOCK_MONOTONIC_METHOD)
return 0;
#elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME
return psnip_clock__clock_getres(PSNIP_CLOCK_CLOCK_GETTIME_MONOTONIC);
#elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME
static mach_timebase_info_data_t tbi = { 0, };
if (tbi.denom == 0)
mach_timebase_info(&tbi);
return (psnip_uint32_t) (tbi.numer / tbi.denom);
#elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_GETTICKCOUNT64
return 1000;
#elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_QUERYPERFORMANCECOUNTER
LARGE_INTEGER Frequency;
QueryPerformanceFrequency(&Frequency);
return (psnip_uint32_t) ((Frequency.QuadPart > PSNIP_CLOCK_NSEC_PER_SEC) ? PSNIP_CLOCK_NSEC_PER_SEC : Frequency.QuadPart);
#else
return 0;
#endif
}
PSNIP_CLOCK__FUNCTION int
psnip_clock_monotonic_get_time (struct PsnipClockTimespec* res) {
#if !defined(PSNIP_CLOCK_MONOTONIC_METHOD)
(void) res;
return -2;
#elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_CLOCK_GETTIME
return psnip_clock__clock_gettime(PSNIP_CLOCK_CLOCK_GETTIME_MONOTONIC, res);
#elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_MACH_ABSOLUTE_TIME
psnip_uint64_t nsec = mach_absolute_time();
static mach_timebase_info_data_t tbi = { 0, };
if (tbi.denom == 0)
mach_timebase_info(&tbi);
nsec *= ((psnip_uint64_t) tbi.numer) / ((psnip_uint64_t) tbi.denom);
res->seconds = nsec / PSNIP_CLOCK_NSEC_PER_SEC;
res->nanoseconds = nsec % PSNIP_CLOCK_NSEC_PER_SEC;
#elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_QUERYPERFORMANCECOUNTER
LARGE_INTEGER t, f;
if (QueryPerformanceCounter(&t) == 0)
return -12;
QueryPerformanceFrequency(&f);
res->seconds = t.QuadPart / f.QuadPart;
res->nanoseconds = t.QuadPart % f.QuadPart;
if (f.QuadPart > PSNIP_CLOCK_NSEC_PER_SEC)
res->nanoseconds /= f.QuadPart / PSNIP_CLOCK_NSEC_PER_SEC;
else
res->nanoseconds *= PSNIP_CLOCK_NSEC_PER_SEC / f.QuadPart;
#elif defined(PSNIP_CLOCK_MONOTONIC_METHOD) && PSNIP_CLOCK_MONOTONIC_METHOD == PSNIP_CLOCK_METHOD_GETTICKCOUNT64
const ULONGLONG msec = GetTickCount64();
res->seconds = msec / 1000;
res->nanoseconds = sec % 1000;
#else
return -2;
#endif
return 0;
}
/* Returns the number of ticks per second for the specified clock.
* For example, a clock with millisecond precision would return 1000,
* and a clock with 1 second (such as the time() function) would
* return 1.
*
* If the requested clock isn't available, it will return 0.
* Hopefully this will be rare, but if it happens to you please let us
* know so we can work on finding a way to support your system.
*
* Note that different clocks on the same system often have a
* different precisions.
*/
PSNIP_CLOCK__FUNCTION psnip_uint32_t
psnip_clock_get_precision (enum PsnipClockType clock_type) {
switch (clock_type) {
case PSNIP_CLOCK_TYPE_MONOTONIC:
return psnip_clock_monotonic_get_precision ();
case PSNIP_CLOCK_TYPE_CPU:
return psnip_clock_cpu_get_precision ();
case PSNIP_CLOCK_TYPE_WALL:
return psnip_clock_wall_get_precision ();
}
PSNIP_CLOCK_UNREACHABLE();
return 0;
}
/* Set the provided timespec to the requested time. Returns 0 on
* success, or a negative value on failure. */
PSNIP_CLOCK__FUNCTION int
psnip_clock_get_time (enum PsnipClockType clock_type, struct PsnipClockTimespec* res) {
assert(res != NULL);
switch (clock_type) {
case PSNIP_CLOCK_TYPE_MONOTONIC:
return psnip_clock_monotonic_get_time (res);
case PSNIP_CLOCK_TYPE_CPU:
return psnip_clock_cpu_get_time (res);
case PSNIP_CLOCK_TYPE_WALL:
return psnip_clock_wall_get_time (res);
}
return -1;
}
#endif /* !defined(PSNIP_CLOCK_H) */
+137
View File
@@ -0,0 +1,137 @@
#ifndef COG_UTILS_H
#define COG_UTILS_H
#include <stdio.h>
#include <stdint.h>
#include "attributes.h"
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
/**
* @brief Load file contents into a string
*
* @param fp the file to be read
* @param len optional pointer to store the amount of bytes read
* @return the file contents
*/
char *cog_load_whole_file_fp(FILE *fp, size_t *len);
/**
* @brief Load file contents into a string
*
* Wrapper over cog_load_whole_file_fp(), get the file by its
* relative-path.
* @param filename the name of the file to be read
* @param len optional pointer to store the amount of bytes read
* @return the file contents
*/
char *cog_load_whole_file(const char filename[], size_t *len);
/**
* @brief Get the difference between UTC and the latest local standard time, in
* seconds.
* @return difference between UTC and local time in seconds
*/
long cog_timezone(void);
/**
* @brief Convert a iso8601 string to a unix timestamp (milliseconds)
*
* Can be matched to the json_extract() and json_inject() %F specifier
* @param str the iso8601 string timestamp
* @param len the string length
* @param p_value pointer to the `uint64_t` variable to receive the converted
* timestamp
* @return 1 on success, 0 on failure
*/
int cog_iso8601_to_unix_ms(const char str[], size_t len, uint64_t *p_value);
/**
* @brief Convert a unix timestamp (milliseconds) to a iso8601 string
*
* @param timestamp the buffer to receive the converted timestamp
* @param len the size of the buffer
* @param value the unix timestamp to be converted to iso8601
* @return the amount of characters (in bytes) written to the buffer
*/
int cog_unix_ms_to_iso8601(char str[], size_t len, const uint64_t value);
/**
* @brief Convert a numerical string to `uint64_t`
*
* @param str the numerical string
* @param len the string length
* @param p_value pointer to the `uint64_t` variable to receive the converted
* value
* @return 1 on success, 0 on failure
*/
int cog_strtou64(char *str, size_t len, uint64_t *p_value);
/**
* @brief Convert `uint64_t` to a numerical string
*
* @param str the buffer to store the numerical string
* @param len the size of the buffer
* @param p_value the `unsigned long long` value
* @return the amount of characters (in bytes) written to the buffer
*/
int cog_u64tostr(char *str, size_t len, uint64_t *p_value);
/**
* @brief Copies at most `len` bytes of `src` to `*p_dest`.
*
* Analogous to `strndup()`
* @param src the buffer to be copied
* @param len the maximum amount of characters to be copied
* @param p_dest a pointer to the new `src` copy
* @return length of copied string on success, 0 on failure
*/
size_t cog_strndup(const char src[], size_t len, char **p_dest);
/**
* @brief Copies at most `len` bytes of `src` to `*p_dest`.
*
* Analogous to `asprintf()`
* @param strp source to write resulting string to
* @param fmt printf format string
* @param ... variadic arguments to be matched to `fmt` specifiers
* @return length of copied string on success, -1 on failure
*/
size_t cog_asprintf(char **strp, const char fmt[], ...) PRINTF_LIKE(2, 3);
/**
* @brief Sleep for amount of milliseconds
*
* @param tms amount of milliseconds to sleep for
* @return 0 on success, -1 on error with an `errno` set to indicate the error
*/
int cog_sleep_ms(const long tms);
/**
* @brief Sleep for amount of microseconds
*
* @param tms amount of microseconds to sleep for
* @return 0 on success, -1 on error with an `errno` set to indicate the error
*/
int cog_sleep_us(const long tms);
/**
* @brief Get the current timestamp in milliseconds
*
* @return the timestamp on success, 0 on failure
*/
uint64_t cog_timestamp_ms(void);
/**
* @brief Get the current timestamp in microseconds
*
* @return the timestamp on success, 0 on failure
*/
uint64_t cog_timestamp_us(void);
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* COG_UTILS_H */
+69
View File
@@ -0,0 +1,69 @@
/** @file error.h */
#ifndef CONCORD_ERROR_H
#define CONCORD_ERROR_H
/** @defgroup ConcordError Error handling
* @brief Concord error codes and meaning
* @{ */
/** @brief Concord error codes */
typedef int CCORDcode;
/* XXX: As new values are added, ccord_strerror() and ccord_code_as_string()
* should be updated accordingly! */
/** @defgroup CoreError Core error codes
* @brief These codes are used by the core library and should be used by all
* modules
* @
* @{ */
/** most likely a bug in the library, please report it */
#define CCORD_INTERNAL_ERROR -200
/** couldn't encode format */
#define CCORD_BAD_ENCODE -101
/** couldn't decode format */
#define CCORD_BAD_DECODE -100
/** out of memory, something really bad happened! */
#define CCORD_OUT_OF_MEMORY -60
/** check strerror() for more information */
#define CCORD_ERRNO -50
/** curl has been compiled without the --enable-websockets flag */
#define CCORD_CURL_WEBSOCKETS_MISSING -14
/** curl need to be updated to 8.7.1 or greater */
#define CCORD_CURL_OUTDATED_VERSION -13
/** failure when creating request's payload */
#define CCORD_MALFORMED_PAYLOAD -12
/** couldn't enqueue worker thread (queue is full) */
#define CCORD_FULL_WORKER -11
/** couldn't perform action because resource is unavailable */
#define CCORD_RESOURCE_UNAVAILABLE -10
/** couldn't cleanup resource automatically due to being claimed */
#define CCORD_RESOURCE_OWNERSHIP -9
/** attempt to initialize globals more than once */
#define CCORD_GLOBAL_INIT -8
/** curl's multi handle internal error */
#define CCORD_CURLM_INTERNAL -7
/** curl's easy handle internal error */
#define CCORD_CURLE_INTERNAL -6
/** internal failure when encoding or decoding JSON */
#define CCORD_BAD_JSON -5
/** bad value for parameter */
#define CCORD_BAD_PARAMETER -4
/** received a non-standard http code */
#define CCORD_UNUSUAL_HTTP_CODE -3
/** no response came through from curl */
#define CCORD_CURL_NO_RESPONSE -2
/** request wasn't succesful */
#define CCORD_HTTP_CODE -1
/** action was a success */
#define CCORD_OK 0
const char *ccord_code_as_string(CCORDcode code);
const char *ccord_strerror(CCORDcode code);
/** @} CoreError */
/** @} ConcordError */
#endif /* CONCORD_ERROR_H */
+50
View File
@@ -0,0 +1,50 @@
/**
* @file concord-notifier.h
* @author Cogmasters
* @brief Notifier fds listening to pipe, this can be used to propagate events
*/
#ifndef CONCORD_NOTIFIER_H
#define CONCORD_NOTIFIER_H
#include "concord-error.h"
/**
* @brief Open notifier pipe
*
* @param pipe The pipe to open for emitting notifications
* @return CCORDcode on success, CCORD_ERRNO on error
*/
CCORDcode ccord_notifier_open(int pipe[2]);
/**
* @brief Close notifier pipe
*
* @param pipe The pipe to close
*/
void ccord_notifier_close(int pipe[2]);
/**
* @brief Notify fds listening to pipe
*
* @param pipe The pipe to notify
*/
void ccord_notifier_notify(int pipe[2]);
/**
* @brief Whether or not pipe is currently notifying fds
*
* @param pipe The pipe to check
* @return 1 if notifying, 0 if not
*/
_Bool ccord_notifier_is_notifying(int pipe[2]);
/**
* @brief Receive a listener for pipe notifications
*
* @param pipe The pipe to listen to
* @return fd on success, -1 on error
*/
int ccord_notifier_listen(const int pipe[2]);
#endif /* CONCORD_NOTIFIER_H */
+49
View File
@@ -0,0 +1,49 @@
/**
* @file concord-once.h
* @author Cogmasters
* @brief Initialized once
*/
#ifndef CONCORD_ONCE_H
#define CONCORD_ONCE_H
#include "concord-error.h"
/** Callback function type for user initialization */
typedef CCORDcode (*ccord_once_cb)(long flags);
/**
* @brief Register a user callback for initialization
*
* This callback will be executed exactly once during initialization
* after all internal initializations have been performed.
* Multiple callbacks can be registered from different modules, and they will
* be executed in the order they were registered.
*
* @param callback The function to call during initialization
* @param flags Flag to pass to the callback, that will be passed to the
* callback function
* @CCORD_return
*/
CCORDcode ccord_once_set_callback(ccord_once_cb callback, long flags);
/**
* @brief Initialize context once
*
* @param once Pointer to a static boolean flag that will be set to true if the
* initialization was successful. This flag should be used to ensure that
* the initialization is only performed once.
* @CCORD_return
*/
CCORDcode ccord_once(_Bool *once);
/**
* @brief Cleanup once context
*
* This function will be called to cleanup the once context.
* It will be called when the program is exiting, and it will
* cleanup all registered callbacks.
*/
void ccord_once_cleanup(void);
#endif /* CONCORD_ONCE_H */
+51
View File
@@ -0,0 +1,51 @@
/**
* @file discord-cache.h
* @author Cogmasters
* @brief Caching of Discord resources
*/
#ifndef DISCORD_CACHE_H
#define DISCORD_CACHE_H
/** @defgroup DiscordClientCache Caching
* @ingroup DiscordClient
* @brief Caching API supported by Concord
* @{ */
enum discord_cache_options {
DISCORD_CACHE_MESSAGES = 1 << 0,
DISCORD_CACHE_GUILDS = 1 << 1,
};
void discord_cache_enable(struct discord *client,
enum discord_cache_options options);
/**
* @brief Get a message from cache, only if locally available in RAM
* @note When done, discord_unclaim() must be called on the message resource
*
* @param client the client initialized with discord_from_token()
* @param channel_id the channel id the message is in
* @param message_id the id of the message
* @return `NULL` if not found, or a cache'd message
*/
const struct discord_message *discord_cache_get_channel_message(
struct discord *client, u64snowflake channel_id, u64snowflake message_id);
/**
* @brief Get a guild from cache, only if locally available in RAM
* @note When done, discord_unclaim() must be called on the guild resource
*
* @param client the client initialized with discord_from_token()
* @param guild_id the id of the guild
* @return `NULL` if not found, or a cache'd guild
*/
const struct discord_guild *discord_cache_get_guild(struct discord *client,
u64snowflake guild_id);
/** @example cache.c
* Demonstrates cache usage */
/** @} DiscordClientCache */
#endif /* DISCORD_CACHE_H */
+989
View File
@@ -0,0 +1,989 @@
/**
* @file discord-events.h
* @author Cogmasters
* @brief Listen, react and trigger Discord Gateway events
*/
#ifndef DISCORD_EVENTS_H
#define DISCORD_EVENTS_H
/** @defgroup DiscordCommands Commands
* @ingroup DiscordClient
* @brief Requests made by the client to the Gateway socket
* @{ */
/**
* @brief Request all members for a guild or a list of guilds
* @see
* https://discord.com/developers/docs/topics/gateway#request-guild-members
*
* @param client the client created with discord_from_token()
* @param request request guild members information
*/
void discord_request_guild_members(
struct discord *client, struct discord_request_guild_members *request);
/**
* @brief Sent when a client wants to join, move or disconnect from a voice
* channel
*
* @param client the client created with discord_from_token()
* @param update request guild members information
*/
void discord_update_voice_state(struct discord *client,
struct discord_update_voice_state *update);
/**
* @brief Update the client presence status
* @see discord_presence_add_activity()
*
* @param client the client created with discord_from_token()
* @param presence status to update the client's to
*/
void discord_update_presence(struct discord *client,
struct discord_presence_update *presence);
/** @} DiscordCommands */
/** @defgroup DiscordEvents Events
* @ingroup DiscordClient
* @brief Events sent over the Gateway socket to the client
* @{ */
/** @brief Discord Gateway's events */
enum discord_gateway_events {
DISCORD_EV_NONE = 0, /**< missing event */
DISCORD_EV_READY,
DISCORD_EV_RESUMED,
DISCORD_EV_RECONNECT,
DISCORD_EV_INVALID_SESSION,
DISCORD_EV_APPLICATION_COMMAND_PERMISSIONS_UPDATE,
DISCORD_EV_AUTO_MODERATION_RULE_CREATE,
DISCORD_EV_AUTO_MODERATION_RULE_UPDATE,
DISCORD_EV_AUTO_MODERATION_RULE_DELETE,
DISCORD_EV_AUTO_MODERATION_ACTION_EXECUTION,
DISCORD_EV_CHANNEL_CREATE,
DISCORD_EV_CHANNEL_UPDATE,
DISCORD_EV_CHANNEL_DELETE,
DISCORD_EV_CHANNEL_PINS_UPDATE,
DISCORD_EV_THREAD_CREATE,
DISCORD_EV_THREAD_UPDATE,
DISCORD_EV_THREAD_DELETE,
DISCORD_EV_THREAD_LIST_SYNC,
DISCORD_EV_THREAD_MEMBER_UPDATE,
DISCORD_EV_THREAD_MEMBERS_UPDATE,
DISCORD_EV_GUILD_CREATE,
DISCORD_EV_GUILD_UPDATE,
DISCORD_EV_GUILD_DELETE,
DISCORD_EV_GUILD_BAN_ADD,
DISCORD_EV_GUILD_BAN_REMOVE,
DISCORD_EV_GUILD_EMOJIS_UPDATE,
DISCORD_EV_GUILD_STICKERS_UPDATE,
DISCORD_EV_GUILD_INTEGRATIONS_UPDATE,
DISCORD_EV_GUILD_MEMBER_ADD,
DISCORD_EV_GUILD_MEMBER_REMOVE,
DISCORD_EV_GUILD_MEMBER_UPDATE,
DISCORD_EV_GUILD_MEMBERS_CHUNK,
DISCORD_EV_GUILD_ROLE_CREATE,
DISCORD_EV_GUILD_ROLE_UPDATE,
DISCORD_EV_GUILD_ROLE_DELETE,
DISCORD_EV_GUILD_SCHEDULED_EVENT_CREATE,
DISCORD_EV_GUILD_SCHEDULED_EVENT_UPDATE,
DISCORD_EV_GUILD_SCHEDULED_EVENT_DELETE,
DISCORD_EV_GUILD_SCHEDULED_EVENT_USER_ADD,
DISCORD_EV_GUILD_SCHEDULED_EVENT_USER_REMOVE,
DISCORD_EV_INTEGRATION_CREATE,
DISCORD_EV_INTEGRATION_UPDATE,
DISCORD_EV_INTEGRATION_DELETE,
DISCORD_EV_INTERACTION_CREATE,
DISCORD_EV_INVITE_CREATE,
DISCORD_EV_INVITE_DELETE,
DISCORD_EV_MESSAGE_CREATE,
DISCORD_EV_MESSAGE_UPDATE,
DISCORD_EV_MESSAGE_DELETE,
DISCORD_EV_MESSAGE_DELETE_BULK,
DISCORD_EV_MESSAGE_REACTION_ADD,
DISCORD_EV_MESSAGE_REACTION_REMOVE,
DISCORD_EV_MESSAGE_REACTION_REMOVE_ALL,
DISCORD_EV_MESSAGE_REACTION_REMOVE_EMOJI,
DISCORD_EV_PRESENCE_UPDATE,
DISCORD_EV_STAGE_INSTANCE_CREATE,
DISCORD_EV_STAGE_INSTANCE_DELETE,
DISCORD_EV_STAGE_INSTANCE_UPDATE,
DISCORD_EV_TYPING_START,
DISCORD_EV_USER_UPDATE,
DISCORD_EV_VOICE_STATE_UPDATE,
DISCORD_EV_VOICE_SERVER_UPDATE,
DISCORD_EV_WEBHOOKS_UPDATE,
DISCORD_EV_MAX /**< total amount of enumerators */
};
/**
* @brief return value of discord_set_event_scheduler() callback
* @see discord_set_event_scheduler()
*/
typedef enum discord_event_scheduler {
/** this event has been handled */
DISCORD_EVENT_IGNORE,
/** handle this event in main thread */
DISCORD_EVENT_MAIN_THREAD,
/** handle this event in a worker thread */
DISCORD_EVENT_WORKER_THREAD
} discord_event_scheduler_t;
/**
* @brief Event Handling Mode callback
*
* A very important callback that enables the user with a fine-grained control
* of how each event is handled: blocking, non-blocking or ignored
* @see discord_set_event_scheduler(), @ref discord_gateway_events
*/
typedef enum discord_event_scheduler (*discord_ev_scheduler)(
struct discord *client,
const char data[],
size_t size,
enum discord_gateway_events event);
/**
* @brief Provides control over Discord event's callback scheduler
* @see @ref discord_event_scheduler, @ref discord_gateway_events
*
* Allows the user to scan the preliminary raw JSON event payload, and control
* whether it should trigger callbacks
* @param client the client created_with discord_from_token()
* @param fn the function that will be executed
* @warning The user is responsible for providing their own locking mechanism
* to avoid race-condition on sensitive data
*/
void discord_set_event_scheduler(struct discord *client,
discord_ev_scheduler callback);
/**
* @brief Subscribe to Discord Events
*
* @param client the client created with discord_from_token()
* @param code the intents opcode, can be set as a bitmask operation
*/
void discord_add_intents(struct discord *client, uint64_t code);
/**
* @brief Unsubscribe from Discord Events
*
* @param client the client created with discord_from_token()
* @param code the intents opcode, can be set as bitmask operation
* Ex: 1 << 0 | 1 << 1 | 1 << 4
*/
void discord_remove_intents(struct discord *client, uint64_t code);
/**
* @brief Set a mandatory prefix before commands
* @see discord_set_on_command()
*
* Example: If @a 'help' is a command and @a '!' prefix is set, the command
* will only be validated if @a '!help' is sent
* @param client the client created with discord_from_token()
* @param prefix the mandatory command prefix
*/
void discord_set_prefix(struct discord *client, const char prefix[]);
/**
* @brief Set command/callback pair
*
* The callback is triggered when a user types the assigned command in a
* chat visible to the client
* @param client the client created with discord_from_token()
* @param command the command to trigger the callback
* @param callback the callback to be triggered on event
* @note The command and any subjacent empty space is left out of
* the message content
*/
void discord_set_on_command(
struct discord *client,
const char *command,
void (*callback)(struct discord *client,
const struct discord_message *event));
/**
* @brief Set a variadic series of NULL terminated commands to a callback
*
* The callback is triggered when a user types one of the assigned commands in
* a chat visble to the client
* @param client the client created with discord_from_token()
* @param commands array of commands to trigger the callback
* @param amount amount of commands provided
* @param callback the callback to be triggered on event
* @note The command and any subjacent empty space is left out of
* the message content
*/
void discord_set_on_commands(
struct discord *client,
const char *commands[],
int amount,
void (*callback)(struct discord *client,
const struct discord_message *event));
/**
* @brief Triggers when idle
* @note This is a Concord custom event
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_idle(struct discord *client,
void (*callback)(struct discord *client));
/**
* @brief Triggers once per event-loop cycle
* @note This is a Concord custom event
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_cycle(struct discord *client,
void (*callback)(struct discord *client));
/**
* @brief Triggers when the client session is ready
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_ready(struct discord *client,
void (*callback)(struct discord *client,
const struct discord_ready *event));
/**
* @brief Triggers when an application command permission is updated
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_application_command_permissions_update(
struct discord *client,
void (*callback)(
struct discord *client,
const struct discord_application_command_permissions *event));
/**
* @brief Triggers when an auto moderation rule is created
* @note This implicitly sets
* @ref DISCORD_GATEWAY_AUTO_MODERATION_CONFIGURATION intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_auto_moderation_rule_create(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_auto_moderation_rule *event));
/**
* @brief Triggers when an auto moderation rule is updated
* @note This implicitly sets
* @ref DISCORD_GATEWAY_AUTO_MODERATION_CONFIGURATION intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_auto_moderation_rule_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_auto_moderation_rule *event));
/**
* @brief Triggers when an auto moderation rule is deleted
* @note This implicitly sets
* @ref DISCORD_GATEWAY_AUTO_MODERATION_CONFIGURATION intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_auto_moderation_rule_delete(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_auto_moderation_rule *event));
/**
* @brief Triggers when an auto moderation rule is triggered and an execution
* is executed (e.g a message was blocked)
* @note This implicitly sets @ref DISCORD_GATEWAY_AUTO_MODERATION_EXECUTION
* intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_auto_moderation_action_execution(
struct discord *client,
void (*callback)(
struct discord *client,
const struct discord_auto_moderation_action_execution *event));
/**
* @brief Triggers when a channel is created
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILDS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_channel_create(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_channel *event));
/**
* @brief Triggers when a channel is updated
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILDS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_channel_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_channel *event));
/**
* @brief Triggers when a channel is deleted
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILDS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_channel_delete(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_channel *event));
/**
* @brief Triggers when a channel pin is updated
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILDS and
* @ref DISCORD_GATEWAY_DIRECT_MESSAGES intents
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_channel_pins_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_channel_pins_update *event));
/**
* @brief Triggers when a thread is created
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILDS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_thread_create(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_channel *event));
/**
* @brief Triggers when a thread is updated
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILDS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_thread_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_channel *event));
/**
* @brief Triggers when a thread is deleted
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILDS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_thread_delete(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_channel *event));
/**
* @brief Triggers when the current user gains access to a channel
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILDS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_thread_list_sync(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_thread_list_sync *event));
/**
* @brief Triggers when a thread the bot is in gets updated
* @note For bots, this event largely is just a signal that you are a member of
* the thread
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILDS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_thread_member_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_thread_member *event));
/**
* @brief Triggers when someone is added or removed from a thread
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILDS and
* @ref DISCORD_GATEWAY_GUILD_MEMBERS intents
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_thread_members_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_thread_members_update *event));
/**
* @brief Triggers when a guild is created
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILDS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_create(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_guild *event));
/**
* @brief Triggers when a guild is updated
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILDS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_guild *event));
/**
* @brief Triggers when a guild is deleted
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILDS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_delete(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_guild *event));
/**
* @brief Triggers when a user is banned from a guild
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_BANS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_ban_add(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_guild_ban_add *event));
/**
* @brief Triggers when a user is unbanned from a guild
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_BANS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_ban_remove(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_guild_ban_remove *event));
/**
* @brief Triggers when a guild emojis are updated
* @note This implicitly sets
* @ref DISCORD_GATEWAY_GUILD_EMOJIS_AND_STICKERS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_emojis_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_guild_emojis_update *event));
/**
* @brief Triggers when a guild stickers are updated
* @note This implicitly sets
* @ref DISCORD_GATEWAY_GUILD_EMOJIS_AND_STICKERS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_stickers_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_guild_stickers_update *event));
/**
* @brief Triggers when a guild integrations are updated
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_INTEGRATIONS
* intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_integrations_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_guild_integrations_update *event));
/**
* @brief Triggers when a guild member is added
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_MEMBERS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_member_add(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_guild_member *event));
/**
* @brief Triggers when a guild member is updated
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_MEMBERS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_member_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_guild_member_update *event));
/**
* @brief Triggers when a guild member is removed
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_MEMBERS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_member_remove(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_guild_member_remove *event));
/**
* @brief Triggers in response to discord_request_guild_members()
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_members_chunk(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_guild_members_chunk *event));
/**
* @brief Triggers when a guild role is created
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILDS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_role_create(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_guild_role_create *event));
/**
* @brief Triggers when a guild role is updated
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILDS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_role_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_guild_role_update *event));
/**
* @brief Triggers when a guild role is deleted
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILDS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_role_delete(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_guild_role_delete *event));
/**
* @brief Triggers when a guild scheduled event is created
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_SCHEDULED_EVENTS
* intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_scheduled_event_create(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_guild_scheduled_event *event));
/**
* @brief Triggers when a guild scheduled event is updated
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_SCHEDULED_EVENTS
* intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_scheduled_event_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_guild_scheduled_event *event));
/**
* @brief Triggers when a guild scheduled event is deleted
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_SCHEDULED_EVENTS
* intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_scheduled_event_delete(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_guild_scheduled_event *event));
/**
* @brief Triggers when a user subscribes to a guild scheduled event
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_SCHEDULED_EVENTS
* intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_scheduled_event_user_add(
struct discord *client,
void (*callback)(
struct discord *client,
const struct discord_guild_scheduled_event_user_add *event));
/**
* @brief Triggers when a user unsubscribes from a guild scheduled event
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_SCHEDULED_EVENTS
* intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_guild_scheduled_event_user_remove(
struct discord *client,
void (*callback)(
struct discord *client,
const struct discord_guild_scheduled_event_user_remove *event));
/**
* @brief Triggers when a guild integration is created
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_INTEGRATIONS
* intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_integration_create(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_integration *event));
/**
* @brief Triggers when a guild integration is updated
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_INTEGRATIONS
* intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_integration_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_integration *event));
/**
* @brief Triggers when a guild integration is deleted
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_INTEGRATIONS
* intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_integration_delete(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_integration_delete *event));
/**
* @brief Triggers when user has used an interaction, such as an application
* command
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_interaction_create(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_interaction *event));
/**
* @brief Triggers when an invite to a channel has been created
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_INVITES intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_invite_create(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_invite_create *event));
/**
* @brief Triggers when an invite to a channel has been deleted
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_INVITES intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_invite_delete(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_invite_delete *event));
/**
* @brief Triggers when a message is created
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_MESSAGES and
* @ref DISCORD_GATEWAY_DIRECT_MESSAGES intents
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_message_create(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_message *event));
/**
* @brief Triggers when a message is updated
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_MESSAGES and
* @ref DISCORD_GATEWAY_DIRECT_MESSAGES intents
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_message_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_message *event));
/**
* @brief Triggers when a message is deleted
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_MESSAGES and
* @ref DISCORD_GATEWAY_DIRECT_MESSAGES intents
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_message_delete(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_message_delete *event));
/**
* @brief Triggers when messages are deleted in bulk
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_MESSAGES
* intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_message_delete_bulk(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_message_delete_bulk *event));
/**
* @brief Triggers when a message reaction is added
* @note This implicitly sets
* @ref DISCORD_GATEWAY_GUILD_MESSAGE_REACTIONS and
* @ref DISCORD_GATEWAY_DIRECT_MESSAGE_REACTIONS intents
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_message_reaction_add(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_message_reaction_add *event));
/**
* @brief Triggers when a message reaction is removed
* @note This implicitly sets
* @ref DISCORD_GATEWAY_GUILD_MESSAGE_REACTIONS and
* @ref DISCORD_GATEWAY_DIRECT_MESSAGE_REACTIONS intents
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_message_reaction_remove(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_message_reaction_remove *event));
/**
* @brief Triggers when all message reactions are removed
* @note This implicitly sets
* @ref DISCORD_GATEWAY_GUILD_MESSAGE_REACTIONS and
* @ref DISCORD_GATEWAY_DIRECT_MESSAGE_REACTIONS intents
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_message_reaction_remove_all(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_message_reaction_remove_all *event));
/** @brief Triggers when all instances of a particular reaction from some
* message is removed */
/**
* @brief Triggers when all instances of a particular reaction is removed from
* a message
* @note This implicitly sets
* @ref DISCORD_GATEWAY_GUILD_MESSAGE_REACTIONS and
* @ref DISCORD_GATEWAY_DIRECT_MESSAGE_REACTIONS intents
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_message_reaction_remove_emoji(
struct discord *client,
void (*callback)(
struct discord *client,
const struct discord_message_reaction_remove_emoji *event));
/**
* @brief Triggers when user presence is updated
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_PRESENCES intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_presence_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_presence_update *event));
/**
* @brief Triggers when a stage instance is created
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILDS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_stage_instance_create(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_stage_instance *event));
/**
* @brief Triggers when a stage instance is updated
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILDS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_stage_instance_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_stage_instance *event));
/**
* @brief Triggers when a stage instance is deleted
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILDS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_stage_instance_delete(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_stage_instance *event));
/**
* @brief Triggers when user starts typing in a channel
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_MESSAGE_TYPING and
* @ref DISCORD_GATEWAY_DIRECT_MESSAGE_TYPING intents
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_typing_start(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_typing_start *event));
/**
* @brief Triggers when properties about a user changed
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_user_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_user *event));
/**
* @brief Triggers when a voice state is updated
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_voice_state_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_voice_state *event));
/**
* @brief Triggers when voice server is updated
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_voice_server_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_voice_server_update *event));
/**
* @brief Triggers when guild channel has been created, updated or deleted
* @note This implicitly sets @ref DISCORD_GATEWAY_GUILD_WEBHOOKS intent
*
* @param client the client created with discord_from_token()
* @param callback the callback to be triggered on event
*/
void discord_set_on_webhooks_update(
struct discord *client,
void (*callback)(struct discord *client,
const struct discord_webhooks_update *event));
/** @} DiscordEvents */
#endif /* DISCORD_EVENTS_H */
File diff suppressed because it is too large Load Diff
+82
View File
@@ -0,0 +1,82 @@
/**
* @file discord-request.h
* @ingroup DiscordInternalREST
* @author Cogmasters
* @brief Generic macros for initializing a @ref discord_attributes
*/
#ifndef DISCORD_REQUEST_H
#define DISCORD_REQUEST_H
/* helper typedefs for casting */
typedef void (*cast_done_typed)(struct discord *,
struct discord_response *,
const void *);
typedef struct reflectc_wrap *(*cast_init)(struct reflectc *,
void *,
struct reflectc_wrap *);
/* helper typedef for getting sizeof of `struct discord_ret` common fields */
typedef struct {
DISCORD_RET_DEFAULT_FIELDS;
} discord_ret_default_fields;
#define _RET_COPY_TYPED(_dest, _src) \
do { \
memcpy(&(_dest), &(_src), sizeof(discord_ret_default_fields)); \
(_dest).has_type = true; \
(_dest).done.typed = (cast_done_typed)(_src).done; \
(_dest).sync = (_src).sync; \
} while (0)
#define _RET_COPY_TYPELESS(_dest, _src) \
do { \
memcpy(&(_dest), &(_src), sizeof(discord_ret_default_fields)); \
(_dest).has_type = false; \
(_dest).done.typeless = (_src).done; \
(_dest).sync = (void *)(_src).sync; \
} while (0)
/**
* @brief Helper for setting attributes for a specs-generated return struct
*
* @param[out] _attr @ref discord_attributes handler to be initialized
* @param[in] _type datatype of the struct
* @param[in] _ret dispatch attributes
* @param[in] _reason reason for request (if available)
*/
#define DISCORD_ATTR_INIT(_attr, _type, _ret, _reason) \
do { \
(_attr).response.size = sizeof(struct _type); \
(_attr).response.init = (cast_init)reflectc_from_##_type; \
(_attr).reason = _reason; \
if (_ret) _RET_COPY_TYPED((_attr).dispatch, *(_ret)); \
} while (0)
/**
* @brief Helper for setting attributes for attributes that don't have a
* response object
*
* @param[out] _attr @ref discord_attributes handler to be initialized
* @param[in] _ret dispatch attributes
* @param[in] _reason reason for request (if available)
*/
#define DISCORD_ATTR_BLANK_INIT(_attr, _ret, _reason) \
do { \
(_attr).reason = _reason; \
if (_ret) _RET_COPY_TYPELESS((_attr).dispatch, *(_ret)); \
} while (0)
/**
* @brief Helper for initializing attachments ids
*
* @param[in,out] attchs a @ref discord_attachments to have its IDs initialized
*/
#define DISCORD_ATTACHMENTS_IDS_INIT(_attchs) \
do { \
for (int i = 0; i < (_attchs)->size; ++i) { \
(_attchs)->array[i].id = (u64snowflake)i; \
} \
} while (0)
#endif /* DISCORD_REQUEST_H */
+192
View File
@@ -0,0 +1,192 @@
/**
* @file discord-response.h
* @author Cogmasters
* @brief Generic macros for initializing a @ref discord_response and return
* handles
*/
#ifndef DISCORD_RESPONSE_H
#define DISCORD_RESPONSE_H
/** @brief The response for the completed request */
struct discord_response {
/** user arbitrary data provided at @ref discord_ret */
void *data;
/** kept concord's parameter provided at @ref discord_ret */
const void *keep;
/** request completion status @see @ref ConcordError */
CCORDcode code;
/** the JSON object in case of a @ref CCORD_OK, or the JSON error
* object in case of a @ref CCORD_DISCORD_JSON_CODE
*
* @see https://discord.com/developers/docs/reference#error-messages */
struct ccord_szbuf_readonly json;
};
/******************************************************************************
* Templates for generating type-safe return handles for async requests
******************************************************************************/
/**
* @brief Macro containing common fields for `struct discord_ret*` datatypes
* @note this exists for alignment purposes
*/
#define DISCORD_RET_DEFAULT_FIELDS \
/** user arbitrary data to be passed to `done` or `fail` callbacks */ \
void *data; \
/** cleanup method to be called for `data`, once its no longer \
being referenced */ \
void (*cleanup)(struct discord * client, void *data); \
/** Concord callback parameter the client wish to keep reference */ \
const void *keep; \
/** if `true` then request will be prioritized over already enqueued \
requests */ \
bool high_priority; \
/** optional callback to be executed on a failed request */ \
void (*fail)(struct discord * client, struct discord_response * resp)
#define DISCORD_RETURN(_type) \
/** @brief Request's return context */ \
struct discord_ret_##_type { \
DISCORD_RET_DEFAULT_FIELDS; \
/** optional callback to be executed on a successful request */ \
void (*done)(struct discord * client, \
struct discord_response *resp, \
const struct discord_##_type *ret); \
/** if an address is provided, then request will block the thread and \
perform on-spot. \
On success the response object will be written to the address, \
unless enabled with @ref DISCORD_SYNC_FLAG */ \
struct discord_##_type *sync; \
}
/** @brief Request's return context */
struct discord_ret {
DISCORD_RET_DEFAULT_FIELDS;
/** optional callback to be executed on a successful request */
void (*done)(struct discord *client, struct discord_response *resp);
/** if `true`, request will block the thread and perform on-spot */
bool sync;
};
/** @brief flag for enabling `sync` mode without expecting a datatype return */
#define DISCORD_SYNC_FLAG ((void *)-1)
/** @addtogroup DiscordAPIOAuth2
* @{ */
DISCORD_RETURN(application);
DISCORD_RETURN(auth_response);
/** @} DiscordAPIOAuth2 */
/** @addtogroup DiscordAPIAuditLog
* @{ */
DISCORD_RETURN(audit_log);
/** @} DiscordAPIAuditLog */
/** @addtogroup DiscordAPIAutoModeration
* @{ */
DISCORD_RETURN(auto_moderation_rule);
DISCORD_RETURN(auto_moderation_rules);
/** @} DiscordAPIAutoModeration */
/** @addtogroup DiscordAPIChannel
* @{ */
DISCORD_RETURN(channel);
DISCORD_RETURN(channels);
DISCORD_RETURN(message);
DISCORD_RETURN(messages);
DISCORD_RETURN(followed_channel);
DISCORD_RETURN(thread_members);
DISCORD_RETURN(thread_response_body);
/** @} DiscordAPIChannel */
/** @addtogroup DiscordAPIEmoji
* @{ */
DISCORD_RETURN(emoji);
DISCORD_RETURN(emojis);
/** @} DiscordAPIEmoji */
/** @addtogroup DiscordAPIGuild
* @{ */
DISCORD_RETURN(guild);
DISCORD_RETURN(guilds);
DISCORD_RETURN(guild_preview);
DISCORD_RETURN(guild_member);
DISCORD_RETURN(guild_members);
DISCORD_RETURN(guild_widget);
DISCORD_RETURN(guild_widget_settings);
DISCORD_RETURN(ban);
DISCORD_RETURN(bans);
DISCORD_RETURN(role);
DISCORD_RETURN(roles);
DISCORD_RETURN(welcome_screen);
DISCORD_RETURN(integrations);
DISCORD_RETURN(prune_count);
/** @} DiscordAPIGuild */
/** @addtogroup DiscordAPIGuildScheduledEvent
* @{ */
DISCORD_RETURN(guild_scheduled_event);
DISCORD_RETURN(guild_scheduled_events);
DISCORD_RETURN(guild_scheduled_event_users);
/** @} DiscordAPIGuildScheduledEvent */
/** @addtogroup DiscordAPIGuildTemplate
* @{ */
DISCORD_RETURN(guild_template);
DISCORD_RETURN(guild_templates);
/** @} DiscordAPIGuildTemplate */
/** @addtogroup DiscordAPIInvite
* @{ */
DISCORD_RETURN(invite);
DISCORD_RETURN(invites);
/** @} DiscordAPIInvite */
/** @addtogroup DiscordAPIStageInstance
* @{ */
DISCORD_RETURN(stage_instance);
/** @} DiscordAPIStageInstance */
/** @addtogroup DiscordAPISticker
* @{ */
DISCORD_RETURN(sticker);
DISCORD_RETURN(stickers);
DISCORD_RETURN(list_nitro_sticker_packs);
/** @} DiscordAPISticker */
/** @addtogroup DiscordAPIUser
* @{ */
DISCORD_RETURN(user);
DISCORD_RETURN(users);
DISCORD_RETURN(connections);
/** @} DiscordAPIUser */
/** @addtogroup DiscordAPIVoice
* @{ */
DISCORD_RETURN(voice_regions);
/** @} DiscordAPIVoice */
/** @addtogroup DiscordAPIWebhook
* @{ */
DISCORD_RETURN(webhook);
DISCORD_RETURN(webhooks);
/** @} DiscordAPIWebhook */
/** @addtogroup DiscordAPIInteractionsApplicationCommand
* @ingroup DiscordAPIInteractions
* @{ */
DISCORD_RETURN(application_command);
DISCORD_RETURN(application_commands);
DISCORD_RETURN(application_command_permission);
DISCORD_RETURN(application_command_permissions);
DISCORD_RETURN(guild_application_command_permissions);
/** @} DiscordAPIInteractionsApplicationCommand */
/** @addtogroup DiscordAPIInteractionsReact
* @ingroup DiscordAPIInteractions
* @{ */
DISCORD_RETURN(interaction_response);
/** @} DiscordAPIInteractionsReact */
#endif /* DISCORD_RESPONSE_H */
+54
View File
@@ -0,0 +1,54 @@
/**
* @file discord-worker.h
* @author Cogmasters
* @brief Global threadpool
*/
#ifndef DISCORD_WORKER_H
#define DISCORD_WORKER_H
#include "concord-error.h"
/* forward declaration */
struct discord;
/**/
/** @defgroup DiscordInternalWorker Global threadpool
* @ingroup DiscordInternal
* @brief A global threadpool for worker-threads handling
* @{ */
/**
* @brief Initialize global threadpool and priority queue
*
* @param flags unused for now, but reserved for future use
* @CCORD_return
*/
CCORDcode discord_worker_global_init(long flags);
/** @brief Cleanup global threadpool and priority queue */
void discord_worker_global_cleanup(void);
/**
* @brief Run a callback from a worker thread
*
* @param client the client that will be using the worker thread
* @param callback user callback to be executed
* @param data user data to be passed to callback
* @CCORD_return
*/
CCORDcode discord_worker_add(struct discord *client,
void (*callback)(void *data),
void *data);
/**
* @brief Wait until worker-threads being used by `client` have been joined
*
* @param client the client currently using a worker thread
* @CCORD_return
*/
CCORDcode discord_worker_join(struct discord *client);
/** @} DiscordInternalWorker */
#endif /* DISCORD_WORKER_H */
+818
View File
@@ -0,0 +1,818 @@
/**
* @file discord.h
* @author Cogmasters
* @brief Public functions and datatypes
*
* These symbols are organized in a intuitive fashion to be easily
* matched to the official Discord API docs
* @see https://discord.com/developers/docs/intro
*/
#ifndef DISCORD_H
#define DISCORD_H
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
#include <inttypes.h>
#include <stdbool.h>
#include "concord-error.h"
#include "types.h"
#include "io_poller.h"
#define LOGMOD_HEADER
#include "logmod.h"
#ifndef DISCORD_VERSION
/**
* @brief The Discord API version to use
* @warning only change this if you know what you are doing!
*/
#define DISCORD_VERSION "10"
#endif
#define DISCORD_API_BASE_URL "https://discord.com/api/v" DISCORD_VERSION
#define DISCORD_GATEWAY_URL_SUFFIX "?v=" DISCORD_VERSION "&encoding=json"
/* forward declaration */
struct discord;
struct reflectc;
/**/
/**
* @brief Get the reflectc registry from a Discord client
*
* @param client the client created with discord_from_json() or discord_from_token()
* @return the reflectc registry used by the client
*/
struct reflectc *discord_get_registry(struct discord *client);
#include "discord_codecs.h"
#include "discord-response.h"
/** @defgroup DiscordClient Client */
/** @defgroup DiscordConstants Constants
* @brief Macros for constants defined by Discord
* @note macros assume the worst-case scenario for strings, where each
* character is 4 bytes long (UTF8)
* @{ */
/** @defgroup DiscordConstantsGeneral General lengths
* @brief Max length for general fields
* @{ */
#define DISCORD_MAX_NAME_LEN 4 * 100 + 1
#define DISCORD_MAX_TOPIC_LEN 4 * 1024 + 1
#define DISCORD_MAX_DESCRIPTION_LEN 4 * 2048 + 1
#define DISCORD_MAX_USERNAME_LEN 4 * 32 + 1
#define DISCORD_MAX_DISCRIMINATOR_LEN 4 + 1
#define DISCORD_MAX_REASON_LEN 4 * 512 + 1
#define DISCORD_MAX_MESSAGE_LEN 4 * 2000 + 1
#define DISCORD_MAX_PAYLOAD_LEN 4 * 4096 + 1
/** @} DiscordConstantsGeneral */
/** @defgroup DiscordConstantsEmbed Embed lengths
* @brief Max length for embed fields
* @{ */
#define DISCORD_EMBED_TITLE_LEN 4 * 256 + 1
#define DISCORD_EMBED_DESCRIPTION_LEN 4 * 4096 + 1
#define DISCORD_EMBED_MAX_FIELDS 25
#define DISCORD_EMBED_FIELD_NAME_LEN 4 * 256 + 1
#define DISCORD_EMBED_FIELD_VALUE_LEN 4 * 1024 + 1
#define DISCORD_EMBED_FOOTER_TEXT_LEN 4 * 2048 + 1
#define DISCORD_EMBED_AUTHOR_NAME_LEN 4 * 256 + 1
/** @} DiscordConstantsEmbed */
/** @defgroup DiscordConstantsWebhook Webhook lengths
* @brief Max length for embed fields
* @{ */
#define DISCORD_WEBHOOK_NAME_LEN 4 * 80 + 1
/** @} DiscordConstantsWebhook */
/** @} DiscordConstants */
/** @addtogroup ConcordError
* @{ */
/* XXX: As new values are added, discord_strerror() and
* discord_code_as_string() should be updated accordingly! */
/** @defgroup DiscordError Discord error codes
* @brief Error codes triggered from Discord
* @{ */
/** Alias for @ref CCORD_OK */
#define CCORD_DISCORD_OK CCORD_OK
/** action is pending (ex: request has been enqueued and will be performed
* later) */
#define CCORD_PENDING 1
/** received a JSON error message */
#define CCORD_DISCORD_JSON_CODE 100
/** bad authentication token */
#define CCORD_DISCORD_BAD_AUTH 101
/** being ratelimited */
#define CCORD_DISCORD_RATELIMIT 102
/** couldn't establish connection to Discord */
#define CCORD_DISCORD_CONNECTION 103
/**
* @brief Return the value of CCORDcode as a string
*
* @param code the CCORDcode value
* @return the enum value as a string
*/
const char *discord_code_as_string(CCORDcode code);
/**
* @brief Return the meaning of CCORDcode
*
* @param code the CCORDcode value
* @param client @note unused parameter
* @return a string containing the code meaning
*/
const char *discord_strerror(CCORDcode code, struct discord *client);
/** @} DiscordError */
/** @} ConcordError */
/** @defgroup DiscordAPI API
* @brief The Discord public API supported by Concord
* @{ */
#include "audit_log.h"
#include "auto_moderation.h"
#include "invite.h"
#include "channel.h"
#include "emoji.h"
#include "guild.h"
#include "guild_scheduled_event.h"
#include "guild_template.h"
#include "stage_instance.h"
#include "sticker.h"
#include "user.h"
#include "voice.h"
#include "webhook.h"
#include "gateway.h"
#include "oauth2.h"
/** @defgroup DiscordAPIInteractions Interactions
* @brief Interactions public API supported by Concord
* @{ */
#include "application_command.h"
#include "interaction.h"
/** @} DiscordAPIInteractions */
/** @} DiscordAPI */
/** @addtogroup DiscordClient
* @brief Client functions and datatypes
* @{ */
/** @struct discord */
#include "discord-cache.h"
#include "discord-events.h"
/**
* @brief Claim ownership of a resource provided by Concord
* @see discord_unclaim()
*
* @param client the client initialized with discord_from_token()
* @param data a resource provided by Concord
* @return pointer to `data` (for one-liners)
*/
#define discord_claim(client, data) (__discord_claim(client, data), data)
void __discord_claim(struct discord *client, const void *data);
/**
* @brief Unclaim ownership of a resource provided by Concord
* @note this will make the resource eligible for cleanup, so this should
* only be called when you no longer plan to use it
* @see discord_claim()
*
* @param client the client initialized with discord_from_token()
* @param data a resource provided by Concord, that has been
* previously claimed with discord_claim()
*/
void discord_unclaim(struct discord *client, const void *data);
/** @deprecated since v3.0.0, keep backwards compatibility */
#define ccord_global_init()
/** @deprecated since v3.0.0, keep backwards compatibility */
#define ccord_global_cleanup()
/**
* @brief Gracefully notify all Discord connections for shutting down
*
* @note this function will not wait before returning, and will
* return immediately. The shutdown process will be handled
* in the background.
*/
void discord_shutdown_all(void);
/**
* @brief Check if all Discord connections shutting down is in progress
*
* @return true if all shutdown is in progress, false otherwise
*/
bool discord_shutdown_all_ongoing(void);
/**
* @brief Backwards compatible alias for discord_shutdown_all()
* @deprecated since v3.0.0
*/
#define ccord_shutdown_async() discord_shutdown_all()
/**
* @brief Creates a Discord Client handle from a token
* @see discord_get_logmod() to configure logging behavior
*
* @param token the bot token
* @return the newly created Discord Client handle
*/
struct discord *discord_from_token(const char token[]);
/**
* @brief Creates a Discord Client handle from a `config.json` file
* @see discord_get_logmod() to configure logging behavior
*
* @param config_file the `config.json` file name
* @return the newly created Discord Client handle
*/
struct discord *discord_from_json(const char config_file[]);
/**
* @brief The Discord configuration handler
*
* This struct is used to store the Discord client configuration
*/
struct discord_config {
/** the bot token */
char *token;
/**
* optional override for the REST API base URL
* (e.g. `"http://127.0.0.1:8080"` for a local test server)
* @note when `NULL` (the default) @ref DISCORD_API_BASE_URL is used
* @note only read during client initialization and copied by the
* User-Agent layer; the client does not take ownership and will
* never free this pointer
*/
char *base_url;
struct {
/** minimum logging level */
enum logmod_levels level;
/** silence terminal logging */
bool quiet;
/** enable color to terminal logging */
bool color;
/** overwrite existing files */
bool overwrite;
/* the trace log file */
FILE *trace;
/* the http log file */
FILE *http;
/* the ws log file */
FILE *ws;
struct {
size_t size;
char **ids;
} disable; /**< list of 'id' that should be ignored */
} log; /**< logging directives */
};
/**
* @brief Creates a Discord Client handle from a
* @ref discord_config structure
* @see discord_get_logmod() to configure logging behavior
*
* @param config the @ref discord_config structure
* @return the newly created Discord Client handle
*/
struct discord *discord_from_config(const struct discord_config *config);
/**
* @brief Backwards compatible alias for discord_from_token()
* @deprecated since v3.0.0
*/
#define discord_init discord_from_token
/**
* @brief Backwards compatible alias for discord_from_json()
* @deprecated since v3.0.0
*/
#define discord_config_init discord_from_json
/**
* @brief Get the contents from the config file field
* @note your bot **MUST** have been initialized with discord_from_json()
*
* @code{.c}
* // Assume the following custom config.json field to be extracted
* // "field": { "foo": "a string", "bar": 1234 }
*
* ...
* struct ccord_szbuf_readonly value;
* char foo[128];
* long bar;
*
* // field.foo
* value = discord_config_get_field(client, (char *[2]){ "field", "foo" }, 2);
* snprintf(foo, sizeof(foo), "%.*s", (int)value.size, value.start);
* // field.bar
* value = discord_config_get_field(client, (char *[2]){ "field", "bar" }, 2);
* bar = strtol(value.start, NULL, 10);
*
* printf("%s %ld", foo, bar); // "a string" 1234
* @endcode
*
* @param client the client created with discord_from_json()
* @param path the JSON key path
* @param depth the path depth
* @return a read-only sized buffer containing the field's contents
*/
struct ccord_szbuf_readonly discord_config_get_field(struct discord *client,
char *const path[],
unsigned depth);
/**
* @brief Clone a discord client
*
* Should be called before entering a thread, to ensure each thread
* has its own client instance with unique buffers, url and headers
* @param orig the original client created with discord_from_token()
* @return the client clone
*/
struct discord *discord_clone(const struct discord *orig);
/**
* @brief Free a Discord Client handle
*
* @param client the client created with discord_from_token()
*/
void discord_cleanup(struct discord *client);
/**
* @brief Get the client's cached user
*
* @param client the client created with discord_from_token()
* @warning the returned structure should NOT be modified
*/
const struct discord_user *discord_get_self(struct discord *client);
/**
* @brief Start a connection to the Discord Gateway
*
* @param client the client created with discord_from_token()
* @CCORD_return
*/
CCORDcode discord_run(struct discord *client);
/**
* @brief Gracefully shutdown an ongoing Discord connection
*
* @param client the client created with discord_from_token()
*/
void discord_shutdown(struct discord *client);
/**
* @brief Gracefully reconnects an ongoing Discord connection
*
* @param client the client created with discord_from_token()
* @param resume true to attempt to resume to previous session,
* false restart a fresh session
*/
void discord_reconnect(struct discord *client, bool resume);
/**
* @brief Store user arbitrary data that can be retrieved by discord_get_data()
*
* @param client the client created with discord_from_token()
* @param data user arbitrary data
* @return pointer to user data
* @warning the user should provide their own locking mechanism to protect
* its data from race conditions
*/
void *discord_set_data(struct discord *client, void *data);
/**
* @brief Receive user arbitrary data stored with discord_set_data()
*
* @param client the client created with discord_from_token()
* @return pointer to user data
* @warning the user should provide their own locking mechanism to protect
* its data from race conditions
*/
void *discord_get_data(struct discord *client);
/**
* @brief Get the client WebSockets ping
* @note Only works after a connection has been established via
* discord_run()
*
* @param client the client created with discord_from_token()
* @return the ping in milliseconds
*/
int discord_get_ping(struct discord *client);
/**
* @brief Get the current timestamp (in milliseconds)
*
* @param client the client created with discord_from_token()
* @return the timestamp in milliseconds
*/
uint64_t discord_timestamp(struct discord *client);
/**
* @brief Get the current timestamp (in microseconds)
*
* @param client the client created with discord_from_token()
* @return the timestamp in microseconds
*/
uint64_t discord_timestamp_us(struct discord *client);
/**
* @brief Retrieve client's logging module for configuration purposes
* @see logmod.h
*
* @param client the client created with discord_from_token()
* @return the client's logging manager
*/
struct logmod *discord_get_logmod(struct discord *client);
/**
* @brief get the io_poller used by the discord client
*
* @param client the client created with discord_from_token()
* @return struct io_poller*
*/
struct io_poller *discord_get_io_poller(struct discord *client);
/** @addtogroup DiscordDataWrap Data Wrap
* @brief Helpers for wrapping Discord data types for easier management
* @{ */
#define __CAT(_a, _b) _a##_b
#define _CAT(_a, _b) __CAT(_a, _b)
#define _EXPECT_CONTAINER__struct
#define _EXPECT_CONTAINER__union
#define _DISCORD_SYMBOL_WITHOUT_CONTAINER(_symbol) \
_CAT(_EXPECT_CONTAINER__, _symbol)
/**
* @brief The Discord data wrap structure
*
* This struct is used to wrap Discord data types for easier management
*/
#define discord_data_wrap reflectc_wrap
/**
* @brief Wrap a Discord data type into a reflectc_wrap structure
*
* @param _symbol the Discord data type symbol (with struct or union)
* @param _client the client created with discord_from_token()
* @param _data the Discord data type to be wrapped
* @return the @ref discord_data_wrap Discord data type
*/
#define discord_data_wrap_from(_symbol, _client, _data) \
_CAT(reflectc_from_, _DISCORD_SYMBOL_WITHOUT_CONTAINER(_symbol))( \
discord_get_registry((_client)), _data, NULL)
/**
* @brief Cleanup a Discord data type wrapped into a reflectc_wrap structure
*
* Releases everything discord_data_from_json() allocated for `_data`
* strings, nested objects, list arrays plus the reflect-c wrap and its
* registry entry. Every non-NULL pointer member is treated as owned: do
* not call this on hand-built structs pointing at literals or stack
* objects.
*
* @param _client the client created with discord_from_token()
* @param _data the Discord data type to be cleaned up
*/
#define discord_data_cleanup(_client, _data) \
discord_data_free(discord_get_registry((_client)), (_data))
/** Function backing @ref discord_data_cleanup; see its contract */
void discord_data_free(struct reflectc *registry, void *data);
/**
* @brief Drop the wrap cached for `_data` without touching its contents
*
* discord_data_to_json() caches a reflect-c wrap keyed by `_data`'s
* address; release it with this once done encoding a caller-owned
* struct (e.g. stack-built params). Without it the stale registry entry
* would be wrongly reused by a future object at the same address.
* Decoded structs don't need this discord_data_cleanup() releases
* both the data and the wrap.
*
* @param _client the client created with discord_from_token()
* @param _data the encoded Discord data type to drop the wrap for
*/
#define discord_data_unwrap(_client, _data) \
discord_data_release(discord_get_registry((_client)), (_data))
/** Function backing @ref discord_data_unwrap */
void discord_data_release(struct reflectc *registry, void *data);
/** @addtogroup DiscordDataWrapJSON JSON Conversion
* @brief Helpers for converting Discord data types to/from JSON
* @{ */
/* forward declaration */
struct jsmnf_pair;
struct jsonb;
/**
* @brief Transform a wrapped Discord data type into a JSON string
*
* @param member the wrapped Discord data type
* @param p_buf pointer to the JSON buffer
* @param p_bufsize pointer to the JSON buffer size
* @CCORD_return
*/
CCORDcode discord_data_wrap_to_json(const struct discord_data_wrap *member,
char *p_buf[],
size_t *p_bufsize);
/**
* @brief Transform a Discord data type into a JSON string
*
* @param _symbol the Discord data type symbol (with struct or union)
* @param _client the client created with discord_from_token()
* @param _data the Discord data type to be transformed
* @param _p_buf pointer to the JSON buffer
* @param _p_bufsize pointer to the JSON buffer size
* @CCORD_return
*/
#define discord_data_to_json(_symbol, _client, _data, _p_buf, _p_bufsize) \
discord_data_wrap_to_json( \
discord_data_wrap_from(_symbol, _client, _data), _p_buf, _p_bufsize)
/**
* @brief Transform a wrapped Discord data type into a jsonb handle
*
* @param jb the jsonb handle
* @param member the wrapped Discord data type
* @param p_buf pointer to the JSON buffer
* @param p_bufsize pointer to the JSON buffer size
* @CCORD_return
*/
CCORDcode discord_data_wrap_to_jsonb(struct jsonb *jb,
const struct discord_data_wrap *member,
char *p_buf[],
size_t *p_bufsize);
/**
* @brief Transform a Discord data type into a jsonb handle
*
* @param _symbol the Discord data type symbol (with struct or union)
* @param _client the client created with discord_from_token()
* @param _jb the jsonb handle
* @param _data the Discord data type to be transformed
* @param _p_buf pointer to the JSON buffer
* @param _p_bufsize pointer to the JSON buffer size
* @CCORD_return
*/
#define discord_data_to_jsonb(_symbol, _client, _jb, _data, _p_buf, \
_p_bufsize) \
discord_data_wrap_to_jsonb( \
_jb, discord_data_wrap_from(_symbol, _client, _data), _p_buf, \
_p_bufsize)
/**
* @brief Parse a JSON string and fill a wrapped Discord data type
*
* @param json the JSON string
* @param len length of @ref json
* @param root the root wrapped Discord data type
* @CCORD_return
*/
CCORDcode discord_data_wrap_from_json(const char *json,
size_t len,
struct discord_data_wrap *root);
/**
* @brief Parse a JSON string and fill a Discord data type
*
* @param _symbol the Discord data type symbol (with struct or union)
* @param _client the client created with discord_from_token()
* @param _json the JSON string
* @param _len length of @ref json
* @param _data the Discord data type to be filled
* @CCORD_return
*/
#define discord_data_from_json(_symbol, _client, _json, _len, _data) \
discord_data_wrap_from_json( \
_json, _len, discord_data_wrap_from(_symbol, _client, _data))
/**
* @brief Parse a jsmnf_pair and fill a wrapped Discord data type
*
* @param p the jsmnf_pair
* @param json the JSON string
* @param length length of @ref json
* @param member the wrapped Discord data type
* @CCORD_return
*/
CCORDcode discord_data_wrap_from_jsmnf(const struct jsmnf_pair *p,
const char *json,
size_t length,
const struct discord_data_wrap *member);
/**
* @brief Parse a jsmnf_pair and fill a Discord data type
*
* @param _client the client created with discord_from_token()
* @param _p the jsmnf_pair
* @param _json the JSON string
* @param _length length of @ref json
* @param _type the Discord data type symbol (without struct/union)
* @param _data the Discord data type to be filled
* @CCORD_return
*/
#define discord_data_from_jsmnf(_client, _p, _json, _length, _type, _data) \
discord_data_wrap_from_jsmnf( \
_p, _json, _length, discord_data_wrap_from(_type, _client, _data))
/** @} DiscordDataWrapJSON */
/** @} DiscordDataWrap */
/** @addtogroup DiscordTimer Timer
* @brief Schedule callbacks to be called in the future
* @{ */
/* forward declaration */
struct discord_timer;
/**/
/** @brief callback to be used with struct discord_timer */
typedef void (*discord_ev_timer)(struct discord *client,
struct discord_timer *ev);
/** @brief flags used to change behaviour of timer */
enum discord_timer_flags {
/** use milliseconds for interval and start_time */
DISCORD_TIMER_MILLISECONDS = 0,
/** use microseconds for interval and start_time */
DISCORD_TIMER_MICROSECONDS = 1 << 0,
/** whether or not timer is marked for deletion */
DISCORD_TIMER_DELETE = 1 << 1,
/** automatically delete a timer once its repeat counter runs out */
DISCORD_TIMER_DELETE_AUTO = 1 << 2,
/** timer has been canceled. user should cleanup only */
DISCORD_TIMER_CANCELED = 1 << 3,
/** flag is set when on_tick callback has been called */
DISCORD_TIMER_TICK = 1 << 4,
/** used in discord_timer_ctl to get the timer's data */
DISCORD_TIMER_GET = 1 << 5,
/** timer should run using a fixed interval based on start time */
DISCORD_TIMER_INTERVAL_FIXED = 1 << 6,
};
/** @brief struct used for modifying, and getting info about a timer */
struct discord_timer {
/** the identifier used for the timer. 0 creates a new timer */
unsigned id;
/** the flags used to manipulate the timer */
enum discord_timer_flags flags;
/** (nullable) the callback that should be called when timer triggers */
discord_ev_timer on_tick;
/** (nullable) the callback for status updates timer->flags
* will have: DISCORD_TIMER_CANCELED, and DISCORD_TIMER_DELETE */
discord_ev_timer on_status_changed;
/** user data */
void *data;
/** delay before timer should start */
int64_t delay;
/** interval that the timer should repeat at. must be >= 0 */
int64_t interval;
/** how many times a timer should repeat (-1 == infinity) */
int64_t repeat;
};
/**
* @brief modifies or creates a timer
*
* @param client the client created with discord_from_token()
* @param timer the timer that should be modified
* @return the id of the timer
*/
unsigned discord_timer_ctl(struct discord *client,
struct discord_timer *timer);
/**
* @brief creates a one shot timer that automatically
* deletes itself upon completion
*
* @param client the client created with discord_from_token()
* @param on_tick_cb (nullable) the callback that should be called when timer
* triggers
* @param on_status_changed_cb (nullable) the callback for status updates
* timer->flags will have: DISCORD_TIMER_CANCELED, and DISCORD_TIMER_DELETE
* @param data user data
* @param delay delay before timer should start in milliseconds
* @return the id of the timer
*/
unsigned discord_timer(struct discord *client,
discord_ev_timer on_tick_cb,
discord_ev_timer on_status_changed_cb,
void *data,
int64_t delay);
/**
* @brief creates a repeating timer that automatically
* deletes itself upon completion
*
* @param client the client created with discord_from_token()
* @param on_tick_cb (nullable) the callback that should be called when timer
* triggers
* @param on_status_changed_cb (nullable) the callback for status updates
* timer->flags will have: DISCORD_TIMER_CANCELED, and DISCORD_TIMER_DELETE
* @param data user data
* @param delay delay before timer should start in milliseconds
* @param interval interval between runs. (-1 == disable repeat)
* @param repeat repetitions (-1 == infinity)
* @return the id of the timer
*/
unsigned discord_timer_interval(struct discord *client,
discord_ev_timer on_tick_cb,
discord_ev_timer on_status_changed_cb,
void *data,
int64_t delay,
int64_t interval,
int64_t repeat);
/**
* @brief get the data associated with the timer
*
* @param client the client created with discord_from_token()
* @param id id of the timer
* @param timer where to copy the timer data to
* @return true on success
*/
bool discord_timer_get(struct discord *client,
unsigned id,
struct discord_timer *timer);
/**
* @brief starts a timer
*
* @param client the client created with discord_from_token()
* @param id id of the timer
* @return true on success
*/
bool discord_timer_start(struct discord *client, unsigned id);
/**
* @brief stops a timer
*
* @param client the client created with discord_from_token()
* @param id id of the timer
* @return true on success
*/
bool discord_timer_stop(struct discord *client, unsigned id);
/**
* @brief cancels a timer,
* this will delete the timer if DISCORD_TIMER_DELETE_AUTO is enabled
*
* @param client the client created with discord_from_token()
* @param id id of the timer
* @return true on success
*/
bool discord_timer_cancel(struct discord *client, unsigned id);
/**
* @brief deletes a timer
*
* @param client the client created with discord_from_token()
* @param id id of the timer
* @return true on success
*/
bool discord_timer_delete(struct discord *client, unsigned id);
/**
* @brief cancels, and deletes a timer
*
* @param client the client created with discord_from_token()
* @param id id of the timer
* @return true on success
*/
bool discord_timer_cancel_and_delete(struct discord *client, unsigned id);
/** @example timers.c
* Demonstrates the Timer API for callback scheduling */
/** @} DiscordTimer */
/** @} DiscordClient */
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* DISCORD_H */
File diff suppressed because it is too large Load Diff
+95
View File
@@ -0,0 +1,95 @@
/**
* @file emoji.h
* @author Cogmasters
* @brief Emoji public functions and datatypes
*/
#ifndef DISCORD_EMOJI_H
#define DISCORD_EMOJI_H
/** @defgroup DiscordAPIEmoji Emoji
* @ingroup DiscordAPI
* @brief Emoji's public API supported by Concord
* @{ */
/**
* @brief Get emojis of a given guild
*
* @param client the client created with discord_from_token()
* @param guild_id guild to get emojis from
* @CCORD_ret_obj{ret,emojis}
* @CCORD_return
*/
CCORDcode discord_list_guild_emojis(struct discord *client,
u64snowflake guild_id,
struct discord_ret_emojis *ret);
/**
* @brief Get a specific emoji from a guild
*
* @param client the client created with discord_from_token()
* @param guild_id guild the emoji belongs to
* @param emoji_id the emoji to be fetched
* @CCORD_ret_obj{ret,emoji}
* @CCORD_return
*/
CCORDcode discord_get_guild_emoji(struct discord *client,
u64snowflake guild_id,
u64snowflake emoji_id,
struct discord_ret_emoji *ret);
/**
* @brief Create a new emoji for the guild
* @note Fires a `Guild Emojis Update` event
*
* @param client the client created with discord_from_token()
* @param guild_id guild to add the new emoji to
* @param params request parameters
* @CCORD_ret_obj{ret,emoji}
* @CCORD_return
*/
CCORDcode discord_create_guild_emoji(struct discord *client,
u64snowflake guild_id,
struct discord_create_guild_emoji *params,
struct discord_ret_emoji *ret);
/**
* @brief Modify the given emoji
* @note Fires a `Guild Emojis Update` event
*
* @param client the client created with discord_from_token()
* @param guild_id guild the emoji belongs to
* @param emoji_id the emoji to be modified
* @param params request parameters
* @CCORD_ret_obj{ret,emoji}
* @CCORD_return
*/
CCORDcode discord_modify_guild_emoji(struct discord *client,
u64snowflake guild_id,
u64snowflake emoji_id,
struct discord_modify_guild_emoji *params,
struct discord_ret_emoji *ret);
/**
* @brief Deletes the given emoji
* @note Fires a `Guild Emojis Update` event
*
* @param client the client created with discord_from_token()
* @param guild_id guild the emoji belongs to
* @param emoji_id the emoji to be deleted
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_guild_emoji(struct discord *client,
u64snowflake guild_id,
u64snowflake emoji_id,
struct discord_delete_guild_emoji *params,
struct discord_ret *ret);
/** @example emoji.c
* Demonstrates a couple use cases of the Emoji API */
/** @} DiscordAPIEmoji */
#endif /* DISCORD_EMOJI_H */
+76
View File
@@ -0,0 +1,76 @@
/**
* @file gateway.h
* @author Cogmasters
* @brief Gateway public functions and datatypes
*/
#ifndef DISCORD_GATEWAY_H
#define DISCORD_GATEWAY_H
/** @defgroup DiscordAPIGateway Gateway
* @ingroup DiscordAPI
* @brief Gateway's public API supported by Concord
* @{ */
/**
* @brief Get a single valid WSS URL, which the client can use for connecting
* @note This route should be cached, and only call the function again if
* unable to properly establishing a connection with the cached version
* @warning This function blocks the running thread
*
* @param client the client created with discord_from_token()
* @param ret if successful, a @ref ccord_szbuf containing the JSON response
* @param ret a sized buffer containing the response JSON
* @CCORD_return
*/
CCORDcode discord_get_gateway(struct discord *client, struct ccord_szbuf *ret);
/**
* @brief Get a single valid WSS URL, and additional metadata that can help
* during the operation of large bots.
* @note This route should not be cached for extended periods of time as the
* value is not guaranteed to be the same per-call, and changes as the
* bot joins/leaves guilds
* @warning This function blocks the running thread
*
* @param client the client created with discord_from_token()
* @param ret if successful, a @ref ccord_szbuf containing the JSON response
* @param ret a sized buffer containing the response JSON
* @CCORD_return
*/
CCORDcode discord_get_gateway_bot(struct discord *client,
struct ccord_szbuf *ret);
/** @defgroup DiscordAPIGatewayHelper Helper functions
* @brief Custom helper functions
* @{ */
/**
* @brief Disconnect a member from voice channel
*
* @param client the client created with discord_from_token()
* @param guild_id the guild the member belongs to
* @param user_id the user to be disconnected
* @param params request parameters
* @CCORD_ret_obj{ret,guild_member}
* @CCORD_return
*/
CCORDcode discord_disconnect_guild_member(
struct discord *client,
u64snowflake guild_id,
u64snowflake user_id,
struct discord_modify_guild_member *params,
struct discord_ret_guild_member *ret);
/**
* @brief Helper function to add presence activities
* @see discord_set_presence()
*/
void discord_presence_add_activity(struct discord_presence_update *presence,
struct discord_activity *activity);
/** @} DiscordAPIGatewayHelper */
/** @} DiscordAPIGateway */
#endif /* DISCORD_GATEWAY_H */
+683
View File
@@ -0,0 +1,683 @@
/**
* @file guild.h
* @author Cogmasters
* @brief Guild public functions and datatypes
*/
#ifndef DISCORD_GUILD_H
#define DISCORD_GUILD_H
/** @defgroup DiscordAPIGuild Guild
* @ingroup DiscordAPI
* @brief Guild's public API supported by Concord
* @{ */
/**
* @brief Create a new guild
* @note Fires a `Guild Create` event
*
* @param client the client created with discord_from_token()
* @param params request parameters
* @CCORD_ret_obj{ret,guild}
* @CCORD_return
*/
CCORDcode discord_create_guild(struct discord *client,
struct discord_create_guild *params,
struct discord_ret_guild *ret);
/**
* @brief Get the guild with given id
* @todo missing query parameters
* @note If with_counts is set to true, this endpoint will also return
* approximate_member_count and approximate_presence_count for the
* guild
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild to retrieve
* @CCORD_ret_obj{ret,guild}
* @CCORD_return
*/
CCORDcode discord_get_guild(struct discord *client,
u64snowflake guild_id,
struct discord_ret_guild *ret);
/**
* @brief Get the preview for the given guild
* @note If the user is not in the guild, then the guild must be lurkable
*
* @param client the client created with discord_from_token()
* @param guild_id guild to get preview from
* @CCORD_ret_obj{ret,guild_preview}
* @CCORD_return
*/
CCORDcode discord_get_guild_preview(struct discord *client,
u64snowflake guild_id,
struct discord_ret_guild_preview *ret);
/**
* @brief Modify a guild's settings
* @note Requires the MANAGE_GUILD permission
* @note Fires a `Guild Update` event
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild to modify
* @param params request parameters
* @CCORD_ret_obj{ret,guild}
* @CCORD_return
*/
CCORDcode discord_modify_guild(struct discord *client,
u64snowflake guild_id,
struct discord_modify_guild *params,
struct discord_ret_guild *ret);
/**
* @brief Delete a guild permanently, user must be owner
* @note Fires a `Guild Delete` event
*
* @param client the client created with discord_from_token()
* @param guild_id id of guild to delete
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_guild(struct discord *client,
u64snowflake guild_id,
struct discord_ret *ret);
/**
* @brief Fetch channels from given guild. Does not include threads
*
* @param client the client created with discord_from_token()
* @param guild_id id of guild to fetch channels from
* @CCORD_ret_obj{ret,channels}
* @CCORD_return
*/
CCORDcode discord_get_guild_channels(struct discord *client,
u64snowflake guild_id,
struct discord_ret_channels *ret);
/**
* @brief Create a new guild channel
* @note Requires the MANAGE_CHANNELS permission
* @note If setting permission overwrites, only permissions your
* bot has in the guild can be allowed/denied. Setting MANAGE_ROLES
* permission in channels is only possible for guild administrators
* @note Fires a `Channel Create` event
*
* @param client the client created with discord_from_token()
* @param guild_id id of the guild to create a channel at
* @param params request parameters
* @CCORD_ret_obj{ret,channel}
* @CCORD_return
*/
CCORDcode discord_create_guild_channel(
struct discord *client,
u64snowflake guild_id,
struct discord_create_guild_channel *params,
struct discord_ret_channel *ret);
/**
* @brief Modify guild channel positions
* @note Requires MANAGE_CHANNELS permission
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild to change the positions of the
* channels in
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_modify_guild_channel_positions(
struct discord *client,
u64snowflake guild_id,
struct discord_modify_guild_channel_positions *params,
struct discord_ret *ret);
/**
* @brief Get guild member of a guild from given user id
*
* @param client the client created with discord_from_token()
* @param guild_id guild the member belongs to
* @param user_id unique user id of member
* @CCORD_ret_obj{ret,guild_member}
* @CCORD_return
*/
CCORDcode discord_get_guild_member(struct discord *client,
u64snowflake guild_id,
u64snowflake user_id,
struct discord_ret_guild_member *ret);
/**
* @brief Get guild members of a guild
*
* @param client the client created with discord_from_token()
* @param guild_id guild the members belongs to
* @param request parameters
* @CCORD_ret_obj{ret,guild_members}
* @CCORD_return
*/
CCORDcode discord_list_guild_members(struct discord *client,
u64snowflake guild_id,
struct discord_list_guild_members *params,
struct discord_ret_guild_members *ret);
/**
* @brief Get guild members whose username or nickname starts with a provided
* string
*
* @param client the client created with discord_from_token()
* @param guild_id guild the members belongs to
* @param request parameters
* @CCORD_ret_obj{ret,guild_members}
* @CCORD_return
*/
CCORDcode discord_search_guild_members(
struct discord *client,
u64snowflake guild_id,
struct discord_search_guild_members *params,
struct discord_ret_guild_members *ret);
/**
* @brief Adds a user to the guild
* @note Requires valid oauth2 access token for the user with `guilds.join`
* scope
* @note Fires a `Guild Member Add` event
* @note The bot must be a member of the guild with CREATE_INSTANT_INVITE
* permission
*
* @param client the client created with discord_from_token()
* @param guild_id guild to add the member to
* @param user_id the user to be added
* @param request parameters
* @CCORD_ret_obj{ret,guild_member}
* @CCORD_return
*/
CCORDcode discord_add_guild_member(struct discord *client,
u64snowflake guild_id,
u64snowflake user_id,
struct discord_add_guild_member *params,
struct discord_ret_guild_member *ret);
/**
* @brief Modify retibutes of a guild member
* @note Fires a `Guild Member Update` event
* @see discord_disconnect_guild_member()
*
* @param client the client created with discord_from_token()
* @param guild_id guild the member belongs to
* @param user_id the user id of member
* @param request parameters
* @CCORD_ret_obj{ret,guild_member}
* @CCORD_return
*/
CCORDcode discord_modify_guild_member(
struct discord *client,
u64snowflake guild_id,
u64snowflake user_id,
struct discord_modify_guild_member *params,
struct discord_ret_guild_member *ret);
/**
* @brief Modifies the current member in the guild
* @note Fires a `Guild Member Update` event
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild where the member exists
* @param params request parameters
* @CCORD_ret_obj{ret,guild_member}
* @CCORD_return
*/
CCORDcode discord_modify_current_member(
struct discord *client,
u64snowflake guild_id,
struct discord_modify_current_member *params,
struct discord_ret_guild_member *ret);
/**
* @brief Adds a role to a guild member
* @note Fires a `Guild Member Update` event
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild where the member exists
* @param user_id the unique id of the user
* @param role_id the unique id of the role to be added
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_add_guild_member_role(
struct discord *client,
u64snowflake guild_id,
u64snowflake user_id,
u64snowflake role_id,
struct discord_add_guild_member_role *params,
struct discord_ret *ret);
/**
* @brief Removes a role from a guild member
* @note Requires the MANAGE_ROLES permission
* @note Fires a `Guild Member Update` event
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild where the member exists
* @param user_id the unique id of the user
* @param role_id the unique id of the role to be removed
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_remove_guild_member_role(
struct discord *client,
u64snowflake guild_id,
u64snowflake user_id,
u64snowflake role_id,
struct discord_remove_guild_member_role *params,
struct discord_ret *ret);
/**
* @brief Remove a member from a guild
* @note Requires the KICK_MEMBERS permission
* @note Fires a `Guild Member Update` event
*
* @param client the client created with discord_from_token()
* @param guild_id the guild to remove the member from
* @param user_id the user to be removed
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_remove_guild_member(
struct discord *client,
u64snowflake guild_id,
u64snowflake user_id,
struct discord_remove_guild_member *params,
struct discord_ret *ret);
/**
* @brief Fetch banned users for given guild
* @note Requires the BAN_MEMBERS permission
*
* @param client the client created with discord_from_token()
* @param guild_id the guild to get the list from
* @CCORD_ret_obj{ret,bans}
* @CCORD_return
*/
CCORDcode discord_get_guild_bans(struct discord *client,
u64snowflake guild_id,
struct discord_ret_bans *ret);
/**
* @brief Fetch banned user from given guild
* @note Requires the BAN_MEMBERS permission
*
* @param client the client created with discord_from_token()
* @param guild_id the guild to return the ban from
* @param user_id the user that is banned
* @CCORD_ret_obj{ret,ban}
* @CCORD_return
*/
CCORDcode discord_get_guild_ban(struct discord *client,
u64snowflake guild_id,
u64snowflake user_id,
struct discord_ret_ban *ret);
/**
* @brief Bans user from a given guild
* @note Requires the BAN_MEMBERS permission
* @note Fires a `Guild Ban Add` event
*
* @param client the client created with discord_from_token()
* @param guild_id guild the user belongs to
* @param user_id the user to be banned
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_create_guild_ban(struct discord *client,
u64snowflake guild_id,
u64snowflake user_id,
struct discord_create_guild_ban *params,
struct discord_ret *ret);
/**
* @brief Remove the ban for a user
* @note Requires the BAN_MEMBERS permission
* @note Fires a `Guild Ban Remove` event
*
* @param client the client created with discord_from_token()
* @param guild_id guild the user belonged to
* @param user_id the user to have its ban revoked
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_remove_guild_ban(struct discord *client,
u64snowflake guild_id,
u64snowflake user_id,
struct discord_remove_guild_ban *params,
struct discord_ret *ret);
/**
* @brief Get guild roles
*
* @param client the client created with discord_from_token()
* @param guild_id guild to get roles from
* @CCORD_ret_obj{ret,roles}
* @CCORD_return
*/
CCORDcode discord_get_guild_roles(struct discord *client,
u64snowflake guild_id,
struct discord_ret_roles *ret);
/**
* @brief Create a new guild role
* @note Requires MANAGE_ROLES permission
* @note Fires a `Guild Role Create` event
*
* @param client the client created with discord_from_token()
* @param guild_id guild to add a role to
* @param params request parameters
* @CCORD_ret_obj{ret,role}
* @CCORD_return
*/
CCORDcode discord_create_guild_role(struct discord *client,
u64snowflake guild_id,
struct discord_create_guild_role *params,
struct discord_ret_role *ret);
/**
* @brief Returns the number of members that would be removed in a prune
* operation
* @note Requires the KICK_MEMBERS permission
* @note By default will not remove users with roles. You can include specific
* roles in your prune by providing the `params.include_roles` value
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild to be checked
* @param params request parameters
* @CCORD_ret_obj{ret,prune_count}
* @CCORD_return
*/
CCORDcode discord_get_guild_prune_count(
struct discord *client,
u64snowflake guild_id,
struct discord_get_guild_prune_count *params,
struct discord_ret_prune_count *ret);
/**
* @brief Begin guild prune operation
* @note Discord recommends for larger servers to set "compute_prune_count" to
* false
* @note Requires the KICK_MEMBERS permission
* @note Fires multiple `Guild Member Remove` events
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild to start the prune
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_begin_guild_prune(struct discord *client,
u64snowflake guild_id,
struct discord_begin_guild_prune *params,
struct discord_ret *ret);
/**
* @brief Get voice regions (includes VIP servers when the guild is
* VIP-enabled)
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild to get voice regions from
* @CCORD_ret_obj{ret,voice_regions}
* @CCORD_return
*/
CCORDcode discord_get_guild_voice_regions(
struct discord *client,
u64snowflake guild_id,
struct discord_ret_voice_regions *ret);
/**
* @brief Get guild invites
* @note requires the `MANAGE_GUILD` permission
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild to get invites from
* @CCORD_ret_obj{ret,invites}
* @CCORD_return
*/
CCORDcode discord_get_guild_invites(struct discord *client,
u64snowflake guild_id,
struct discord_ret_invites *ret);
/**
* @brief Get guild integrations
* @note requires the `MANAGE_GUILD` permission
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild to get integrations from
* @CCORD_ret_obj{ret,integrations}
* @CCORD_return
*/
CCORDcode discord_get_guild_integrations(struct discord *client,
u64snowflake guild_id,
struct discord_ret_integrations *ret);
/**
* @brief Deletes the integration for the guild. It will also delete any
* associated webhooks and bots
* @note Requires the MANAGE_GUILD permission
* @note Fires a `Guild Integrations Update` event
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild to delete the integrations from
* @param integration_id the id of the integration to delete
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_guild_integrations(
struct discord *client,
u64snowflake guild_id,
u64snowflake integration_id,
struct discord_delete_guild_integrations *params,
struct discord_ret *ret);
/**
* @brief Get a guild widget settings
* @note requires the `MANAGE_GUILD` permission
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild to get widget settings from
* @CCORD_ret_obj{ret,guild_widget_settings}
* @CCORD_return
*/
CCORDcode discord_get_guild_widget_settings(
struct discord *client,
u64snowflake guild_id,
struct discord_ret_guild_widget_settings *ret);
/**
* @brief Modify a guild widget settings
* @note requires the `MANAGE_GUILD` permission
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild to modify the widget settings
* from
* @param param request parameters
* @CCORD_ret_obj{ret,guild_widget_settings}
* @CCORD_return
*/
CCORDcode discord_modify_guild_widget(
struct discord *client,
u64snowflake guild_id,
struct discord_guild_widget_settings *params,
struct discord_ret_guild_widget_settings *ret);
/**
* @brief Get the widget for the guild
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild to get the widget from
* @CCORD_ret_obj{ret,guild_widget}
* @CCORD_return
*/
CCORDcode discord_get_guild_widget(struct discord *client,
u64snowflake guild_id,
struct discord_ret_guild_widget *ret);
/**
* @brief Get invite from a given guild
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild to get vanity url from
* @CCORD_ret_obj{ret,invite}
* @CCORD_return
*/
CCORDcode discord_get_guild_vanity_url(struct discord *client,
u64snowflake guild_id,
struct discord_ret_invite *ret);
/* TODO: handle ContentType: image/png and add 'struct discord_png' */
#if 0
/**
* @brief Get a PNG image widget for the guild
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild to get a PNG widget image from
* @param params request parameters
* @CCORD_ret_obj{ret,png}
* @CCORD_return
*/
CCORDcode discord_get_guild_widget_image(
struct discord *client,
u64snowflake guild_id,
struct discord_get_guild_widget_image *params,
struct discord_ret_png *ret);
#endif
/**
* @brief Get the Welcome Screen for the guild
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild to get welcome screen of
* @CCORD_ret_obj{ret,welcome_screen}
* @CCORD_return
*/
CCORDcode discord_get_guild_welcome_screen(
struct discord *client,
u64snowflake guild_id,
struct discord_ret_welcome_screen *ret);
/**
* @brief Modify the Welcome Screen for the guild
* @note requires the `MANAGE_GUILD` permission
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild to modify welcome screen of
* @param params request parameters
* @CCORD_ret_obj{ret,welcome_screen}
* @CCORD_return
*/
CCORDcode discord_modify_guild_welcome_screen(
struct discord *client,
u64snowflake guild_id,
struct discord_modify_guild_welcome_screen *params,
struct discord_ret_welcome_screen *ret);
/**
* @brief Updates the current user's voice state
* @see Caveats
* https://discord.com/developers/docs/resources/guild#modify-current-user-voice-state-caveats
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild to modify the current user's
* voice state
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_modify_current_user_voice_state(
struct discord *client,
u64snowflake guild_id,
struct discord_modify_current_user_voice_state *params,
struct discord_ret *ret);
/**
* @brief Updates user's voice state
* @see Caveats
* https://discord.com/developers/docs/resources/guild#modify-user-voice-state-caveats
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild to modify the user's voice state
* @param user_id the unique id of user to have its voice state modified
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_modify_user_voice_state(
struct discord *client,
u64snowflake guild_id,
u64snowflake user_id,
struct discord_modify_user_voice_state *params,
struct discord_ret *ret);
/**
* @brief Modify the positions of a given role list for the guild
* @note Requires the MANAGE_ROLES permission
* @note Fires multiple `Guild Role Update` events
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild to get welcome screen of
* @param params request parameters
* @CCORD_ret_obj{ret,roles}
* @CCORD_return
*/
CCORDcode discord_modify_guild_role_positions(
struct discord *client,
u64snowflake guild_id,
struct discord_modify_guild_role_positions *params,
struct discord_ret_roles *ret);
/**
* @brief Modify a guild role
* @note Requires the MANAGE_ROLES permission
* @note Fires a `Guild Role Update` event
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild that the role belongs to
* @param role_id the unique id of the role to modify
* @param params request parameters
* @CCORD_ret_obj{ret,role}
* @CCORD_return
*/
CCORDcode discord_modify_guild_role(struct discord *client,
u64snowflake guild_id,
u64snowflake role_id,
struct discord_modify_guild_role *params,
struct discord_ret_role *ret);
/**
* @brief Delete a guild role
* @note Requires the MANAGE_ROLES permission
* @note Fires a `Guild Role Delete` event
*
* @param client the client created with discord_from_token()
* @param guild_id the unique id of the guild that the role belongs to
* @param role_id the unique id of the role to delete
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_guild_role(struct discord *client,
u64snowflake guild_id,
u64snowflake role_id,
struct discord_delete_guild_role *params,
struct discord_ret *ret);
/** @example guild.c
* Demonstrates a couple use cases of the Guild API */
/** @example ban.c
* Demonstrates banning and unbanning members */
/** @} DiscordAPIGuild */
#endif /* DISCORD_GUILD_H */
+118
View File
@@ -0,0 +1,118 @@
/**
* @file guild_scheduled_event.h
* @author Cogmasters
* @brief Guild Scheduled Event public functions and datatypes
*/
#ifndef DISCORD_GUILD_SCHEDULED_EVENT_H
#define DISCORD_GUILD_SCHEDULED_EVENT_H
/** @defgroup DiscordAPIGuildScheduledEvent Guild Scheduled Event
* @ingroup DiscordAPI
* @brief Guild Scheduled Event's public API supported by Concord
* @{ */
/**
* @brief Get a list of scheduled events for the guild
*
* @param client the client created with discord_from_token()
* @param guild_id the guild to fetch the scheduled events from
* @param params request parameters
* @CCORD_ret_obj{ret,guild_scheduled_events}
* @CCORD_return
*/
CCORDcode discord_list_guild_scheduled_events(
struct discord *client,
u64snowflake guild_id,
struct discord_list_guild_scheduled_events *params,
struct discord_ret_guild_scheduled_events *ret);
/**
* @brief Create a guild scheduled event
* @note A guild can have a maximum of 100 events with `SCHEDULED` or `ACTIVE`
* status at any time
*
* @param client the client created with discord_from_token()
* @param guild_id the guild to create the scheduled event at
* @param params request parameters
* @CCORD_ret_obj{ret,guild_scheduled_event}
* @CCORD_return
*/
CCORDcode discord_create_guild_scheduled_event(
struct discord *client,
u64snowflake guild_id,
struct discord_create_guild_scheduled_event *params,
struct discord_ret_guild_scheduled_event *ret);
/**
* @brief Get a guild scheduled event
*
* @param client the client created with discord_from_token()
* @param guild_id the guild to fetch the scheduled event from
* @param guild_scheduled_event_id the scheduled event to be fetched
* @param params request parameters
* @CCORD_ret_obj{ret,guild_scheduled_event}
* @CCORD_return
*/
CCORDcode discord_get_guild_scheduled_event(
struct discord *client,
u64snowflake guild_id,
u64snowflake guild_scheduled_event_id,
struct discord_get_guild_scheduled_event *params,
struct discord_ret_guild_scheduled_event *ret);
/**
* @brief Modify a guild scheduled event
* @note Silently discards `entity_metadata` for non-`EXTERNAL` events
*
* @param client the client created with discord_from_token()
* @param guild_id the guild where the scheduled event to be modified is at
* @param guild_scheduled_event_id the scheduled event to be modified
* @param params request parameters
* @CCORD_ret_obj{ret,guild_scheduled_event}
* @CCORD_return
*/
CCORDcode discord_modify_guild_scheduled_event(
struct discord *client,
u64snowflake guild_id,
u64snowflake guild_scheduled_event_id,
struct discord_modify_guild_scheduled_event *params,
struct discord_ret_guild_scheduled_event *ret);
/**
* @brief Delete a guild scheduled event
*
* @param client the client created with discord_from_token()
* @param guild_id the guild where the scheduled event to be deleted is at
* @param guild_scheduled_event_id the scheduled event to be deleted
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_guild_scheduled_event(
struct discord *client,
u64snowflake guild_id,
u64snowflake guild_scheduled_event_id,
struct discord_ret *ret);
/**
* @brief Get a list of members subscribed to a guild scheduled event
* @note Guild member data, if it exists, is included if the
* `params.with_member` value is set
*
* @param client the client created with discord_from_token()
* @param guild_id the guild with the scheduled event belongs to
* @param guild_scheduled_event_id the scheduled event
* @param params request parameters
* @CCORD_ret_obj{ret,guild_scheduled_event_users}
* @CCORD_return
*/
CCORDcode discord_get_guild_scheduled_event_users(
struct discord *client,
u64snowflake guild_id,
u64snowflake guild_scheduled_event_id,
struct discord_get_guild_scheduled_event_users *params,
struct discord_ret_guild_scheduled_event_users *ret);
/** @} DiscordAPIGuildScheduledEvent */
#endif /* DISCORD_GUILD_SCHEDULED_EVENT_H */
+126
View File
@@ -0,0 +1,126 @@
/**
* @file guild_template.h
* @author Cogmasters
* @brief Guild Template public functions and datatypes
*/
#ifndef DISCORD_GUILD_TEMPLATE_H
#define DISCORD_GUILD_TEMPLATE_H
/** @defgroup DiscordAPIGuildTemplate Guild Template
* @ingroup DiscordAPI
* @brief Guild Template's public API supported by Concord
* @{ */
/**
* @brief Get a guild template for the given code
*
* @param client the client created with discord_from_token()
* @param template_code the guild template code
* @CCORD_ret_obj{ret,guild_template}
* @CCORD_return
*/
CCORDcode discord_get_guild_template(struct discord *client,
const char template_code[],
struct discord_ret_guild_template *ret);
/**
* @brief Create a new guild based on a template
* @note This endpoint can be used only by bots in less than 10 guilds
*
* @param client the client created with discord_from_token()
* @param template_code the guild template code
* @param params the request parameters
* @CCORD_ret_obj{ret,guild}
* @CCORD_return
*/
CCORDcode discord_create_guild_from_guild_template(
struct discord *client,
const char template_code[],
struct discord_create_guild_from_guild_template *params,
struct discord_ret_guild *ret);
/**
* @brief Returns @ref discord_guild_templates from a guild
* @note Requires the `MANAGE_GUILD` permission
*
* @param client the client created with discord_from_token()
* @param guild_id the guild to fetch the templates from
* @CCORD_ret_obj{ret,guild_templates}
* @CCORD_return
*/
CCORDcode discord_get_guild_templates(struct discord *client,
u64snowflake guild_id,
struct discord_ret_guild_templates *ret);
/**
* @brief Creates a template for the guild
* @note Requires the `MANAGE_GUILD` permission
*
* @param client the client created with discord_from_token()
* @param guild_id the guild to create a template from
* @param params the request parameters
* @CCORD_ret_obj{ret,guild_template}
* @CCORD_return
*/
CCORDcode discord_create_guild_template(
struct discord *client,
u64snowflake guild_id,
struct discord_create_guild_template *params,
struct discord_ret_guild_template *ret);
/**
* @brief Syncs the template to the guild's current state
* @note Requires the `MANAGE_GUILD` permission
*
* @param client the client created with discord_from_token()
* @param guild_id the guild to sync the template from
* @param template_code the guild template code
* @CCORD_ret_obj{ret,guild_template}
* @CCORD_return
*/
CCORDcode discord_sync_guild_template(struct discord *client,
u64snowflake guild_id,
const char template_code[],
struct discord_ret_guild_template *ret);
/**
* @brief Modifies the template's metadata
* @note Requires the `MANAGE_GUILD` permission
*
* @param client the client created with discord_from_token()
* @param guild_id the guild to modify the template at
* @param template_code the guild template code
* @param params the request parameters
* @CCORD_ret_obj{ret,guild_template}
* @CCORD_return
*/
CCORDcode discord_modify_guild_template(
struct discord *client,
u64snowflake guild_id,
const char template_code[],
struct discord_modify_guild_template *params,
struct discord_ret_guild_template *ret);
/**
* @brief Deletes the guild template
* @note Requires the `MANAGE_GUILD` permission
*
* @param client the client created with discord_from_token()
* @param guild_id the guild to delete the template at
* @param template_code the guild template code
* @CCORD_ret_obj{ret,guild_template}
* @CCORD_return
*/
CCORDcode discord_delete_guild_template(
struct discord *client,
u64snowflake guild_id,
const char template_code[],
struct discord_ret_guild_template *ret);
/** @example guild-template.c
* Demonstrates a couple use cases of the Guild Template API */
/** @} DiscordAPIGuildTemplate */
#endif /* DISCORD_GUILD_TEMPLATE_H */
+152
View File
@@ -0,0 +1,152 @@
/**
* @file interaction.h
* @author Cogmasters
* @brief Interaciton public functions and datatypes
*/
#ifndef DISCORD_INTERACTION_H
#define DISCORD_INTERACTION_H
/** @defgroup DiscordAPIInteractionsReact Receiving and sending
* @ingroup DiscordAPIInteractions
* @brief Receiving and sending interactions
* @{ */
/**
* @brief Create a response to an Interaction from the gateway
*
* @param client the client created with discord_from_token()
* @param interaction_id the unique id of the interaction
* @param interaction_token the unique token of the interaction
* @param params the request parameters
* @CCORD_ret_obj{ret,interaction_response}
* @CCORD_return
*/
CCORDcode discord_create_interaction_response(
struct discord *client,
u64snowflake interaction_id,
const char interaction_token[],
struct discord_interaction_response *params,
struct discord_ret_interaction_response *ret);
/**
* @brief Get the initial Interaction response
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the application
* @param interaction_token the unique token of the interaction
* @CCORD_ret_obj{ret,interaction_response}
* @CCORD_return
*/
CCORDcode discord_get_original_interaction_response(
struct discord *client,
u64snowflake application_id,
const char interaction_token[],
struct discord_ret_interaction_response *ret);
/**
* @brief Edit the initial Interaction response
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the application
* @param interaction_token the unique token of the interaction
* @param params request parameters
* @CCORD_ret_obj{ret,interaction_response}
* @CCORD_return
*/
CCORDcode discord_edit_original_interaction_response(
struct discord *client,
u64snowflake application_id,
const char interaction_token[],
struct discord_edit_original_interaction_response *params,
struct discord_ret_interaction_response *ret);
/**
* @brief Delete the initial Interaction response
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the application
* @param interaction_token the unique token of the interaction
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_original_interaction_response(
struct discord *client,
u64snowflake application_id,
const char interaction_token[],
struct discord_ret *ret);
/**
* @brief Create a followup message for an Interaction
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the application
* @param interaction_token the unique token of the interaction
* @param params request parameters
* @CCORD_ret_obj{ret,webhook}
* @CCORD_return
*/
CCORDcode discord_create_followup_message(
struct discord *client,
u64snowflake application_id,
const char interaction_token[],
struct discord_create_followup_message *params,
struct discord_ret_webhook *ret);
/**
* @brief Get a followup message for an interaction
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the application
* @param interaction_token the unique token of the interaction
* @param message_id the unique id of the message
* @CCORD_ret_obj{ret,message}
* @CCORD_return
*/
CCORDcode discord_get_followup_message(struct discord *client,
u64snowflake application_id,
const char interaction_token[],
u64snowflake message_id,
struct discord_ret_message *ret);
/**
* @brief Edits a followup message for an interaction
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the application
* @param interaction_token the unique token of the interaction
* @param message_id the unique id of the message
* @param params request parameters
* @CCORD_ret_obj{ret,message}
* @CCORD_return
*/
CCORDcode discord_edit_followup_message(
struct discord *client,
u64snowflake application_id,
const char interaction_token[],
u64snowflake message_id,
struct discord_edit_followup_message *params,
struct discord_ret_message *ret);
/**
* @brief Edits a followup message for an interaction
*
* @param client the client created with discord_from_token()
* @param application_id the unique id of the application
* @param interaction_token the unique token of the interaction
* @param message_id the unique id of the message
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_followup_message(struct discord *client,
u64snowflake application_id,
const char interaction_token[],
u64snowflake message_id,
struct discord_ret *ret);
/** @example components.c
* Demonstrates a couple use cases of the Message Components API */
/** @} DiscordAPIInteractionsReact */
#endif /* DISCORD_INTERACTION_H */
+51
View File
@@ -0,0 +1,51 @@
/**
* @file invite.h
* @author Cogmasters
* @brief Invite public functions and datatypes
*/
#ifndef DISCORD_INVITE_H
#define DISCORD_INVITE_H
/** @defgroup DiscordAPIInvite Invite
* @ingroup DiscordAPI
* @brief Invite's public API supported by Concord
* @{ */
/**
* @brief Get an invite for the given code
*
* @param client the client created with discord_from_token()
* @param invite_code the invite code
* @param params request parameters
* @CCORD_ret_obj{ret,invite}
* @CCORD_return
*/
CCORDcode discord_get_invite(struct discord *client,
char *invite_code,
struct discord_get_invite *params,
struct discord_ret_invite *ret);
/**
* @brief Delete an invite
* @note Requires the MANAGE_CHANNELS permission on the channel this invite
* belongs to, or MANAGE_GUILD to remove any invite across the guild.
* @note Fires a `Invite Delete` event
*
* @param client the client created with discord_from_token()
* @param invite_code the invite code
* @param params request parameters
* @CCORD_ret_obj{ret,invite}
* @CCORD_return
*/
CCORDcode discord_delete_invite(struct discord *client,
char *invite_code,
struct discord_delete_invite *params,
struct discord_ret_invite *ret);
/** @example invite.c
* Demonstrates a couple use cases of the Invite API */
/** @} DiscordAPIInvite */
#endif /* DISCORD_INVITE_H */
+118
View File
@@ -0,0 +1,118 @@
#ifndef CONCORD_IO_POLLER_H
#define CONCORD_IO_POLLER_H
#include <stdbool.h>
#include <curl/curl.h>
/**
* @brief The flags to poll for
*/
enum io_poller_events {
IO_POLLER_IN = 1 << 0,
IO_POLLER_OUT = 1 << 1,
};
/**
* @brief a socket or file descriptor
*/
typedef int io_poller_socket;
/**
* @brief handle for watching file descriptors, sockets, and curl multis
*/
struct io_poller;
/**
* @brief callback for when an event is triggered by the socket
*/
typedef void (*io_poller_cb)(struct io_poller *io,
enum io_poller_events events,
void *user_data);
struct io_poller *io_poller_create(void);
void io_poller_destroy(struct io_poller *io);
/**
* @brief wakeup the thread listening to this io_poller
*
* @param io the io_poller to wake up
*/
void
io_poller_wakeup(struct io_poller *io);
/**
* @brief wait for events to be triggered
* @param io the io_poller to poll on
* @param milliseconds -1 for infinity, or ms to poll for
* @return -1 for error, or number of sockets that have events waiting
*/
int io_poller_poll(struct io_poller *io, int milliseconds);
/**
* @brief performs any actions needed and clears events set by io_poller_poll
* @param io the io_poller to perform on
* @return 0 on success
*/
int io_poller_perform(struct io_poller *io);
/**
* @brief adds or modifies a socket or file descriptor to watch list
* @param io the io_poller to add socket to
* @param sock the file descriptor or socket to handle
* @param events the events to watch for
* @param cb the callback for when any event is triggered
* @param user_data custom user data
* @return true on success
*/
bool io_poller_socket_add(struct io_poller *io,
io_poller_socket sock,
enum io_poller_events events,
io_poller_cb cb,
void *user_data);
/**
* @brief removes a socket or file descriptor from watch list
* @param io the io_poller to remove the socket from
* @param sock the file descriptor or socket to remove
* @return true on success
*/
bool io_poller_socket_del(struct io_poller *io, io_poller_socket sock);
/**
* @brief callback for when curl multi should be performed on
*/
typedef int (*io_poller_curl_cb)(struct io_poller *io,
CURLM *multi,
void *user_data);
/**
* @brief add or modifies a curl multi to watch list
* @param io the io_poller to add curl multi to
* @param multi the curl multi to add or modify
* @param cb the callback for when curl multi should be performed on
* @param user_data custom user data
* @return true on success
*/
bool io_poller_curlm_add(struct io_poller *io,
CURLM *multi,
io_poller_curl_cb cb,
void *user_data);
/**
* @brief remove curl multi from watch list
* @param io the io_poller to remove curl multi from
* @param multi the curl multi to remove
* @return true on success
*/
bool io_poller_curlm_del(struct io_poller *io, CURLM *multi);
/**
* @brief this multi should be performed on next cycle
* causing poll to return immediately
* @param io the io_poller to enable perform on
* @param multi the multi that should be performed
* @return true on success
*/
bool io_poller_curlm_enable_perform(struct io_poller *io, CURLM *multi);
#endif // CONCORD_IO_POLLER_H
+683
View File
@@ -0,0 +1,683 @@
#ifndef JSMN_FIND_H
#define JSMN_FIND_H
#ifdef __cplusplus
extern "C" {
#endif
#ifndef JSMN_H
#error "jsmn-find.h should be included after jsmn.h"
#else
#define OA_HASH_HEADER
#include "oa_hash.h"
#undef OA_HASH_HEADER
#define JSMNF_PAIR_ATTRS_const \
/** JSON object or array pair attributes */ \
OA_HASH_ATTRS(const); \
/** JSON object or array fields */ \
const struct jsmnf_pair *const fields; \
/** key attributes */ \
const jsmntok_t *const k; \
/** value attribute */ \
const jsmntok_t *const v
#define JSMNF_PAIR_ATTRS_mut \
/** JSON object or array pair attributes */ \
OA_HASH_ATTRS(mut); \
/** JSON object or array fields */ \
struct jsmnf_pair *fields; \
/** key attributes */ \
jsmntok_t *k; \
/** value attribute */ \
jsmntok_t *v
#define JSMNF_PAIR_ATTRS(_qualifier) JSMNF_PAIR_ATTRS_##_qualifier
typedef struct jsmnf_pair {
JSMNF_PAIR_ATTRS(const);
} jsmnf_pair;
/** @brief Bucket @ref jsmnf_pair loader, keeps track of pair array
* position */
typedef struct jsmnf_loader {
/** jsmnf_loader can be cast to jsmn_parser */
jsmn_parser parser;
/** next pair to allocate */
unsigned pairnext;
/** root pair */
const jsmnf_pair *root;
} jsmnf_loader;
/** @brief JSON table, not supposed to be accessed by user */
typedef struct jsmnf_table {
/** @private */
const struct jsmntok _;
const struct jsmnf_pair __;
const struct oa_hash_entry ___;
} jsmnf_table;
/**
* @brief Initialize a @ref jsmnf_loader
*
* @param[out] loader jsmnf_loader to be initialized
*/
JSMN_API void jsmnf_init(jsmnf_loader *loader);
/**
* @brief Populate the @ref jsmnf_pair pairs from jsmn tokens
*
* @param[in,out] loader the @ref jsmnf_loader initialized with jsmnf_init()
* @param[in] js the JSON data string
* @param[in] len the raw JSON string length
* @param[out] tokens jsmn tokens
* @param[out] table jsmnf_table pairs array
* @param[in] table_len maximum amount of pairs provided
* @attention must not be less than the amount of tokens
* @return a `enum jsmnerr` value for error or the amount of `pairs` used
*/
JSMN_API long jsmnf_load(jsmnf_loader *loader,
const char js[],
const size_t len,
jsmnf_table table[],
const size_t table_len);
/**
* @brief Find a @ref jsmnf_pair token by its associated key
*
* @param[in] head a @ref jsmnf_pair object or array loaded at jsmnf_init()
* @param[in] key the key too be matched
* @param[in] length length of the key too be matched
* @return the @ref jsmnf_pair `head`'s field matched to `key`, or NULL if
* not encountered
*/
JSMN_API const jsmnf_pair *jsmnf_find(const jsmnf_pair *const head,
const char key[],
const size_t length);
/**
* @brief Find a @ref jsmnf_pair token by its full key path
*
* @param[in] head a @ref jsmnf_pair object or array loaded at jsmnf_init()
* @param[in] path an array of key path strings, from least to highest depth
* @param[in] depth the depth level of the last `path` key
* @return the @ref jsmnf_pair `head`'s field matched to `path`, or NULL if
* not encountered
*/
JSMN_API const jsmnf_pair *jsmnf_find_path(const jsmnf_pair *const head,
char *const path[],
unsigned depth);
/**
* @brief Populate and automatically allocate the @ref jsmnf_pair pairs from
* jsmn tokens
* @brief jsmnf_load() counterpart that automatically allocates the necessary
* amount of pairs necessary for sorting the JSON tokens
*
* @param[in,out] loader the @ref jsmnf_loader initialized with jsmnf_init()
* @param[in] js the JSON data string
* @param[in] len the raw JSON string length
* @param[out] p_table pointer to @ref jsmnf_table to be dynamically increased
* @note must be `free()`'d once done being used
* @param[in,out] table_len maximum amount of pairs provided
* @return a `enum jsmnerr` value for error or the amount of `pairs` used
*/
JSMN_API long jsmnf_load_auto(jsmnf_loader *loader,
const char js[],
const size_t len,
jsmnf_table **p_table,
size_t *num_pairs);
/**
* @brief `jsmn_parse()` counterpart that automatically allocates the necessary
* amount of tokens necessary for parsing the JSON string
*
* @param[in,out] parser the `jsmn_parser` initialized with `jsmn_init()`
* @param[in] js the JSON data string
* @param[in] len the raw JSON string length
* @param[out] p_tokens pointer to `jsmntok_t` to be dynamically increased
* @note must be `free()`'d once done being used
* @param[in,out] num_tokens amount of tokens
* @return a `enum jsmnerr` value for error or the amount of `tokens` used
*/
JSMN_API long jsmn_parse_auto(jsmn_parser *parser,
const char js[],
const size_t len,
jsmntok_t **p_tokens,
unsigned *num_tokens);
/**
* @brief Utility function for unescaping a Unicode string
*
* @param[out] buf destination buffer
* @param[in] bufsize destination buffer size
* @param[in] src source string to be unescaped
* @param[in] length source string length
* @return length of unescaped string if successful or a negative jsmn error
* code on failure
*/
JSMN_API long jsmnf_unescape(char buf[],
size_t bufsize,
const char src[],
size_t length);
#ifndef JSMN_HEADER
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define OA_HASH_STATIC
#include "oa_hash.h"
#undef OA_HASH_STATIC
struct _jsmnf_pair_mut {
JSMNF_PAIR_ATTRS(mut);
};
JSMN_API void
jsmnf_init(jsmnf_loader *loader)
{
jsmn_init(&loader->parser);
loader->pairnext = 0;
}
static long
_jsmnf_load_pairs(struct jsmnf_loader *loader,
const char js[],
struct _jsmnf_pair_mut *curr,
const size_t num_tokens,
struct _jsmnf_pair_mut pairs[],
struct oa_hash_entry buckets[],
const size_t table_len)
{
int offset = 0;
if (!num_tokens) return 0;
switch (curr->v->type) {
case JSMN_STRING:
case JSMN_PRIMITIVE:
break;
case JSMN_OBJECT:
case JSMN_ARRAY: {
const unsigned value_size = (unsigned)curr->v->size,
top_idx = loader->pairnext + (1 + value_size),
bottom_idx = loader->pairnext;
int ret;
if (value_size > (table_len - bottom_idx)
|| top_idx > (table_len - bottom_idx))
{
return JSMN_ERROR_NOMEM;
}
loader->pairnext = top_idx;
oa_hash_init((struct oa_hash *)curr, &buckets[bottom_idx],
top_idx - bottom_idx);
if (curr == NULL) {
abort();
}
if (JSMN_OBJECT == curr->v->type) {
while (curr->length < value_size) {
struct _jsmnf_pair_mut *fields = pairs + bottom_idx,
*element = fields + curr->length;
element->k = curr->v + 1 + (offset++);
if (element->k->size > 0) {
element->v = curr->v + 1 + offset;
oa_hash_set((struct oa_hash *)curr, js + element->k->start,
element->k->end - element->k->start, element);
if ((ret = _jsmnf_load_pairs(loader, js, element,
num_tokens - offset, pairs,
buckets, table_len))
< 0)
{
return ret;
}
curr->fields = (struct jsmnf_pair *)fields;
offset += ret;
}
else {
oa_hash_set((struct oa_hash *)curr, js + element->k->start,
element->k->end - element->k->start, NULL);
}
}
}
else if (JSMN_ARRAY == curr->v->type) {
for (; curr->length < value_size; ++curr->length) {
static jsmntok_t empty_key = { 0 };
struct oa_hash_entry *entry = curr->buckets + curr->length;
struct _jsmnf_pair_mut *fields = pairs + bottom_idx,
*element = fields + curr->length;
entry->state = OA_HASH_ENTRY_OCCUPIED;
entry->value = element;
element->v = curr->v + 1 + offset;
element->k = &empty_key;
if ((ret = _jsmnf_load_pairs(loader, js, element,
num_tokens - offset, pairs,
buckets, table_len))
< 0)
{
return ret;
}
curr->fields = (struct jsmnf_pair *)fields;
offset += ret;
}
}
break;
}
default:
case JSMN_UNDEFINED:
return JSMN_ERROR_INVAL;
}
return offset + 1;
}
JSMN_API long
jsmnf_load(struct jsmnf_loader *loader,
const char js[],
const size_t len,
struct jsmnf_table table[],
const size_t table_len)
{
struct jsmntok *tokens = (struct jsmntok *)table;
struct _jsmnf_pair_mut
*pairs = (struct _jsmnf_pair_mut *)(((char *)tokens)
+ (table_len * sizeof *tokens)),
*mut_root = &pairs[0];
struct oa_hash_entry *buckets =
(struct oa_hash_entry *)(((char *)pairs)
+ (table_len * sizeof *pairs));
long ret;
if (loader->pairnext == 0) { /* first run, initialize pairs */
/* initialize tokens if not already initialized */
if (loader->parser.toknext == 0) {
memset(tokens, 0, table_len * sizeof *tokens);
if ((ret = jsmn_parse(&loader->parser, js, len, tokens, table_len))
< 0)
{
return jsmn_init(&loader->parser), ret;
}
}
memset(pairs, 0, table_len * sizeof *pairs);
memset(buckets, 0, table_len * sizeof *buckets);
mut_root->v = tokens + loader->pairnext++;
loader->root = (struct jsmnf_pair *)mut_root;
}
if ((ret = _jsmnf_load_pairs(loader, js, mut_root, loader->parser.toknext,
pairs, buckets, table_len))
< 0)
{
/* TODO: rather than reseting pairnext keep the last 'bucket' ptr
* stored, so it can continue from there in the next try */
loader->pairnext = 0;
loader->root = NULL;
}
return ret;
}
JSMN_API const struct jsmnf_pair *
jsmnf_find(const struct jsmnf_pair *head,
const char key[],
const size_t length)
{
if (!head || !head->v) return NULL;
if (!key && !length) return head;
if (JSMN_OBJECT == head->v->type) {
return oa_hash_get((struct oa_hash *)head, key, length);
}
if (JSMN_ARRAY == head->v->type) {
char *endptr;
const unsigned idx = (unsigned)strtoul(key, &endptr, 10);
if (endptr != key && (idx < head->length)
&& head->buckets[idx].state == OA_HASH_ENTRY_OCCUPIED)
{
return head->buckets[idx].value;
}
}
return NULL;
}
JSMN_API const struct jsmnf_pair *
jsmnf_find_path(const struct jsmnf_pair *head,
char *const path[],
unsigned depth)
{
const struct jsmnf_pair *iter = head, *found = NULL;
unsigned i;
for (i = 0; i < depth; ++i) {
if (!iter || !(found = jsmnf_find(iter, path[i], strlen(path[i]))))
break;
iter = found;
}
return found;
}
#define RECALLOC_OR_ERROR(ptr, prev_size) \
do { \
const unsigned new_size = *(prev_size) * 2; \
void *tmp = realloc((ptr), new_size * sizeof *(ptr)); \
if (!tmp) return JSMN_ERROR_NOMEM; \
(ptr) = tmp; \
memset((ptr) + *(prev_size), 0, \
(new_size - *(prev_size)) * sizeof *(ptr)); \
*(prev_size) = new_size; \
} while (0)
JSMN_API long
jsmn_parse_auto(struct jsmn_parser *parser,
const char js[],
const size_t len,
struct jsmntok **p_tokens,
unsigned *num_tokens)
{
int ret;
if (NULL == *p_tokens || 0 == *num_tokens) {
*p_tokens = calloc(1, sizeof **p_tokens);
*num_tokens = 1;
}
while ((ret = jsmn_parse(parser, js, len, *p_tokens, *num_tokens))
== JSMN_ERROR_NOMEM)
{
RECALLOC_OR_ERROR(*p_tokens, num_tokens);
}
return ret;
}
JSMN_API long
jsmnf_load_auto(struct jsmnf_loader *loader,
const char js[],
const size_t len,
struct jsmnf_table **p_table,
size_t *table_len)
{
int ret;
if (NULL == *p_table || 0 == *table_len) {
if (!(*p_table = calloc(1, sizeof **p_table))) {
return JSMN_ERROR_NOMEM;
}
*table_len = 1;
}
while ((ret = jsmnf_load(loader, js, len, *p_table, *table_len))
== JSMN_ERROR_NOMEM)
{
RECALLOC_OR_ERROR(*p_table, table_len);
}
return ret;
}
#undef RECALLOC_OR_ERROR
static int
_jsmnf_read_4_digits(char *s, const char *end, unsigned *p_hex)
{
char buf[5] = { 0 };
int i;
if (end - s < 4) return JSMN_ERROR_PART;
for (i = 0; i < 4; i++) {
buf[i] = s[i];
if (('0' <= s[i] && s[i] <= '9') || ('A' <= s[i] && s[i] <= 'F')
|| ('a' <= s[i] && s[i] <= 'f'))
{
continue;
}
return JSMN_ERROR_INVAL;
}
*p_hex = (unsigned)strtoul(buf, NULL, 16);
return 4;
}
#define _JSMNF_UTF16_IS_FIRST_SURROGATE(c) \
(0xD800 <= (unsigned)c && (unsigned)c <= 0xDBFF)
#define _JSMNF_UTF16_IS_SECOND_SURROGATE(c) \
(0xDC00 <= (unsigned)c && (unsigned)c <= 0xDFFF)
#define _JSMNF_UTF16_JOIN_SURROGATE(c1, c2) \
(((((unsigned long)c1 & 0x3FF) << 10) | ((unsigned)c2 & 0x3FF)) + 0x10000)
#define _JSMNF_UTF8_IS_VALID(c) \
(((unsigned long)c <= 0x10FFFF) \
&& ((unsigned long)c < 0xD800 || (unsigned long)c > 0xDFFF))
#define _JSMNF_UTF8_IS_TRAIL(c) (((unsigned char)c & 0xC0) == 0x80)
#define _JSMNF_UTF_ILLEGAL 0xFFFFFFFFu
static int
_jsmnf_utf8_trail_length(unsigned char c)
{
if (c < 128) return 0;
if (c < 194) return -1;
if (c < 224) return 1;
if (c < 240) return 2;
if (c <= 244) return 3;
return -1;
}
static int
_jsmnf_utf8_width(unsigned long value)
{
if (value <= 0x7F) return 1;
if (value <= 0x7FF) return 2;
if (value <= 0xFFFF) return 3;
return 4;
}
/* See RFC 3629
Based on: http://www.w3.org/International/questions/qa-forms-utf-8 */
static unsigned long
_jsmnf_utf8_next(char **p, const char *end)
{
unsigned char lead, tmp;
int trail_size;
unsigned long c;
if (*p == end) return _JSMNF_UTF_ILLEGAL;
lead = **p;
(*p)++;
/* First byte is fully validated here */
trail_size = _jsmnf_utf8_trail_length(lead);
if (trail_size < 0) return _JSMNF_UTF_ILLEGAL;
/* Ok as only ASCII may be of size = 0 also optimize for ASCII text */
if (trail_size == 0) return lead;
c = lead & ((1 << (6 - trail_size)) - 1);
/* Read the rest */
switch (trail_size) {
case 3:
if (*p == end) return _JSMNF_UTF_ILLEGAL;
tmp = **p;
(*p)++;
if (!_JSMNF_UTF8_IS_TRAIL(tmp)) return _JSMNF_UTF_ILLEGAL;
c = (c << 6) | (tmp & 0x3F);
/* fall-through */
case 2:
if (*p == end) return _JSMNF_UTF_ILLEGAL;
tmp = **p;
(*p)++;
if (!_JSMNF_UTF8_IS_TRAIL(tmp)) return _JSMNF_UTF_ILLEGAL;
c = (c << 6) | (tmp & 0x3F);
/* fall-through */
case 1:
if (*p == end) return _JSMNF_UTF_ILLEGAL;
tmp = **p;
(*p)++;
if (!_JSMNF_UTF8_IS_TRAIL(tmp)) return _JSMNF_UTF_ILLEGAL;
c = (c << 6) | (tmp & 0x3F);
}
/* Check code point validity: no surrogates and valid range */
if (!_JSMNF_UTF8_IS_VALID(c)) return _JSMNF_UTF_ILLEGAL;
/* make sure it is the most compact representation */
if (_jsmnf_utf8_width(c) != trail_size + 1) return _JSMNF_UTF_ILLEGAL;
return c;
}
static long
_jsmnf_utf8_validate(char *p, const char *end)
{
const char *start = p;
while (p != end) {
if (_jsmnf_utf8_next(&p, end) == _JSMNF_UTF_ILLEGAL)
return JSMN_ERROR_INVAL;
}
return (long)(end - start);
}
static unsigned
_jsmnf_utf8_encode(unsigned long value, char utf8_seq[4])
{
if (value <= 0x7F) {
utf8_seq[0] = value;
return 1;
}
if (value <= 0x7FF) {
utf8_seq[0] = (value >> 6) | 0xC0;
utf8_seq[1] = (value & 0x3F) | 0x80;
return 2;
}
if (value <= 0xFFFF) {
utf8_seq[0] = (value >> 12) | 0xE0;
utf8_seq[1] = ((value >> 6) & 0x3F) | 0x80;
utf8_seq[2] = (value & 0x3F) | 0x80;
return 3;
}
utf8_seq[0] = (value >> 18) | 0xF0;
utf8_seq[1] = ((value >> 12) & 0x3F) | 0x80;
utf8_seq[2] = ((value >> 6) & 0x3F) | 0x80;
utf8_seq[3] = (value & 0x3F) | 0x80;
return 4;
}
static int
_jsmnf_utf8_append(unsigned long hex, char *buf_tok, const char *buf_end)
{
char utf8_seq[4];
unsigned utf8_seqlen = _jsmnf_utf8_encode(hex, utf8_seq);
unsigned i;
if ((buf_tok + utf8_seqlen) >= buf_end) return JSMN_ERROR_NOMEM;
for (i = 0; i < utf8_seqlen; ++i)
buf_tok[i] = utf8_seq[i];
return utf8_seqlen;
}
#define BUF_PUSH(buf_tok, c, buf_end) \
do { \
if (buf_tok >= buf_end) return JSMN_ERROR_NOMEM; \
*buf_tok++ = c; \
} while (0)
JSMN_API long
jsmnf_unescape(char buf[], size_t bufsize, const char src[], size_t len)
{
char *src_tok = (char *)src, *const src_end = src_tok + len;
char *buf_tok = buf, *const buf_end = buf + bufsize;
int second_surrogate_expected = 0;
unsigned first_surrogate = 0;
while (*src_tok && src_tok < src_end) {
char c = *src_tok++;
if (0 <= c && c <= 0x1F) return JSMN_ERROR_INVAL;
if (c != '\\') {
if (second_surrogate_expected) return JSMN_ERROR_INVAL;
BUF_PUSH(buf_tok, c, buf_end);
continue;
}
/* expects escaping but src is a well-formed string */
if (!*src_tok || src_tok >= src_end) return JSMN_ERROR_PART;
c = *src_tok++;
if (second_surrogate_expected && c != 'u') return JSMN_ERROR_INVAL;
switch (c) {
case '"':
case '\\':
case '/':
BUF_PUSH(buf_tok, c, buf_end);
break;
case 'b':
BUF_PUSH(buf_tok, '\b', buf_end);
break;
case 'f':
BUF_PUSH(buf_tok, '\f', buf_end);
break;
case 'n':
BUF_PUSH(buf_tok, '\n', buf_end);
break;
case 'r':
BUF_PUSH(buf_tok, '\r', buf_end);
break;
case 't':
BUF_PUSH(buf_tok, '\t', buf_end);
break;
case 'u': {
unsigned hex;
int ret = _jsmnf_read_4_digits(src_tok, src_end, &hex);
if (ret != 4) return ret;
src_tok += ret;
if (second_surrogate_expected) {
if (!_JSMNF_UTF16_IS_SECOND_SURROGATE(hex))
return JSMN_ERROR_INVAL;
ret = _jsmnf_utf8_append(
_JSMNF_UTF16_JOIN_SURROGATE(first_surrogate, hex), buf_tok,
buf_end);
if (ret < 0) return ret;
buf_tok += ret;
second_surrogate_expected = 0;
}
else if (_JSMNF_UTF16_IS_FIRST_SURROGATE(hex)) {
second_surrogate_expected = 1;
first_surrogate = hex;
}
else {
ret = _jsmnf_utf8_append(hex, buf_tok, buf_end);
if (ret < 0) return ret;
buf_tok += ret;
}
} break;
default:
return JSMN_ERROR_INVAL;
}
}
return _jsmnf_utf8_validate(buf, buf_tok);
}
#undef BUF_PUSH
#endif /* JSMN_HEADER */
#endif /* JSMN_H */
#undef JSMNF_PAIR_ATTRS_const
#undef JSMNF_PAIR_ATTRS_mut
#undef JSMNF_PAIR_ATTRS
#ifdef __cplusplus
}
#endif
#endif /* JSMN_FIND_H */
+471
View File
@@ -0,0 +1,471 @@
/*
* MIT License
*
* Copyright (c) 2010 Serge Zaitsev
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#ifndef JSMN_H
#define JSMN_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#ifdef JSMN_STATIC
#define JSMN_API static
#else
#define JSMN_API extern
#endif
/**
* JSON type identifier. Basic types are:
* o Object
* o Array
* o String
* o Other primitive: number, boolean (true/false) or null
*/
typedef enum {
JSMN_UNDEFINED = 0,
JSMN_OBJECT = 1,
JSMN_ARRAY = 2,
JSMN_STRING = 3,
JSMN_PRIMITIVE = 4
} jsmntype_t;
enum jsmnerr {
/* Not enough tokens were provided */
JSMN_ERROR_NOMEM = -1,
/* Invalid character inside JSON string */
JSMN_ERROR_INVAL = -2,
/* The string is not a full JSON packet, more bytes expected */
JSMN_ERROR_PART = -3
};
/**
* JSON token description.
* type type (object, array, string etc.)
* start start position in JSON data string
* end end position in JSON data string
*/
typedef struct jsmntok {
jsmntype_t type;
int start;
int end;
int size;
#ifdef JSMN_PARENT_LINKS
int parent;
#endif
} jsmntok_t;
/**
* JSON parser. Contains an array of token blocks available. Also stores
* the string being parsed now and current position in that string.
*/
typedef struct jsmn_parser {
unsigned int pos; /* offset in the JSON string */
unsigned int toknext; /* next token to allocate */
int toksuper; /* superior token node, e.g. parent object or array */
} jsmn_parser;
/**
* Create JSON parser over an array of tokens
*/
JSMN_API void jsmn_init(jsmn_parser *parser);
/**
* Run JSON parser. It parses a JSON data string into and array of tokens, each
* describing
* a single JSON object.
*/
JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len,
jsmntok_t *tokens, const unsigned int num_tokens);
#ifndef JSMN_HEADER
/**
* Allocates a fresh unused token from the token pool.
*/
static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, jsmntok_t *tokens,
const size_t num_tokens) {
jsmntok_t *tok;
if (parser->toknext >= num_tokens) {
return NULL;
}
tok = &tokens[parser->toknext++];
tok->start = tok->end = -1;
tok->size = 0;
#ifdef JSMN_PARENT_LINKS
tok->parent = -1;
#endif
return tok;
}
/**
* Fills token type and boundaries.
*/
static void jsmn_fill_token(jsmntok_t *token, const jsmntype_t type,
const int start, const int end) {
token->type = type;
token->start = start;
token->end = end;
token->size = 0;
}
/**
* Fills next available token with JSON primitive.
*/
static int jsmn_parse_primitive(jsmn_parser *parser, const char *js,
const size_t len, jsmntok_t *tokens,
const size_t num_tokens) {
jsmntok_t *token;
int start;
start = parser->pos;
for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
switch (js[parser->pos]) {
#ifndef JSMN_STRICT
/* In strict mode primitive must be followed by "," or "}" or "]" */
case ':':
#endif
case '\t':
case '\r':
case '\n':
case ' ':
case ',':
case ']':
case '}':
goto found;
default:
/* to quiet a warning from gcc*/
break;
}
if (js[parser->pos] < 32 || js[parser->pos] >= 127) {
parser->pos = start;
return JSMN_ERROR_INVAL;
}
}
#ifdef JSMN_STRICT
/* In strict mode primitive must be followed by a comma/object/array */
parser->pos = start;
return JSMN_ERROR_PART;
#endif
found:
if (tokens == NULL) {
parser->pos--;
return 0;
}
token = jsmn_alloc_token(parser, tokens, num_tokens);
if (token == NULL) {
parser->pos = start;
return JSMN_ERROR_NOMEM;
}
jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos);
#ifdef JSMN_PARENT_LINKS
token->parent = parser->toksuper;
#endif
parser->pos--;
return 0;
}
/**
* Fills next token with JSON string.
*/
static int jsmn_parse_string(jsmn_parser *parser, const char *js,
const size_t len, jsmntok_t *tokens,
const size_t num_tokens) {
jsmntok_t *token;
int start = parser->pos;
parser->pos++;
/* Skip starting quote */
for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
char c = js[parser->pos];
/* Quote: end of string */
if (c == '\"') {
if (tokens == NULL) {
return 0;
}
token = jsmn_alloc_token(parser, tokens, num_tokens);
if (token == NULL) {
parser->pos = start;
return JSMN_ERROR_NOMEM;
}
jsmn_fill_token(token, JSMN_STRING, start + 1, parser->pos);
#ifdef JSMN_PARENT_LINKS
token->parent = parser->toksuper;
#endif
return 0;
}
/* Backslash: Quoted symbol expected */
if (c == '\\' && parser->pos + 1 < len) {
int i;
parser->pos++;
switch (js[parser->pos]) {
/* Allowed escaped symbols */
case '\"':
case '/':
case '\\':
case 'b':
case 'f':
case 'r':
case 'n':
case 't':
break;
/* Allows escaped symbol \uXXXX */
case 'u':
parser->pos++;
for (i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0';
i++) {
/* If it isn't a hex character we have an error */
if (!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */
(js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */
(js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */
parser->pos = start;
return JSMN_ERROR_INVAL;
}
parser->pos++;
}
parser->pos--;
break;
/* Unexpected symbol */
default:
parser->pos = start;
return JSMN_ERROR_INVAL;
}
}
}
parser->pos = start;
return JSMN_ERROR_PART;
}
/**
* Parse JSON string and fill tokens.
*/
JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len,
jsmntok_t *tokens, const unsigned int num_tokens) {
int r;
int i;
jsmntok_t *token;
int count = parser->toknext;
for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
char c;
jsmntype_t type;
c = js[parser->pos];
switch (c) {
case '{':
case '[':
count++;
if (tokens == NULL) {
break;
}
token = jsmn_alloc_token(parser, tokens, num_tokens);
if (token == NULL) {
return JSMN_ERROR_NOMEM;
}
if (parser->toksuper != -1) {
jsmntok_t *t = &tokens[parser->toksuper];
#ifdef JSMN_STRICT
/* In strict mode an object or array can't become a key */
if (t->type == JSMN_OBJECT) {
return JSMN_ERROR_INVAL;
}
#endif
t->size++;
#ifdef JSMN_PARENT_LINKS
token->parent = parser->toksuper;
#endif
}
token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY);
token->start = parser->pos;
parser->toksuper = parser->toknext - 1;
break;
case '}':
case ']':
if (tokens == NULL) {
break;
}
type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY);
#ifdef JSMN_PARENT_LINKS
if (parser->toknext < 1) {
return JSMN_ERROR_INVAL;
}
token = &tokens[parser->toknext - 1];
for (;;) {
if (token->start != -1 && token->end == -1) {
if (token->type != type) {
return JSMN_ERROR_INVAL;
}
token->end = parser->pos + 1;
parser->toksuper = token->parent;
break;
}
if (token->parent == -1) {
if (token->type != type || parser->toksuper == -1) {
return JSMN_ERROR_INVAL;
}
break;
}
token = &tokens[token->parent];
}
#else
for (i = parser->toknext - 1; i >= 0; i--) {
token = &tokens[i];
if (token->start != -1 && token->end == -1) {
if (token->type != type) {
return JSMN_ERROR_INVAL;
}
parser->toksuper = -1;
token->end = parser->pos + 1;
break;
}
}
/* Error if unmatched closing bracket */
if (i == -1) {
return JSMN_ERROR_INVAL;
}
for (; i >= 0; i--) {
token = &tokens[i];
if (token->start != -1 && token->end == -1) {
parser->toksuper = i;
break;
}
}
#endif
break;
case '\"':
r = jsmn_parse_string(parser, js, len, tokens, num_tokens);
if (r < 0) {
return r;
}
count++;
if (parser->toksuper != -1 && tokens != NULL) {
tokens[parser->toksuper].size++;
}
break;
case '\t':
case '\r':
case '\n':
case ' ':
break;
case ':':
parser->toksuper = parser->toknext - 1;
break;
case ',':
if (tokens != NULL && parser->toksuper != -1 &&
tokens[parser->toksuper].type != JSMN_ARRAY &&
tokens[parser->toksuper].type != JSMN_OBJECT) {
#ifdef JSMN_PARENT_LINKS
parser->toksuper = tokens[parser->toksuper].parent;
#else
for (i = parser->toknext - 1; i >= 0; i--) {
if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) {
if (tokens[i].start != -1 && tokens[i].end == -1) {
parser->toksuper = i;
break;
}
}
}
#endif
}
break;
#ifdef JSMN_STRICT
/* In strict mode primitives are: numbers and booleans */
case '-':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case 't':
case 'f':
case 'n':
/* And they must not be keys of the object */
if (tokens != NULL && parser->toksuper != -1) {
const jsmntok_t *t = &tokens[parser->toksuper];
if (t->type == JSMN_OBJECT ||
(t->type == JSMN_STRING && t->size != 0)) {
return JSMN_ERROR_INVAL;
}
}
#else
/* In non-strict mode every unquoted value is a primitive */
default:
#endif
r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens);
if (r < 0) {
return r;
}
count++;
if (parser->toksuper != -1 && tokens != NULL) {
tokens[parser->toksuper].size++;
}
break;
#ifdef JSMN_STRICT
/* Unexpected char in strict mode */
default:
return JSMN_ERROR_INVAL;
#endif
}
}
if (tokens != NULL) {
for (i = parser->toknext - 1; i >= 0; i--) {
/* Unmatched opened object or array */
if (tokens[i].start != -1 && tokens[i].end == -1) {
return JSMN_ERROR_PART;
}
}
}
return count;
}
/**
* Creates a new parser based over a given buffer with an array of tokens
* available.
*/
JSMN_API void jsmn_init(jsmn_parser *parser) {
parser->pos = 0;
parser->toknext = 0;
parser->toksuper = -1;
}
#endif /* JSMN_HEADER */
#ifdef __cplusplus
}
#endif
#endif /* JSMN_H */
+957
View File
@@ -0,0 +1,957 @@
/*
* Special thanks to Christopher Wellons (aka skeeto) for giving valuable
* feedback that helped improve this lib.
*
* See: https://www.reddit.com/r/C_Programming/comments/sf95m3/comment/huojrjn
*/
#ifndef JSON_BUILD_H
#define JSON_BUILD_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#ifdef JSONB_STATIC
#define JSONB_API static
#else
#define JSONB_API extern
#endif
#ifndef JSONB_MAX_DEPTH
/**
* Maximum JSON nesting depth, if default value is unwanted then it should be
* defined before json-build.h is included:
*
* #define JSONB_MAX_DEPTH 256
* #include "json-build.h"
*/
#define JSONB_MAX_DEPTH 128
#endif /* JSONB_MAX_DEPTH */
/** @brief json-builder return codes */
typedef enum jsonbcode {
/** no error, operation was a success */
JSONB_OK = 0,
/** string is complete, expects no more inputs */
JSONB_END,
/** not enough tokens were provided */
JSONB_ERROR_NOMEM = -1,
/** token doesn't match expected value */
JSONB_ERROR_INPUT = -2,
/** operation would lead to out of boundaries access */
JSONB_ERROR_STACK = -3,
/** buffer overflow */
JSONB_ERROR_OVERFLOW = -4
} jsonbcode;
/** @brief json-builder serializing state */
enum jsonbstate {
JSONB_INIT = 0,
JSONB_ARRAY_OR_OBJECT_OR_VALUE = JSONB_INIT,
JSONB_OBJECT_KEY_OR_CLOSE,
JSONB_OBJECT_VALUE,
JSONB_OBJECT_NEXT_KEY_OR_CLOSE,
JSONB_ARRAY_VALUE_OR_CLOSE,
JSONB_ARRAY_NEXT_VALUE_OR_CLOSE,
JSONB_ERROR,
JSONB_DONE
};
/** @brief Handle for building a JSON string */
typedef struct jsonb {
/** state stack to keep track and enforce next inputs */
enum jsonbstate stack[JSONB_MAX_DEPTH + 1];
/** pointer to stack top */
enum jsonbstate *top;
/** offset in the JSON buffer (current length) */
size_t pos;
} jsonb;
/**
* @brief Reset a jsonb handle buffer's position tracker
* (for streaming purposes)
* @note Should be used in conjunction with @ref JSONB_ERROR_NOMEM if the
* buffer is meant to be used as a stream
*
* @param builder pointer to the @ref jsonb handle
*/
#define jsonb_reset(builder) ((builder)->pos = 0)
/**
* @brief Initialize a jsonb handle
*
* @param builder the handle to be initialized
*/
JSONB_API void jsonb_init(jsonb *builder);
/**
* @brief Push an object to the builder
*
* @param builder the builder initialized with jsonb_init()
* @param buf the JSON buffer
* @param bufsize the JSON buffer size
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_object(jsonb *builder, char buf[], size_t bufsize);
/**
* @brief @ref jsonb_object() with dynamic buffer
*
* @param builder the builder initialized with jsonb_init()
* @param p_buf pointer to the JSON buffer
* @param p_bufsize pointer the JSON buffer size
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_object_auto(jsonb *builder,
char *p_buf[],
size_t *p_bufsize);
/**
* @brief Pop an object from the builder
*
* @param builder the builder initialized with jsonb_init()
* @param buf the JSON buffer
* @param bufsize the JSON buffer size
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_object_pop(jsonb *builder,
char buf[],
size_t bufsize);
/**
* @brief @ref jsonb_object_pop() with dynamic buffer
*
* @param builder the builder initialized with jsonb_init()
* @param p_buf pointer to the JSON buffer
* @param p_bufsize pointer the JSON buffer size
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_object_pop_auto(jsonb *builder,
char *p_buf[],
size_t *p_bufsize);
/**
* @brief Push a key to the builder
*
* @param builder the builder initialized with jsonb_init()
* @param buf the JSON buffer
* @param bufsize the JSON buffer size
* @param key the key to be inserted
* @param len the key length
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_key(
jsonb *builder, char buf[], size_t bufsize, const char key[], size_t len);
/**
* @brief @ref jsonb_key() with dynamic buffer
*
* @param builder the builder initialized with jsonb_init()
* @param p_buf pointer to the JSON buffer
* @param p_bufsize pointer to the JSON buffer size
* @param key the key to be inserted
* @param len the key length
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_key_auto(jsonb *builder,
char *p_buf[],
size_t *p_bufsize,
const char key[],
size_t len);
/**
* @brief Push an array to the builder
*
* @param builder the builder initialized with jsonb_init()
* @param buf the JSON buffer
* @param bufsize the JSON buffer size
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_array(jsonb *builder, char buf[], size_t bufsize);
/**
* @brief @ref jsonb_array() with dynamic buffer
*
* @param builder the builder initialized with jsonb_init()
* @param p_buf pointer to the JSON buffer
* @param p_bufsize pointer to the JSON buffer size
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_array_auto(jsonb *builder,
char *p_buf[],
size_t *p_bufsize);
/**
* @brief Pop an array from the builder
*
* @param builder the builder initialized with jsonb_init()
* @param buf the JSON buffer
* @param bufsize the JSON buffer size
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_array_pop(jsonb *builder,
char buf[],
size_t bufsize);
/**
* @brief @ref jsonb_array_pop() with dynamic buffer
*
* @param builder the builder initialized with jsonb_init()
* @param p_buf pointer to the JSON buffer
* @param p_bufsize pointer to the JSON buffer size
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_array_pop_auto(jsonb *builder,
char *p_buf[],
size_t *p_bufsize);
/**
* @brief Push a raw JSON token to the builder
*
* @param builder the builder initialized with jsonb_init()
* @param buf the JSON buffer
* @param bufsize the JSON buffer size
* @param token the token to be inserted
* @param len the token length
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_token(jsonb *builder,
char buf[],
size_t bufsize,
const char token[],
size_t len);
/**
* @brief @ref jsonb_token() with dynamic buffer
*
* @param builder the builder initialized with jsonb_init()
* @param p_buf pointer to the JSON buffer
* @param p_bufsize pointer to the JSON buffer size
* @param token the token to be inserted
* @param len the token length
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_token_auto(jsonb *builder,
char *p_buf[],
size_t *p_bufsize,
const char token[],
size_t len);
/**
* @brief Push a boolean token to the builder
*
* @param builder the builder initialized with jsonb_init()
* @param buf the JSON buffer
* @param bufsize the JSON buffer size
* @param boolean the boolean to be inserted
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_bool(jsonb *builder,
char buf[],
size_t bufsize,
int boolean);
/**
* @brief @ref jsonb_bool() with dynamic buffer
*
* @param builder the builder initialized with jsonb_init()
* @param p_buf pointer to the JSON buffer
* @param p_bufsize pointer to the JSON buffer size
* @param boolean the boolean to be inserted
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_bool_auto(jsonb *builder,
char *p_buf[],
size_t *p_bufsize,
int boolean);
/**
* @brief Push a null token to the builder
*
* @param builder the builder initialized with jsonb_init()
* @param buf the JSON buffer
* @param bufsize the JSON buffer size
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_null(jsonb *builder, char buf[], size_t bufsize);
/**
* @brief @ref jsonb_null() with dynamic buffer
*
* @param builder the builder initialized with jsonb_init()
* @param p_buf pointer to the JSON buffer
* @param p_bufsize pointer to the JSON buffer size
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_null_auto(jsonb *builder,
char *p_buf[],
size_t *p_bufsize);
/**
* @brief Push a string token to the builder
*
* @param builder the builder initialized with jsonb_init()
* @param buf the JSON buffer
* @param bufsize the JSON buffer size
* @param str the string to be inserted
* @param len the string length
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_string(
jsonb *builder, char buf[], size_t bufsize, const char str[], size_t len);
/**
* @brief @ref jsonb_string() with dynamic buffer
*
* @param builder the builder initialized with jsonb_init()
* @param p_buf pointer to the JSON buffer
* @param p_bufsize pointer to the JSON buffer size
* @param str the string to be inserted
* @param len the string length
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_string_auto(jsonb *builder,
char *p_buf[],
size_t *p_bufsize,
const char str[],
size_t len);
/**
* @brief Push a number token to the builder
*
* @param builder the builder initialized with jsonb_init()
* @param buf the JSON buffer
* @param bufsize the JSON buffer size
* @param number the number to be inserted
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_number(jsonb *builder,
char buf[],
size_t bufsize,
double number);
/**
* @brief @ref jsonb_number() with dynamic buffer
*
* @param builder the builder initialized with jsonb_init()
* @param p_buf pointer to the JSON buffer
* @param p_bufsize pointer to the JSON buffer size
* @param number the number to be inserted
* @return @ref jsonbcode value
*/
JSONB_API jsonbcode jsonb_number_auto(jsonb *builder,
char *p_buf[],
size_t *p_bufsize,
double number);
#ifndef JSONB_HEADER
#include <stdio.h>
#include <stdlib.h>
#ifndef JSONB_DEBUG
#define TRACE(prev, next) next
#define DECORATOR(a)
#else
static const char *
_jsonb_eval_state(enum jsonbstate state)
{
switch (state) {
case JSONB_ARRAY_OR_OBJECT_OR_VALUE: return "array or object or value";
case JSONB_OBJECT_KEY_OR_CLOSE: return "object key or close";
case JSONB_OBJECT_NEXT_KEY_OR_CLOSE: return "object next key or close";
case JSONB_OBJECT_VALUE: return "object value";
case JSONB_ARRAY_VALUE_OR_CLOSE: return "array value or close";
case JSONB_ARRAY_NEXT_VALUE_OR_CLOSE: return "array next value or close";
case JSONB_ERROR: return "error";
case JSONB_DONE: return "done";
default: return "unknown";
}
}
#define TRACE(prev, next) \
do { \
enum jsonbstate _prev = prev, _next = next; \
fprintf(stderr, "%s():L%d | %s -> %s\n", __func__, __LINE__, \
_jsonb_eval_state(_prev), _jsonb_eval_state(_next)); \
} while (0)
#define DECORATOR(d) d
#endif /* JSONB_DEBUG */
#define STACK_HEAD(b, state) *(b)->top = (state)
#define STACK_PUSH(b, state) TRACE(*(b)->top, *++(b)->top = (state))
#define STACK_POP(b) TRACE(*(b)->top, DECORATOR(*)--(b)->top)
#define BUFFER_COPY_CHAR_STATIC(b, c, _pos, buf, bufsize) \
do { \
if ((b)->pos + (_pos) + 1 + 1 > (bufsize)) { \
(buf)[(b)->pos] = '\0'; \
return JSONB_ERROR_NOMEM; \
} \
(buf)[(b)->pos + (_pos)++] = (c); \
(buf)[(b)->pos + (_pos)] = '\0'; \
} while (0)
#define BUFFER_COPY_STATIC(b, value, len, _pos, buf, bufsize) \
do { \
size_t i; \
if ((b)->pos + (_pos) + (len) + 1 > (bufsize)) { \
(buf)[(b)->pos] = '\0'; \
return JSONB_ERROR_NOMEM; \
} \
for (i = 0; i < (len); ++i) \
(buf)[(b)->pos + (_pos) + i] = (value)[i]; \
(_pos) += (len); \
(buf)[(b)->pos + (_pos)] = '\0'; \
} while (0)
#define BUFFER_COPY_CHAR_REALLOC(b, c, _pos, p_buf, p_bufsize) \
do { \
if ((b)->pos + (_pos) + 1 + 1 > *p_bufsize) { \
char *new_buf = NULL; \
const size_t needed = (b)->pos + (_pos) + 1 + 1; \
size_t new_size = *p_bufsize + (*p_bufsize >> 1); /* 1.5x */ \
if (new_size < needed) new_size = needed; \
if (new_size < *p_bufsize) return JSONB_ERROR_OVERFLOW; \
new_buf = realloc(*p_buf, new_size); \
if (!new_buf) return JSONB_ERROR_NOMEM; \
*p_buf = new_buf; \
*p_bufsize = new_size; \
} \
(*p_buf)[(b)->pos + (_pos)++] = (c); \
(*p_buf)[(b)->pos + (_pos)] = '\0'; \
} while (0)
#define BUFFER_COPY_REALLOC(b, value, len, _pos, p_buf, p_bufsize) \
do { \
size_t i; \
if ((b)->pos + (_pos) + (len) + 1 > *p_bufsize) { \
char *new_buf = NULL; \
const size_t needed = (b)->pos + (_pos) + (len) + 1; \
size_t new_size = *p_bufsize + (*p_bufsize >> 1); /* 1.5x */ \
if (new_size < needed) new_size = needed; \
if (new_size < *p_bufsize) return JSONB_ERROR_OVERFLOW; \
new_buf = realloc(*p_buf, new_size); \
if (!new_buf) return JSONB_ERROR_NOMEM; \
*p_buf = new_buf; \
*p_bufsize = new_size; \
} \
for (i = 0; i < (len); ++i) \
(*p_buf)[(b)->pos + (_pos) + i] = (value)[i]; \
(_pos) += (len); \
(*p_buf)[(b)->pos + (_pos)] = '\0'; \
} while (0)
JSONB_API void
jsonb_init(jsonb *b)
{
static jsonb empty_builder;
*b = empty_builder;
b->top = b->stack;
}
#define JSONB_OBJECT_EXEC(_type, buf, bufsize) \
enum jsonbstate new_state; \
size_t pos = 0; \
if (b->top - b->stack >= JSONB_MAX_DEPTH) return JSONB_ERROR_STACK; \
switch (*b->top) { \
case JSONB_ARRAY_NEXT_VALUE_OR_CLOSE: \
BUFFER_COPY_CHAR_##_type(b, ',', pos, buf, bufsize); \
/* fall-through */ \
case JSONB_ARRAY_VALUE_OR_CLOSE: \
new_state = JSONB_ARRAY_NEXT_VALUE_OR_CLOSE; \
break; \
case JSONB_OBJECT_VALUE: \
new_state = JSONB_OBJECT_NEXT_KEY_OR_CLOSE; \
break; \
case JSONB_ARRAY_OR_OBJECT_OR_VALUE: \
new_state = JSONB_DONE; \
break; \
default: \
STACK_HEAD(b, JSONB_ERROR); \
/* fall-through */ \
case JSONB_DONE: \
case JSONB_ERROR: \
return JSONB_ERROR_INPUT; \
} \
BUFFER_COPY_CHAR_##_type(b, '{', pos, buf, bufsize); \
STACK_HEAD(b, new_state); \
STACK_PUSH(b, JSONB_OBJECT_KEY_OR_CLOSE); \
b->pos += pos; \
return JSONB_OK
JSONB_API jsonbcode
jsonb_object(jsonb *b, char buf[], size_t bufsize)
{
JSONB_OBJECT_EXEC(STATIC, buf, bufsize);
}
JSONB_API jsonbcode
jsonb_object_auto(jsonb *b, char *p_buf[], size_t *p_bufsize)
{
JSONB_OBJECT_EXEC(REALLOC, p_buf, p_bufsize);
}
#define JSONB_OBJECT_POP_EXEC(_type, buf, bufsize) \
enum jsonbcode code; \
size_t pos = 0; \
switch (*b->top) { \
case JSONB_OBJECT_KEY_OR_CLOSE: \
case JSONB_OBJECT_NEXT_KEY_OR_CLOSE: \
code = b->stack == b->top - 1 ? JSONB_END : JSONB_OK; \
break; \
default: \
STACK_HEAD(b, JSONB_ERROR); \
/* fall-through */ \
case JSONB_DONE: \
case JSONB_ERROR: \
return JSONB_ERROR_INPUT; \
} \
BUFFER_COPY_CHAR_##_type(b, '}', pos, buf, bufsize); \
STACK_POP(b); \
b->pos += pos; \
return code
JSONB_API jsonbcode
jsonb_object_pop(jsonb *b, char buf[], size_t bufsize)
{
JSONB_OBJECT_POP_EXEC(STATIC, buf, bufsize);
}
JSONB_API jsonbcode
jsonb_object_pop_auto(jsonb *b, char *p_buf[], size_t *p_bufsize)
{
JSONB_OBJECT_POP_EXEC(REALLOC, p_buf, p_bufsize);
}
static jsonbcode
_jsonb_escape_STATIC(size_t *pos,
char buf[],
size_t bufsize,
unsigned offset,
const char str[],
size_t len)
{
char *esc_tok = NULL, _esc_tok[8] = "\\u00";
char *esc_buf = NULL;
int extra_bytes = 0;
size_t i;
buf += offset;
bufsize -= offset;
second_iter:
/* 1st iteration, esc_buf is NULL and count extra_bytes needed for escaping
* 2st iteration, esc_buf is not NULL, and does escaping. */
for (i = 0; i < len; ++i) {
unsigned char c = str[i];
esc_tok = NULL;
switch (c) {
case 0x22: esc_tok = "\\\""; break;
case 0x5C: esc_tok = "\\\\"; break;
case '\b': esc_tok = "\\b"; break;
case '\f': esc_tok = "\\f"; break;
case '\n': esc_tok = "\\n"; break;
case '\r': esc_tok = "\\r"; break;
case '\t': esc_tok = "\\t"; break;
default: if (c <= 0x1F) {
static const char tohex[] = "0123456789abcdef";
_esc_tok[4] = tohex[c >> 4];
_esc_tok[5] = tohex[c & 0xF];
_esc_tok[6] = 0;
esc_tok = _esc_tok;
}
}
if (esc_tok) {
int j;
for (j = 0; esc_tok[j]; j++) {
if (!esc_buf) /* count how many extra bytes are needed */
continue;
*esc_buf++ = esc_tok[j];
}
extra_bytes += j - 1;
}
else if (esc_buf) {
*esc_buf++ = c;
}
}
if (*pos + len + extra_bytes > bufsize) {
*buf = '\0';
return JSONB_ERROR_NOMEM;
}
if (esc_buf) {
*pos += len + extra_bytes;
return JSONB_OK;
}
if (!extra_bytes) {
size_t j;
for (j = 0; j < len; ++j)
buf[*pos + j] = str[j];
*pos += len;
return JSONB_OK;
}
esc_buf = buf + *pos;
extra_bytes = 0;
goto second_iter;
}
static jsonbcode
_jsonb_escape_REALLOC(size_t *pos,
char *p_buf[],
size_t *p_bufsize,
unsigned offset,
const char str[],
size_t len)
{
char *esc_tok = NULL, _esc_tok[8] = "\\u00";
char *esc_buf = NULL;
int extra_bytes = 0;
size_t i;
int second_pass = 0;
ptrdiff_t esc_buf_offset = 0;
char *buf;
size_t bufsize;
restart:
buf = *p_buf + offset;
bufsize = *p_bufsize - offset;
if (second_pass && esc_buf) esc_buf = buf + esc_buf_offset;
second_iter:
/* 1st iteration, esc_buf is NULL and count extra_bytes needed for escaping
* 2st iteration, esc_buf is not NULL, and does escaping. */
for (i = 0; i < len; ++i) {
unsigned char c = str[i];
esc_tok = NULL;
switch (c) { case 0x22: esc_tok = "\\\""; break;
case 0x5C: esc_tok = "\\\\"; break;
case '\b': esc_tok = "\\b"; break;
case '\f': esc_tok = "\\f"; break;
case '\n': esc_tok = "\\n"; break;
case '\r': esc_tok = "\\r"; break;
case '\t': esc_tok = "\\t"; break;
default: if (c <= 0x1F) {
static const char tohex[] = "0123456789abcdef";
_esc_tok[4] = tohex[c >> 4];
_esc_tok[5] = tohex[c & 0xF];
_esc_tok[6] = 0;
esc_tok = _esc_tok;
}
}
if (esc_tok) {
int j;
for (j = 0; esc_tok[j]; j++) {
if (!esc_buf) /* count how many extra bytes are needed */
continue;
*esc_buf++ = esc_tok[j];
}
extra_bytes += j - 1;
}
else if (esc_buf) {
*esc_buf++ = c;
}
}
if (*pos + len + extra_bytes + 1 > bufsize) {
char *new_buf = NULL;
const size_t needed = *pos + len + extra_bytes + 1;
size_t new_size = *p_bufsize + (*p_bufsize >> 1); /* 1.5x */
if (new_size < needed) new_size = needed;
if (new_size < *p_bufsize) return JSONB_ERROR_OVERFLOW;
new_buf = realloc(*p_buf, new_size);
if (!new_buf) return JSONB_ERROR_NOMEM;
if (esc_buf) esc_buf_offset = esc_buf - buf;
*p_buf = new_buf;
*p_bufsize = new_size;
second_pass = 1;
goto restart;
}
if (esc_buf) {
*pos += len + extra_bytes;
return JSONB_OK;
}
if (!extra_bytes) {
size_t j;
for (j = 0; j < len; ++j)
buf[*pos + j] = str[j];
*pos += len;
return JSONB_OK;
}
esc_buf = buf + *pos;
extra_bytes = 0;
goto second_iter;
}
#define JSONB_KEY_EXEC(_type, buf, bufsize, key, len) \
size_t pos = 0; \
switch (*b->top) { \
case JSONB_OBJECT_NEXT_KEY_OR_CLOSE: \
BUFFER_COPY_CHAR_##_type(b, ',', pos, buf, bufsize); \
/* fall-through */ \
case JSONB_OBJECT_KEY_OR_CLOSE: { \
enum jsonbcode ret; \
BUFFER_COPY_CHAR_##_type(b, '"', pos, buf, bufsize); \
ret = _jsonb_escape_##_type(&pos, buf, bufsize, b->pos, key, len); \
if (ret != JSONB_OK) return ret; \
BUFFER_COPY_##_type(b, "\":", 2, pos, buf, bufsize); \
STACK_HEAD(b, JSONB_OBJECT_VALUE); \
} break; \
default: \
STACK_HEAD(b, JSONB_ERROR); \
/* fall-through */ \
case JSONB_DONE: \
return JSONB_ERROR_INPUT; \
} \
b->pos += pos; \
return JSONB_OK
JSONB_API jsonbcode
jsonb_key(jsonb *b, char buf[], size_t bufsize, const char key[], size_t len)
{
JSONB_KEY_EXEC(STATIC, buf, bufsize, key, len);
}
JSONB_API jsonbcode
jsonb_key_auto(
jsonb *b, char *p_buf[], size_t *p_bufsize, const char key[], size_t len)
{
JSONB_KEY_EXEC(REALLOC, p_buf, p_bufsize, key, len);
}
#define JSONB_ARRAY_EXEC(_type, buf, bufsize) \
enum jsonbstate new_state; \
size_t pos = 0; \
if (b->top - b->stack >= JSONB_MAX_DEPTH) return JSONB_ERROR_STACK; \
switch (*b->top) { \
case JSONB_ARRAY_NEXT_VALUE_OR_CLOSE: \
BUFFER_COPY_CHAR_##_type(b, ',', pos, buf, bufsize); \
/* fall-through */ \
case JSONB_ARRAY_VALUE_OR_CLOSE: \
new_state = JSONB_ARRAY_NEXT_VALUE_OR_CLOSE; \
break; \
case JSONB_OBJECT_VALUE: \
new_state = JSONB_OBJECT_NEXT_KEY_OR_CLOSE; \
break; \
case JSONB_ARRAY_OR_OBJECT_OR_VALUE: \
new_state = JSONB_DONE; \
break; \
default: \
STACK_HEAD(b, JSONB_ERROR); \
/* fall-through */ \
case JSONB_DONE: \
case JSONB_ERROR: \
return JSONB_ERROR_INPUT; \
} \
BUFFER_COPY_CHAR_##_type(b, '[', pos, buf, bufsize); \
STACK_HEAD(b, new_state); \
STACK_PUSH(b, JSONB_ARRAY_VALUE_OR_CLOSE); \
b->pos += pos; \
return JSONB_OK
JSONB_API jsonbcode
jsonb_array(jsonb *b, char buf[], size_t bufsize)
{
JSONB_ARRAY_EXEC(STATIC, buf, bufsize);
}
JSONB_API jsonbcode
jsonb_array_auto(jsonb *b, char *p_buf[], size_t *p_bufsize)
{
JSONB_ARRAY_EXEC(REALLOC, p_buf, p_bufsize);
}
#define JSONB_ARRAY_POP_EXEC(_type, buf, bufsize) \
enum jsonbcode code; \
size_t pos = 0; \
switch (*b->top) { \
case JSONB_ARRAY_VALUE_OR_CLOSE: \
case JSONB_ARRAY_NEXT_VALUE_OR_CLOSE: \
code = b->stack == b->top - 1 ? JSONB_END : JSONB_OK; \
break; \
default: \
STACK_HEAD(b, JSONB_ERROR); \
/* fall-through */ \
case JSONB_DONE: \
case JSONB_ERROR: \
return JSONB_ERROR_INPUT; \
} \
BUFFER_COPY_CHAR_##_type(b, ']', pos, buf, bufsize); \
STACK_POP(b); \
b->pos += pos; \
return code
JSONB_API jsonbcode
jsonb_array_pop(jsonb *b, char buf[], size_t bufsize)
{
JSONB_ARRAY_POP_EXEC(STATIC, buf, bufsize);
}
JSONB_API jsonbcode
jsonb_array_pop_auto(jsonb *b, char *p_buf[], size_t *p_bufsize)
{
JSONB_ARRAY_POP_EXEC(REALLOC, p_buf, p_bufsize);
}
#define JSONB_TOKEN_EXEC(_type, buf, bufsize, token, len) \
enum jsonbstate next_state; \
enum jsonbcode code; \
size_t pos = 0; \
switch (*b->top) { \
case JSONB_ARRAY_OR_OBJECT_OR_VALUE: \
next_state = JSONB_DONE; \
code = JSONB_END; \
break; \
case JSONB_ARRAY_NEXT_VALUE_OR_CLOSE: \
BUFFER_COPY_CHAR_##_type(b, ',', pos, buf, bufsize); \
/* fall-through */ \
case JSONB_ARRAY_VALUE_OR_CLOSE: \
next_state = JSONB_ARRAY_NEXT_VALUE_OR_CLOSE; \
code = JSONB_OK; \
break; \
case JSONB_OBJECT_VALUE: \
next_state = JSONB_OBJECT_NEXT_KEY_OR_CLOSE; \
code = JSONB_OK; \
break; \
default: \
STACK_HEAD(b, JSONB_ERROR); \
/* fall-through */ \
case JSONB_DONE: \
case JSONB_ERROR: \
return JSONB_ERROR_INPUT; \
} \
BUFFER_COPY_##_type(b, token, len, pos, buf, bufsize); \
STACK_HEAD(b, next_state); \
b->pos += pos; \
return code
JSONB_API jsonbcode
jsonb_token(
jsonb *b, char buf[], size_t bufsize, const char token[], size_t len)
{
JSONB_TOKEN_EXEC(STATIC, buf, bufsize, token, len);
}
JSONB_API jsonbcode
jsonb_token_auto(
jsonb *b, char *p_buf[], size_t *p_bufsize, const char token[], size_t len)
{
JSONB_TOKEN_EXEC(REALLOC, p_buf, p_bufsize, token, len);
}
JSONB_API jsonbcode
jsonb_bool(jsonb *b, char buf[], size_t bufsize, int boolean)
{
return boolean ? jsonb_token(b, buf, bufsize, "true", 4)
: jsonb_token(b, buf, bufsize, "false", 5);
}
JSONB_API jsonbcode
jsonb_bool_auto(jsonb *b, char *p_buf[], size_t *p_bufsize, int boolean)
{
return boolean ? jsonb_token_auto(b, p_buf, p_bufsize, "true", 4)
: jsonb_token_auto(b, p_buf, p_bufsize, "false", 5);
}
JSONB_API jsonbcode
jsonb_null(jsonb *b, char buf[], size_t bufsize)
{
return jsonb_token(b, buf, bufsize, "null", 4);
}
JSONB_API jsonbcode
jsonb_null_auto(jsonb *b, char *p_buf[], size_t *p_bufsize)
{
return jsonb_token_auto(b, p_buf, p_bufsize, "null", 4);
}
#define JSONB_STRING_EXEC(_type, buf, bufsize, str, len) \
enum jsonbstate next_state; \
enum jsonbcode code, ret; \
size_t pos = 0; \
switch (*b->top) { \
case JSONB_ARRAY_OR_OBJECT_OR_VALUE: \
next_state = JSONB_DONE; \
code = JSONB_END; \
break; \
case JSONB_ARRAY_NEXT_VALUE_OR_CLOSE: \
BUFFER_COPY_CHAR_##_type(b, ',', pos, buf, bufsize); \
/* fall-through */ \
case JSONB_ARRAY_VALUE_OR_CLOSE: \
next_state = JSONB_ARRAY_NEXT_VALUE_OR_CLOSE; \
code = JSONB_OK; \
break; \
case JSONB_OBJECT_VALUE: \
next_state = JSONB_OBJECT_NEXT_KEY_OR_CLOSE; \
code = JSONB_OK; \
break; \
default: \
STACK_HEAD(b, JSONB_ERROR); \
/* fall-through */ \
case JSONB_DONE: \
case JSONB_ERROR: \
return JSONB_ERROR_INPUT; \
} \
BUFFER_COPY_CHAR_##_type(b, '"', pos, buf, bufsize); \
ret = _jsonb_escape_##_type(&pos, buf, bufsize, b->pos, str, len); \
if (ret != JSONB_OK) return ret; \
BUFFER_COPY_CHAR_##_type(b, '"', pos, buf, bufsize); \
STACK_HEAD(b, next_state); \
b->pos += pos; \
return code
JSONB_API jsonbcode
jsonb_string(
jsonb *b, char buf[], size_t bufsize, const char str[], size_t len)
{
JSONB_STRING_EXEC(STATIC, buf, bufsize, str, len);
}
JSONB_API jsonbcode
jsonb_string_auto(
jsonb *b, char *p_buf[], size_t *p_bufsize, const char str[], size_t len)
{
JSONB_STRING_EXEC(REALLOC, p_buf, p_bufsize, str, len);
}
JSONB_API jsonbcode
jsonb_number(jsonb *b, char buf[], size_t bufsize, double number)
{
char token[32];
const long len = sprintf(token, "%.17G", number);
return (len < 0) ? JSONB_ERROR_INPUT
: jsonb_token(b, buf, bufsize, token, len);
}
JSONB_API jsonbcode
jsonb_number_auto(jsonb *b, char *p_buf[], size_t *p_bufsize, double number)
{
char token[32];
const long len = sprintf(token, "%.17G", number);
return (len < 0) ? JSONB_ERROR_INPUT
: jsonb_token_auto(b, p_buf, p_bufsize, token, len);
}
#undef TRACE
#undef DECORATOR
#undef STACK_HEAD
#undef STACK_PUSH
#undef STACK_POP
#undef BUFFER_COPY_CHAR_STATIC
#undef BUFFER_COPY_STATIC
#undef BUFFER_COPY_CHAR_REALLOC
#undef BUFFER_COPY_REALLOC
#undef JSONB_OBJECT_EXEC
#undef JSONB_OBJECT_POP_EXEC
#undef JSONB_KEY_EXEC
#undef JSONB_ARRAY_EXEC
#undef JSONB_ARRAY_POP_EXEC
#undef JSONB_TOKEN_EXEC
#undef JSONB_STRING_EXEC
#endif /* JSONB_HEADER */
#ifdef __cplusplus
}
#endif
#endif /* JSON_BUILD_H */
+46
View File
@@ -0,0 +1,46 @@
/**
* @file log.h
* @author Cogmasters
* @brief Maintain support for log.c deprecated functions using logmod.h as a
* wrapper
* @attention This file is deprecated and will be removed in future releases
* @deprecated since v3.0.0
*/
#ifndef LOG_DEPRECATED_SUPPORT_H
#define LOG_DEPRECATED_SUPPORT_H
#include "logmod.h"
/**
* @brief Backwards compatible alias for logmod_log()
* @deprecated since v3.0.0
*/
#define log_trace(...) logmod_log(TRACE, NULL, __VA_ARGS__)
/**
* @brief Backwards compatible alias for logmod_log()
* @deprecated since v3.0.0
*/
#define log_debug(...) logmod_log(DEBUG, NULL, __VA_ARGS__)
/**
* @brief Backwards compatible alias for logmod_log()
* @deprecated since v3.0.0
*/
#define log_info(...) logmod_log(INFO, NULL, __VA_ARGS__)
/**
* @brief Backwards compatible alias for logmod_log()
* @deprecated since v3.0.0
*/
#define log_warn(...) logmod_log(WARN, NULL, __VA_ARGS__)
/**
* @brief Backwards compatible alias for logmod_log()
* @deprecated since v3.0.0
*/
#define log_error(...) logmod_log(ERROR, NULL, __VA_ARGS__)
/**
* @brief Backwards compatible alias for logmod_log()
* @deprecated since v3.0.0
*/
#define log_fatal(...) logmod_log(FATAL, NULL, __VA_ARGS__)
#endif /* LOG_DEPRECATED_SUPPORT_H */
+1096
View File
File diff suppressed because it is too large Load Diff
+345
View File
@@ -0,0 +1,345 @@
#ifndef OA_HASH_H
#define OA_HASH_H
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
#ifdef OA_HASH_STATIC
#define OA_HASH_API static
#else
#define OA_HASH_API extern
#endif /* OA_HASH_STATIC */
#include <stddef.h>
/** @brief Hash table entry state */
enum oa_hash_entry_state {
OA_HASH_ENTRY_EMPTY = 0, /**< empty entry */
OA_HASH_ENTRY_OCCUPIED, /**< occupied entry */
OA_HASH_ENTRY_DELETED /**< deleted entry */
};
/** @brief Entry holding key-value pair in hash table */
struct oa_hash_entry {
enum oa_hash_entry_state state; /**< entry state */
struct {
const char *buf; /**< key buffer */
size_t length; /**< key length */
} key;
void *value; /**< value pointer */
};
#define __OA_HASH_ATTRS_const \
const size_t length; /**< amount of entries */ \
const size_t capacity; /**< total buckets capacity */ \
const struct oa_hash_entry *buckets /**< entries array */
#define __OA_HASH_ATTRS_mut \
size_t length; /**< amount of entries */ \
size_t capacity; /**< total buckets capacity */ \
struct oa_hash_entry *buckets /**< entries array */
/** @brief can be used to cast to struct oa_hash */
#define OA_HASH_ATTRS(_qualifier) __OA_HASH_ATTRS_##_qualifier
/** @brief Open addressing hash table */
struct oa_hash {
OA_HASH_ATTRS(mut);
};
/**
* @brief Initialize hash table with given buckets array
*
* @param[out] ht the hash table to be initialized
* @param[out] buckets pre-allocated array of entries
* @param[in] capacity amount of buckets
*/
OA_HASH_API void oa_hash_init(struct oa_hash *ht,
struct oa_hash_entry buckets[],
const size_t capacity);
/**
* @brief Clean up hash table entries and struct
*
* @param[out] ht the hash table to be cleaned
*/
OA_HASH_API void oa_hash_cleanup(struct oa_hash *ht);
/**
* @brief Retrieve entry by key
*
* @param[in] ht the hash table
* @param[in] key the key to search for
* @param[in] len the key length
* @return entry if found, NULL otherwise
*/
OA_HASH_API const struct oa_hash_entry *oa_hash_get_entry(
const struct oa_hash *ht, const char key[], const size_t len);
/**
* @brief Retrieve value by key (wrapper around oa_hash_get_entry)
*
* @param[in] ht the hash table
* @param[in] key the key to search for
* @param[in] len the key length
* @return value if found, NULL otherwise
*/
OA_HASH_API void *oa_hash_get(const struct oa_hash *ht,
const char key[],
const size_t len);
/**
* @brief Insert or update entry
*
* @param[in,out] ht the hash table
* @param[in] key the key to insert/update
* @param[in] len the key length
* @param[in] value the value to be assigned
* @return entry if successful, or NULL if no space left, in which case
* oa_hash_rehash() should be called
*/
OA_HASH_API const struct oa_hash_entry *oa_hash_set_entry(struct oa_hash *ht,
const char key[],
const size_t len,
void *value);
/**
* @brief Insert or update entry (wrapper around oa_hash_set_entry)
*
* @param[in,out] ht the hash table
* @param[in] key the key to insert/update
* @param[in] len the key length
* @param[in] value the value to be assigned
* @return value if successful, or NULL if no space left, in which case
* oa_hash_rehash() should be called
*/
OA_HASH_API void *oa_hash_set(struct oa_hash *ht,
const char key[],
const size_t len,
void *value);
/**
* @brief Remove entry by key
*
* @param[in,out] ht the hash table
* @param[in] key the key to be removed
* @param[in] len the key length
* @return 1 if found and removed, 0 otherwise
*/
OA_HASH_API int oa_hash_remove(struct oa_hash *ht,
const char key[],
const size_t len);
/**
* @brief Rehash table to new buckets array
*
* @param[in,out] ht the hash table
* @param[in,out] new_buckets the new buckets array
* @param[in] new_capacity the new buckets capacity
* @return pointer to old (now unused) bucket if successful, or NULL otherwise
*/
OA_HASH_API struct oa_hash_entry *oa_hash_rehash(
struct oa_hash *ht,
struct oa_hash_entry new_buckets[],
const size_t new_capacity);
#ifndef OA_HASH_HEADER
#include <string.h>
#include <stdint.h>
OA_HASH_API void
oa_hash_init(struct oa_hash *ht,
struct oa_hash_entry buckets[],
const size_t capacity)
{
memset(buckets, 0, sizeof(struct oa_hash_entry) * capacity);
ht->buckets = buckets;
ht->length = 0;
ht->capacity = capacity;
}
OA_HASH_API void
oa_hash_cleanup(struct oa_hash *ht)
{
if (!ht) return;
ht->length = 0;
ht->capacity = 0;
ht->buckets = NULL;
}
static size_t
_oa_hash_genhash(const char key[], size_t len, const size_t capacity)
{
const unsigned char *str = (const unsigned char *)key;
unsigned long hash = 5381; /* DJB2 initial value */
if (!key || !capacity) return 0;
while (len--) {
hash = ((hash & 0x7fffffff) << 5) + hash + *str++;
}
return hash % capacity;
}
OA_HASH_API const struct oa_hash_entry *
oa_hash_get_entry(const struct oa_hash *ht, const char key[], const size_t len)
{
const size_t start_slot = _oa_hash_genhash(key, len, ht->capacity);
size_t slot = start_slot;
if (!len || !ht->capacity) return NULL;
do {
struct oa_hash_entry *entry = &ht->buckets[slot];
if (entry->state == OA_HASH_ENTRY_EMPTY) {
return NULL;
}
if (entry->state == OA_HASH_ENTRY_OCCUPIED && len == entry->key.length
&& 0 == memcmp(entry->key.buf, key, len))
{
return entry;
}
slot = (slot + 1) % ht->capacity;
} while (slot != start_slot);
return NULL;
}
OA_HASH_API void *
oa_hash_get(const struct oa_hash *ht, const char key[], const size_t len)
{
const struct oa_hash_entry *entry = oa_hash_get_entry(ht, key, len);
return entry ? entry->value : NULL;
}
OA_HASH_API const struct oa_hash_entry *
oa_hash_set_entry(struct oa_hash *ht,
const char key[],
const size_t len,
void *value)
{
const size_t start_slot = _oa_hash_genhash(key, len, ht->capacity);
size_t slot = start_slot;
size_t first_deleted = SIZE_MAX;
if (!len || !ht->capacity) return NULL;
do {
struct oa_hash_entry *entry = &ht->buckets[slot];
if (entry->state != OA_HASH_ENTRY_OCCUPIED) {
if (first_deleted == SIZE_MAX
&& entry->state == OA_HASH_ENTRY_DELETED)
{
first_deleted = slot;
}
if (entry->state == OA_HASH_ENTRY_EMPTY) {
slot = (first_deleted != SIZE_MAX) ? first_deleted : slot;
entry = &ht->buckets[slot];
entry->key.buf = (char *)key;
entry->key.length = len;
entry->value = value;
entry->state = OA_HASH_ENTRY_OCCUPIED;
ht->length++;
return entry;
}
}
if (entry->state == OA_HASH_ENTRY_OCCUPIED && len == entry->key.length
&& 0 == memcmp(entry->key.buf, key, len))
{
entry->value = value;
return entry;
}
slot = (slot + 1) % ht->capacity;
} while (slot != start_slot);
return NULL;
}
OA_HASH_API void *
oa_hash_set(struct oa_hash *ht,
const char key[],
const size_t len,
void *value)
{
const struct oa_hash_entry *entry = oa_hash_set_entry(ht, key, len, value);
return entry ? entry->value : NULL;
}
OA_HASH_API int
oa_hash_remove(struct oa_hash *ht, const char key[], const size_t len)
{
const size_t start_slot = _oa_hash_genhash(key, len, ht->capacity);
size_t slot = start_slot;
if (!len || !ht->capacity) return 0;
do {
struct oa_hash_entry *entry = &ht->buckets[slot];
if (entry->state == OA_HASH_ENTRY_EMPTY) {
return 0;
}
if (entry->state == OA_HASH_ENTRY_OCCUPIED && len == entry->key.length
&& 0 == memcmp(entry->key.buf, key, len))
{
entry->state = OA_HASH_ENTRY_DELETED;
ht->length--;
return 1;
}
slot = (slot + 1) % ht->capacity;
} while (slot != start_slot);
return 0;
}
OA_HASH_API struct oa_hash_entry *
oa_hash_rehash(struct oa_hash *ht,
struct oa_hash_entry new_buckets[],
const size_t new_capacity)
{
struct oa_hash_entry *old_buckets = ht->buckets;
const size_t old_capacity = ht->capacity;
const size_t old_length = ht->length;
size_t i;
if (!new_buckets || new_capacity <= old_capacity) return 0;
memset(new_buckets, 0, sizeof(struct oa_hash_entry) * new_capacity);
/* temporarily switch to new buckets */
ht->buckets = new_buckets;
ht->capacity = new_capacity;
ht->length = 0;
for (i = 0; i < old_capacity; ++i) {
if (old_buckets[i].state == OA_HASH_ENTRY_OCCUPIED
&& !oa_hash_set_entry(ht, old_buckets[i].key.buf,
old_buckets[i].key.length,
old_buckets[i].value))
{
/* restore original state on failure */
ht->buckets = old_buckets;
ht->capacity = old_capacity;
ht->length = old_length;
return NULL;
}
}
return old_buckets;
}
#endif /* OA_HASH_HEADER */
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* OA_HASH_H */
+38
View File
@@ -0,0 +1,38 @@
/**
* @file oauth2.h
* @author Cogmasters
* @brief OAuth2 public functions and datatypes
*/
#ifndef DISCORD_OAUTH2_H
#define DISCORD_OAUTH2_H
/** @defgroup DiscordAPIOAuth2 OAuth2
* @ingroup DiscordAPI
* @brief OAuth2's public API supported by Concord
* @{ */
/**
* @brief Returns the bot's application object
*
* @param client the client created with discord_from_token()
* @CCORD_ret_obj{ret,application}
* @CCORD_return
*/
CCORDcode discord_get_current_bot_application_information(
struct discord *client, struct discord_ret_application *ret);
/**
* @brief Returns info about the current authorization
* @note Requires authentication with a bearer token
*
* @param client the client created with discord_from_token()
* @CCORD_ret_obj{ret,auth_response}
* @CCORD_return
*/
CCORDcode discord_get_current_authorization_information(
struct discord *client, struct discord_ret_auth_response *ret);
/** @} DiscordAPIOAuth2 */
#endif /* DISCORD_OAUTH2_H */
+319
View File
@@ -0,0 +1,319 @@
#ifndef OSNAME_H
#define OSNAME_H 1
/*
* HackerSmacker's "Detect-It-All" OS Detector
*/
enum OSClass {
UNIX,
WINDOWS,
DOS,
OS2,
S370,
DEC,
MACINTOSH,
AMIGA,
OTHER
};
#ifdef _AIX
#define OSNAME "AIX"
#define OSCLASS UNIX
#endif
#ifdef __ANDROID__
#define OSNAME "Android"
#define OSCLASS UNIX
#endif
#ifdef UTS
#define OSNAME "UTS"
#define OSCLASS UNIX
#endif
#ifdef aegis
#define OSNAME "Aegis"
#define OSCLASS UNIX
#endif
#ifdef __BEOS__
#define OSNAME "BeOS"
#define OSCLASS OTHER
#endif
#ifdef __FreeBSD__
#define OSNAME "FreeBSD"
#define OSCLASS UNIX
#endif
#ifdef __NetBSD__
#define OSNAME "NetBSD"
#define OSCLASS UNIX
#endif
#ifdef __OpenBSD__
#define OSNAME "OpenBSD"
#define OSCLASS UNIX
#endif
#ifdef __bsdi__
#define OSNAME "BSD/OS"
#define OSCLASS UNIX
#endif
#ifdef __DragonFly__
#define OSNAME "DragonFly BSD"
#define OSCLASS UNIX
#endif
#ifdef __convex__
#define OSNAME "ConvexOS"
#define OSCLASS UNIX
#endif
#ifdef __CYGWIN__
#define OSNAME "Windows NT (Cygwin)"
#define OSCLASS UNIX
#endif
#if defined __DGUX__ || DGUX
#define OSNAME "DG/UX"
#define OSCLASS UNIX
#endif
#if defined __SEQUENT__ || sequent
#define OSNAME "DYNIX/ptx"
#define OSCLASS UNIX
#endif
#ifdef __ECOS
#define OSNAME "eCos"
#define OSCLASS OTHER
#endif
#ifdef __EMX__
#define OSNAME "OS/2 (EMX)"
#define OSCLASS UNIX
#endif
#ifdef __gnu_hurd__
#define OSNAME "GNU/Hurd"
#define OSCLASS UNIX
#endif
#if defined __gnu_linux__ || defined __linux__ || defined linux
#define OSNAME "GNU/Linux"
#define OSCLASS UNIX
#endif
#if defined _hpux || defined hpux || defined __hpux
#define OSNAME "HP-UX"
#define OSCLASS UNIX
#endif
#ifdef __OS400__
#define OSNAME "OS/400"
#define OSCLASS OTHER
#endif
#if defined __sgi || defined sgi
#define OSNAME "IRIX"
#define OSCLASS UNIX
#endif
#ifdef __INTEGRITY
#define OSNAME "INTEGRITY"
#define OSCLASS OTHER
#endif
#ifdef __Lynx__
#define OSNAME "LynxOS"
#define OSCLASS OTHER
#endif
#if defined macintosh || defined Macintosh
#define OSNAME "Classic Mac OS"
#define OSTYPE MACINTOSH
#endif
#ifdef __APPLE__
#ifdef __MACH
#define OSNAME "Mac OS X"
#define OSCLASS UNIX
#endif
#endif
#if defined __OS9000 || defined _OSK
#define OSNAME "OS-9"
#define OSCLASS OTHER
#endif
#ifdef __MORPHOS__
#define OSNAME "MorphOS"
#define OSCLASS AMIGA
#endif
#if defined AMIGA || defined __amigaos__
#define OSNAME "AmigaOS"
#define OSCLASS AMIGA
#endif
#if defined mpeix || defined __mpexl
#define OSNAME "MPE/iX"
#define OSCLASS OTHER
#endif
#if defined MSDOS || defined __MSDOS__ || defined _MSDOS || defined __DOS__
#define OSNAME "MS-DOS"
#define OSCLASS DOS
#endif
#ifdef __TANDEM
#define OSNAME "NonStop OS"
#define OSCLASS OTHER
#endif
#if defined OS2 || defined _OS2 || defined __OS2__ || defined __TOS_OS2__
#define OSNAME "OS/2"
#define OSCLASS OS2
#endif
#ifdef EPLAN9
#define OSNAME "Plan 9"
#define OSCLASS OTHER
#endif
#if defined __QNX__ || defined __QNXNTO__
#define OSNAME "QNX"
#define OSCLASS UNIX
#endif
#ifdef M_I386
#define OSNAME "SCO UNIX"
#define OSCLASS UNIX
#endif
#if defined sun || defined __sun
#if defined __SVR4 || defined __svr4
#define OSNAME "Solaris"
#define OSCLASS UNIX
#endif
#define OSNAME "SunOS"
#define OSCLASS UNIX
#endif
#ifdef __VOS__
#define OSNAME "VOS"
#define OSCLASS OTHER
#endif
#if defined __osf__ || defined __osf
#define OSNAME "OSF/1"
#define OSCLASS UNIX
#endif
#if defined ultrix || defined __ultrix || defined __ultrix__ || __SYSTYPE_BSD
#define OSNAME "ULTRIX"
#define OSCLASS UNIX
#endif
#if defined sco || defined _UNIXWARE7
#define OSNAME "UnixWare"
#define OSCLASS UNIX
#endif
#if defined VMS || defined __VMS
#define OSNAME "VMS"
#define OSCLASS VMS
#endif
#ifdef __VM__
#define OSNAME "VM/CMS"
#define OSCLASS S370
#endif
#ifdef __MVS__
#define OSNAME "MVS"
#define OSCLASS S370
#endif
#ifdef __EDC_LE
#ifndef __VM__
#define OSNAME "VSE"
#define OSCLASS S370
#endif
#ifndef __MVS__
#define OSNAME "VSE"
#define OSCLASS S370
#endif
#endif
#if defined __MCP__
#define OSNAME "MCP"
#define OSCLASS OTHER
#endif
#if defined _NETWARE_ || defined __NETWARE__
#define OSNAME "NetWare"
#define OSCLASS OTHER
#endif
#ifdef __MACH__
#ifndef __APPLE__
#define OSNAME "NeXTSTEP"
#define OSCLASS UNIX
#endif
#endif
#ifdef pyr
#define OSNAME "DC/OSx"
#define OSCLASS UNIX
#endif
#if defined sinux || defined sinix
#define OSNAME "Reliant UNIX"
#define OSCLASS UNIX
#endif
#ifdef _UNICOS
#define OSNAME "UNICOS"
#define OSCLASS UNIX
#endif
#if defined _CRAY || defined _crayx1
#define OSNAME "UNICOS/mp"
#define OSCLASS UNIX
#endif
#ifdef _UWIN
#define OSNAME "Windows NT (U/Win)"
#define OSCLASS WINDOWS
#endif
#if defined __VXWORKS__ || defined __vxworks
#define OSNAME "VxWorks"
#define OSCLASS OTHER
#endif
#ifdef _WIN32_WCE
#define OSNAME "Windows CE"
#define OSCLASS WINDOWS
#endif
#if defined _WIN32 | defined _WIN64 | defined __WIN32__
#define OSNAME "Windows NT"
#define OSCLASS WINDOWS
#endif
#ifdef _WIN16
#define OSNAME "Windows 3.x"
#define OSCLASS WINDOWS
#endif
#ifndef OSNAME
#define OSNAME "POSIX"
#define OSCLASS UNIX
#endif
#endif
+59
View File
@@ -0,0 +1,59 @@
// MIT License
// Copyright (c) 2022 Anotra
// https://github.com/Anotra/priority_queue
#pragma once
#ifndef PRIORITY_QUEUE_H
#define PRIORITY_QUEUE_H
#include <stdlib.h>
typedef struct priority_queue priority_queue;
typedef unsigned priority_queue_id;
typedef enum {
priority_queue_min = 0,
priority_queue_max = 1,
} priority_queue_flags;
priority_queue *priority_queue_create(
size_t key_size, size_t val_size,
int(*cmp)(const void *a, const void *b),
priority_queue_flags flags);
void priority_queue_destroy(priority_queue *queue);
size_t priority_queue_length(priority_queue *queue);
void priority_queue_set_max_capacity(
priority_queue *queue,
size_t capacity);
priority_queue_id priority_queue_push(
priority_queue *queue,
void *key, void *val);
priority_queue_id priority_queue_peek(
priority_queue *queue,
void *key, void *val);
priority_queue_id priority_queue_pop(
priority_queue *queue,
void *key, void *val);
priority_queue_id priority_queue_get(
priority_queue *queue,
priority_queue_id id,
void *key, void *val);
int priority_queue_del(
priority_queue *queue,
priority_queue_id id);
int priority_queue_update(priority_queue *queue,
priority_queue_id id,
void *key, void *val);
#endif //! PRIORITY_QUEUE_H
+30
View File
@@ -0,0 +1,30 @@
#ifndef QUERIEC_H
#define QUERIEC_H
#define QUERIEC_ADDITIONAL_LETTERS_SIZE 2
#define QUERIEC_ERROR_NOMEM -1
#define QUERIEC_OK 0
#include "attributes.h"
struct queriec {
int state;
size_t size;
size_t offset;
};
void
queriec_init(struct queriec *queriec, size_t size);
int queriec_snprintf_add(struct queriec *queriec, char *query,
const char key[], size_t keySize,
char buffer[], size_t bufferLen,
const char *format, ...) PRINTF_LIKE(7, 8);
int
queriec_add(struct queriec *queriec, char *query, char key[],
size_t keySize, char value[], size_t valueSize);
#endif
+111
View File
@@ -0,0 +1,111 @@
/* Copyright (c) 2013, Ben Noordhuis <[email protected]>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#ifndef QUEUE_H_
#define QUEUE_H_
#include <stddef.h>
typedef void *QUEUE[2];
/* Improve readability by letting user specify underlying type. */
#define QUEUE(type) QUEUE
/* Private macros. */
#define QUEUE_NEXT(q) (*(QUEUE **) &((*(q))[0]))
#define QUEUE_PREV(q) (*(QUEUE **) &((*(q))[1]))
#define QUEUE_PREV_NEXT(q) (QUEUE_NEXT(QUEUE_PREV(q)))
#define QUEUE_NEXT_PREV(q) (QUEUE_PREV(QUEUE_NEXT(q)))
/* Public macros. */
#define QUEUE_DATA(ptr, type, field) \
((type *) ((char *) (ptr) - offsetof(type, field)))
/* Important note: mutating the list while QUEUE_FOREACH is
* iterating over its elements results in undefined behavior.
*/
#define QUEUE_FOREACH(q, h) \
for ((q) = QUEUE_NEXT(h); (q) != (h); (q) = QUEUE_NEXT(q))
#define QUEUE_EMPTY(q) \
((const QUEUE *) (q) == (const QUEUE *) QUEUE_NEXT(q))
#define QUEUE_HEAD(q) \
(QUEUE_NEXT(q))
#define QUEUE_INIT(q) \
do { \
QUEUE_NEXT(q) = (q); \
QUEUE_PREV(q) = (q); \
} \
while (0)
#define QUEUE_ADD(h, n) \
do { \
QUEUE_PREV_NEXT(h) = QUEUE_NEXT(n); \
QUEUE_NEXT_PREV(n) = QUEUE_PREV(h); \
QUEUE_PREV(h) = QUEUE_PREV(n); \
QUEUE_PREV_NEXT(h) = (h); \
} \
while (0)
#define QUEUE_SPLIT(h, q, n) \
do { \
QUEUE_PREV(n) = QUEUE_PREV(h); \
QUEUE_PREV_NEXT(n) = (n); \
QUEUE_NEXT(n) = (q); \
QUEUE_PREV(h) = QUEUE_PREV(q); \
QUEUE_PREV_NEXT(h) = (h); \
QUEUE_PREV(q) = (n); \
} \
while (0)
#define QUEUE_MOVE(h, n) \
do { \
if (QUEUE_EMPTY(h)) \
QUEUE_INIT(n); \
else { \
QUEUE* q = QUEUE_HEAD(h); \
QUEUE_SPLIT(h, q, n); \
} \
} \
while (0)
#define QUEUE_INSERT_HEAD(h, q) \
do { \
QUEUE_NEXT(q) = QUEUE_NEXT(h); \
QUEUE_PREV(q) = (h); \
QUEUE_NEXT_PREV(q) = (q); \
QUEUE_NEXT(h) = (q); \
} \
while (0)
#define QUEUE_INSERT_TAIL(h, q) \
do { \
QUEUE_NEXT(q) = (h); \
QUEUE_PREV(q) = QUEUE_PREV(h); \
QUEUE_PREV_NEXT(q) = (q); \
QUEUE_PREV(h) = (q); \
} \
while (0)
#define QUEUE_REMOVE(q) \
do { \
QUEUE_PREV_NEXT(q) = QUEUE_NEXT(q); \
QUEUE_NEXT_PREV(q) = QUEUE_PREV(q); \
} \
while (0)
#endif /* QUEUE_H_ */
+163
View File
@@ -0,0 +1,163 @@
#ifndef REFLECTC_H
#define REFLECTC_H
#include <stddef.h>
/* Allow consumers to rename the runtime namespace (`reflectc_*` /
* `REFLECTC_*`). */
#ifndef REFLECTC_PREFIX
#define REFLECTC_PREFIX reflectc
#endif /* REFLECTC_PREFIX */
#ifndef REFLECTC_PREFIX_UPPER
#define REFLECTC_PREFIX_UPPER REFLECTC
#endif /* REFLECTC_PREFIX_UPPER */
#define __CAT(_a, _b) _a##_b
#define _CAT(_a, _b) __CAT(_a, _b)
#define REFLECTC_NS(_sym) _CAT(REFLECTC_PREFIX, _sym)
#define REFLECTC_NS_UPPER(_sym) _CAT(REFLECTC_PREFIX_UPPER, _sym)
/* Forward declarations */
struct REFLECTC_PREFIX;
struct REFLECTC_NS(_template);
struct REFLECTC_NS(_wrap);
struct REFLECTC_NS(_wrap_mut);
#define REFLECTC_TYPES(_sym) REFLECTC_NS_UPPER(_TYPES__##_sym)
enum REFLECTC_NS(_types) {
REFLECTC_TYPES(void),
REFLECTC_TYPES(bool),
REFLECTC_TYPES(char),
REFLECTC_TYPES(short),
REFLECTC_TYPES(int),
REFLECTC_TYPES(long),
REFLECTC_TYPES(float),
REFLECTC_TYPES(double),
REFLECTC_TYPES(struct),
REFLECTC_TYPES(union),
REFLECTC_TYPES(enum),
REFLECTC_TYPES(EXTEND)
};
typedef struct REFLECTC_NS(_wrap)
* (*REFLECTC_NS(_from_cb))(struct REFLECTC_PREFIX *registry,
void *self,
struct REFLECTC_NS(_wrap) * root);
struct REFLECTC_NS(_template) {
const size_t size;
const struct {
const char *const buf;
const size_t length;
} qualifier, decorator, name, alias, dimensions;
const enum REFLECTC_NS(_types) type;
const unsigned long attrs;
const struct {
const size_t length;
const struct REFLECTC_NS(_template) * array;
} members;
const REFLECTC_NS(_from_cb) from_cb;
};
#define _REFLECTC_PICKER_const struct REFLECTC_NS(_wrap)
#define _REFLECTC_PICKER___BLANK__ struct REFLECTC_NS(_wrap_mut)
#define REFLECTC_INSTANCE_FIELDS(_qualifier) \
_qualifier size_t length; \
struct REFLECTC_PREFIX *_qualifier registry; \
_qualifier void *_qualifier ptr_value; \
_qualifier struct { \
_qualifier size_t length; \
_qualifier _REFLECTC_PICKER_##_qualifier *_qualifier array; \
} members
struct REFLECTC_NS(_wrap) {
const struct REFLECTC_NS(_template) * tmpl;
REFLECTC_INSTANCE_FIELDS(const);
};
#define __BLANK__
struct REFLECTC_NS(_wrap_mut) {
const struct REFLECTC_NS(_template) * tmpl;
REFLECTC_INSTANCE_FIELDS(__BLANK__);
};
#undef __BLANK__
#undef REFLECTC_INSTANCE_FIELDS
#undef _REFLECTC_PICKER_const
#undef _REFLECTC_PICKER___BLANK__
struct REFLECTC_PREFIX *REFLECTC_NS(_init)(void);
void REFLECTC_NS(_dispose)(struct REFLECTC_PREFIX *table);
struct REFLECTC_NS(_wrap)
* REFLECTC_NS(_find)(const struct REFLECTC_PREFIX *table,
const void *value);
int REFLECTC_NS(_put)(struct REFLECTC_PREFIX *table,
void *value,
struct REFLECTC_NS(_wrap) * wrapper);
void REFLECTC_NS(_erase)(struct REFLECTC_PREFIX *table, const void *value);
size_t REFLECTC_NS(_length)(const struct REFLECTC_NS(_wrap) * member);
size_t REFLECTC_NS(_get_pos)(const struct REFLECTC_NS(_wrap) * root,
const char *const name,
const size_t len);
/* Serialized name of a member: its alias when one is declared in the
* recipe (8th tuple slot), otherwise its C member name. Use this when
* mapping members to an external format (e.g. JSON keys). */
const char *REFLECTC_NS(_alias)(const struct REFLECTC_NS(_wrap) * member,
size_t *length);
void REFLECTC_NS(_array)(const struct REFLECTC_NS(_wrap) * root,
const size_t length);
unsigned REFLECTC_NS(_get_pointer_depth)(const struct REFLECTC_NS(_wrap)
* member);
const void *REFLECTC_NS(_deref)(const struct REFLECTC_NS(_wrap) * field);
const void *REFLECTC_NS(_memcpy)(const struct REFLECTC_NS(_wrap) * field,
const void *map,
const size_t size);
const char *REFLECTC_NS(_string)(const struct REFLECTC_NS(_wrap) * dest,
const char src[],
const size_t size);
void *REFLECTC_NS(_resolve)(const struct REFLECTC_NS(_wrap) * member);
int REFLECTC_NS(_expand)(struct REFLECTC_PREFIX *registry,
const struct REFLECTC_NS(_wrap) * member);
typedef int (*REFLECTC_NS(_visit_cb))(const struct REFLECTC_NS(_wrap) * member,
void *ctx);
void REFLECTC_NS(_cleanup)(struct REFLECTC_PREFIX *registry,
struct REFLECTC_NS(_wrap) * member);
void REFLECTC_NS(_cleanup_members)(struct REFLECTC_PREFIX *registry,
struct REFLECTC_NS(_wrap) * member);
int REFLECTC_NS(_for_each)(const struct REFLECTC_NS(_wrap) * root,
REFLECTC_NS(_visit_cb) cb,
void *ctx);
const struct REFLECTC_NS(_wrap)
* REFLECTC_NS(_require_member)(const struct REFLECTC_NS(_wrap) * root,
const char *name,
size_t len);
int REFLECTC_NS(_is_pointer_type)(const struct REFLECTC_NS(_wrap) * member);
int REFLECTC_NS(_is_null)(const struct REFLECTC_NS(_wrap) * member);
int REFLECTC_NS(_expand_all)(struct REFLECTC_PREFIX *registry,
struct REFLECTC_NS(_wrap) * root);
#define REFLECTC_LOOKUP(_container, _namespace, _member_name, _root) \
(size_t)(REFLECTC_NS_UPPER(_LOOKUP__##_namespace##__##_member_name))
const void *REFLECTC_NS(_get)(const struct REFLECTC_NS(_wrap) * member);
const void *REFLECTC_NS(_set)(const struct REFLECTC_NS(_wrap) * member,
const void *value,
size_t size);
const void *REFLECTC_NS(_get_member)(const struct REFLECTC_NS(_wrap) * root,
size_t pos);
const void *REFLECTC_NS(_set_member)(const struct REFLECTC_NS(_wrap) * root,
size_t pos,
const void *value,
size_t size);
#endif /* REFLECTC_H */
+75
View File
@@ -0,0 +1,75 @@
/**
* @file stage_instance.h
* @author Cogmasters
* @brief Stage Instance public functions and datatypes
*/
#ifndef DISCORD_STAGE_INSTANCE_H
#define DISCORD_STAGE_INSTANCE_H
/** @defgroup DiscordAPIStageInstance Stage Instance
* @ingroup DiscordAPI
* @brief Stage Instance's public API supported by Concord
* @{ */
/**
* @brief Creates a new Stage Instance associated to a Stage channel
* @note requires the user to be a moderator of the Stage channel
*
* @param client the client created with discord_from_token()
* @param params the request parameters
* @CCORD_ret_obj{ret,stage_instance}
* @CCORD_return
*/
CCORDcode discord_create_stage_instance(
struct discord *client,
struct discord_create_stage_instance *params,
struct discord_ret_stage_instance *ret);
/**
* @brief Gets the stage instance associated with the Stage channel, if it
* exists
*
* @param client the client created with discord_from_token()
* @param channel_id the stage channel id
* @CCORD_ret_obj{ret,stage_instance}
* @CCORD_return
*/
CCORDcode discord_get_stage_instance(struct discord *client,
u64snowflake channel_id,
struct discord_ret_stage_instance *ret);
/**
* @brief Updates fields of an existing Stage instance
* @note requires the user to be a moderator of the Stage channel
*
* @param client the client created with discord_from_token()
* @param channel_id the stage channel id
* @param params the request parameters
* @CCORD_ret_obj{ret,stage_instance}
* @CCORD_return
*/
CCORDcode discord_modify_stage_instance(
struct discord *client,
u64snowflake channel_id,
struct discord_modify_stage_instance *params,
struct discord_ret_stage_instance *ret);
/**
* @brief Deletes the Stage instance
* @note requires the user to be a moderator of the Stage channel
*
* @param client the client created with discord_from_token()
* @param channel_id the stage channel to be deleted
* @param params the request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_stage_instance(struct discord *client,
u64snowflake channel_id,
struct discord_delete_stage_instance *params,
struct discord_ret *ret);
/** @} DiscordAPIStageInstance */
#endif /* DISCORD_STAGE_INSTANCE_H */
+104
View File
@@ -0,0 +1,104 @@
/**
* @file sticker.h
* @author Cogmasters
* @brief Sticker public functions and datatypes
*/
#ifndef DISCORD_STICKER_H
#define DISCORD_STICKER_H
/** @defgroup DiscordAPISticker Sticker
* @ingroup DiscordAPI
* @brief Sticker's public API supported by Concord
* @{ */
/**
* @brief Get a sticker from a given ID
*
* @param client the client created with discord_from_token()
* @param sticker_id the sticker to be fetched
* @CCORD_ret_obj{ret,sticker}
* @CCORD_return
*/
CCORDcode discord_get_sticker(struct discord *client,
u64snowflake sticker_id,
struct discord_ret_sticker *ret);
/**
* @brief Get a list of sticker packs available to Nitro subscribers
*
* @param client the client created with discord_from_token()
* @CCORD_ret_obj{ret,list_nitro_sticker_packs}
* @CCORD_return
*/
CCORDcode discord_list_nitro_sticker_packs(
struct discord *client, struct discord_ret_list_nitro_sticker_packs *ret);
/**
* @brief Get stickers for the given guild
* @note includes `user` fields if the bot has the `MANAGE_EMOJIS_AND_STICKERS`
* permission
*
* @param client the client created with discord_from_token()
* @param guild_id guild to fetch the stickers from
* @CCORD_ret_obj{ret,stickers}
* @CCORD_return
*/
CCORDcode discord_list_guild_stickers(struct discord *client,
u64snowflake guild_id,
struct discord_ret_stickers *ret);
/**
* @brief Get a sticker for the given guild and sticker ID
* @note includes the `user` field if the bot has the
* `MANAGE_EMOJIS_AND_STICKERS` permission
*
* @param client the client created with discord_from_token()
* @param guild_id the guild where the sticker belongs to
* @param sticker_id the sticker to be fetched
* @CCORD_ret_obj{ret,sticker}
* @CCORD_return
*/
CCORDcode discord_get_guild_sticker(struct discord *client,
u64snowflake guild_id,
u64snowflake sticker_id,
struct discord_ret_sticker *ret);
/**
* @brief Modify the given sticker
* @note requires the `MANAGE_EMOJIS_AND_STICKERS` permission
*
* @param client the client created with discord_from_token()
* @param guild_id the guild where the sticker belongs to
* @param sticker_id the sticker to be modified
* @param params the request parameters
* @CCORD_ret_obj{ret,sticker}
* @CCORD_return
*/
CCORDcode discord_modify_guild_sticker(
struct discord *client,
u64snowflake guild_id,
u64snowflake sticker_id,
struct discord_modify_guild_sticker *params,
struct discord_ret_sticker *ret);
/**
* @brief Delete the given sticker
* @note requires the `MANAGE_EMOJIS_AND_STICKERS` permission
*
* @param client the client created with discord_from_token()
* @param guild_id the guild where the sticker belongs to
* @param sticker_id the sticker to be deleted
* @param params the request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_guild_sticker(struct discord *client,
u64snowflake guild_id,
u64snowflake sticker_id,
struct discord_delete_guild_sticker *params,
struct discord_ret *ret);
/** @} DiscordAPISticker */
#endif /* DISCORD_STICKER_H */
+101
View File
@@ -0,0 +1,101 @@
/*
* Copyright (c) 2016, Mathias Brossard <mathias@brossard.org>.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef _THREADPOOL_H_
#define _THREADPOOL_H_
#ifdef __cplusplus
extern "C" {
#endif
/**
* @file threadpool.h
* @brief Threadpool Header File
*/
/**
* Increase this constants at your own risk
* Large values might slow down your system
*/
#define MAX_THREADS 64
#define MAX_QUEUE 65536
typedef struct threadpool_t threadpool_t;
typedef enum {
threadpool_invalid = -1,
threadpool_lock_failure = -2,
threadpool_queue_full = -3,
threadpool_shutdown = -4,
threadpool_thread_failure = -5
} threadpool_error_t;
typedef enum {
threadpool_graceful = 1
} threadpool_destroy_flags_t;
/**
* @function threadpool_create
* @brief Creates a threadpool_t object.
* @param thread_count Number of worker threads.
* @param queue_size Size of the queue.
* @param flags Unused parameter.
* @return a newly created thread pool or NULL
*/
threadpool_t *threadpool_create(int thread_count, int queue_size, int flags);
/**
* @function threadpool_add
* @brief add a new task in the queue of a thread pool
* @param pool Thread pool to which add the task.
* @param function Pointer to the function that will perform the task.
* @param argument Argument to be passed to the function.
* @param flags Unused parameter.
* @return 0 if all goes well, negative values in case of error (@see
* threadpool_error_t for codes).
*/
int threadpool_add(threadpool_t *pool, void (*routine)(void *),
void *arg, int flags);
/**
* @function threadpool_destroy
* @brief Stops and destroys a thread pool.
* @param pool Thread pool to destroy.
* @param flags Flags for shutdown
*
* Known values for flags are 0 (default) and threadpool_graceful in
* which case the thread pool doesn't accept any new tasks but
* processes all pending tasks before shutdown.
*/
int threadpool_destroy(threadpool_t *pool, int flags);
#ifdef __cplusplus
}
#endif
#endif /* _THREADPOOL_H_ */
+66
View File
@@ -0,0 +1,66 @@
/** @file types.h */
#ifndef CONCORD_TYPES_H
#define CONCORD_TYPES_H
#include <stddef.h>
#include <stdint.h>
/** @defgroup ConcordTypes Primitives
* @brief Commonly used datatypes
*
* @note these datatypes are typedefs of C primitives,
* its purpose is to facilitate identification
* and "intent of use".
* @{ */
/**
* @brief Unix time in milliseconds
*
* Commonly used for fields that may store timestamps
*/
typedef uint64_t u64unix_ms;
/**
* @brief Snowflake datatype
*
* Used in APIs such as Twitter and Discord for their unique IDs
*/
typedef uint64_t u64snowflake;
/**
* @brief Bitmask primitive
*
* Used for fields that may store values of, or perform bitwise operations
*/
typedef uint64_t u64bitmask;
/**
* @brief Raw JSON string
*
* Used for fields that have dynamic or unreliable types. A string made out of
* `json_char` should be used to keep a raw JSON, which can then be
* parsed with the assistance of a JSON library.
*/
typedef char json_char;
/** @brief Generic sized buffer */
struct ccord_szbuf {
/** the buffer's start */
char *start;
/** the buffer's size in bytes */
size_t size;
/** true if buffer is static (else is dynamic and shall be freed) */
bool is_static;
};
/** @brief Read-only generic sized buffer */
struct ccord_szbuf_readonly {
/** the buffer's start */
const char *start;
/** the buffer's size in bytes */
size_t size;
};
/** @} ConcordTypes */
#endif /* CONCORD_TYPES_H */
+400
View File
@@ -0,0 +1,400 @@
/** @file user-agent.h */
#ifndef USER_AGENT_H
#define USER_AGENT_H
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
#include <curl/curl.h>
/** @brief HTTP methods */
enum http_method {
HTTP_INVALID = -1,
HTTP_DELETE,
HTTP_GET,
HTTP_POST,
HTTP_MIMEPOST,
HTTP_PATCH,
HTTP_PUT
};
/**
* @brief Get the HTTP method name string
*
* @param method the HTTP method
* @return the HTTP method name
*/
const char *http_method_print(enum http_method method);
/**
* @brief Get the HTTP method enumerator from a string
*
* @param method the HTTP method string
* @return the HTTP method enumerator
*/
enum http_method http_method_eval(char method[]);
/** @defgroup HttpStatusCode
* @see https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
* @{ */
#define HTTP_OK 200
#define HTTP_CREATED 201
#define HTTP_ACCEPTED 202
#define HTTP_NON_AUTHORITATIVE_INFO 203
#define HTTP_NO_CONTENT 204
#define HTTP_RESET_CONTENT 205
#define HTTP_PARTIAL_CONTENT 206
#define HTTP_MULTI_STATUS 207
#define HTTP_ALREADY_REPORTED 208
#define HTTP_IM_USED 226
#define HTTP_MULTIPLE_CHOICES 300
#define HTTP_MOVED_PERMANENTLY 301
#define HTTP_FOUND 302
#define HTTP_SEE_OTHER 303
#define HTTP_NOT_MODIFIED 304
#define HTTP_USE_PROXY 305
#define HTTP_TEMPORARY_REDIRECT 307
#define HTTP_PERMANENT_REDIRECT 308
#define HTTP_BAD_REQUEST 400
#define HTTP_UNAUTHORIZED 401
#define HTTP_PAYMENT_REQUIRED 402
#define HTTP_FORBIDDEN 403
#define HTTP_NOT_FOUND 404
#define HTTP_METHOD_NOT_ALLOWED 405
#define HTTP_NOT_ACCEPTABLE 406
#define HTTP_PROXY_AUTHENTICATION 407
#define HTTP_REQUEST_TIMEOUT 408
#define HTTP_CONFLICT 409
#define HTTP_GONE 410
#define HTTP_LENGTH_REQUIRED 411
#define HTTP_PRECONDITION_FAILED 412
#define HTTP_PAYLOAD_TOO_LARGE 413
#define HTTP_URI_TOO_LONG 414
#define HTTP_UNSUPPORTED_MEDIA_TYPE 415
#define HTTP_RANGE_NOT_SATISFIABLE 416
#define HTTP_EXPECTATION_FAILED 417
#define HTTP_IM_A_TEAPOT 418
#define HTTP_MISDIRECTED_REQUEST 421
#define HTTP_UNPROCESSABLE_ENTITY 422
#define HTTP_LOCKED 423
#define HTTP_FAILED_DEPENDENCY 424
#define HTTP_TOO_EARLY 425
#define HTTP_UPGRADE_REQUIRED 426
#define HTTP_PRECONDITION_REQUIRED 428
#define HTTP_TOO_MANY_REQUESTS 429
#define HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE 431
#define HTTP_UNAVAILABLE_FOR_LEGAL_REASONS 451
#define HTTP_INTERNAL_SERVER_ERROR 500
#define HTTP_NOT_IMPLEMENTED 501
#define HTTP_BAD_GATEWAY 502
#define HTTP_SERVICE_UNAVAILABLE 503
#define HTTP_GATEWAY_TIMEOUT 504
#define HTTP_VERSION_NOT_SUPPORTED 505
#define HTTP_VARIANT_ALSO_NEGOTIATES 506
#define HTTP_INSUFFICIENT_STORAGE 507
#define HTTP_LOOP_DETECTED 508
#define HTTP_NOT_EXTENDED 510
#define HTTP_NETWORK_AUTHENTICATION_REQUIRED 511
#define HTTP_INVALID_HTTP_CODE 999
/** @} */
/**
* @brief Get the HTTP status code name string
*
* @param httpcode the HTTP status code
* @return the HTTP status code name
*/
const char *http_code_print(int httpcode);
/**
* @brief Get the HTTP status code reason string
*
* @param httpcode the HTTP status code
* @return the HTTP status code reason
*/
const char *http_reason_print(int httpcode);
/**
* @struct user_agent
* @brief Opaque User-Agent handle
*
* @see ua_init(), ua_cleanup(), ua_set_url(), ua_get_url(), ua_set_opt()
*/
struct user_agent;
/* forward declaration */
struct logmod;
/**/
/**
* @struct ua_conn
* @brief Opaque connection handle
*
* @see ua_conn_start(), ua_conn_setup(), ua_conn_reset(), ua_conn_stop(),
* ua_conn_easy_perform(), ua_conn_add_header(), ua_conn_print_header(),
* ua_conn_set_mime(), ua_conn_get_easy_handle()
*/
struct ua_conn;
/** @brief Read-only generic sized buffer */
struct ua_szbuf_readonly {
/** the buffer's start */
const char *start;
/** the buffer's size in bytes */
size_t size;
};
/** @brief header fields to have its contents hidden when logging */
struct ua_log_filter {
/** list of headers */
struct ua_szbuf_readonly *headers;
/** amount of headers to be filtered */
size_t length;
};
/** @brief Connection attributes */
struct ua_conn_attr {
/** the HTTP method of this transfer (GET, POST, ...) */
enum http_method method;
/** the optional request body, can be NULL */
char *body;
/** the request body size */
size_t body_size;
/** the endpoint to be appended to the base URL */
char *endpoint;
/** optional base_url to override ua_set_url(), can be NULL */
char *base_url;
/** @brief header fields to have its contents filtered when logging */
struct ua_log_filter log_filter;
};
/** Maximum amount of header pairs */
#define UA_MAX_HEADER_PAIRS 100 + 1
/** @brief Structure for storing the request's response header */
struct ua_resp_header {
/** response header buffer */
char *buf;
/** response header string length */
size_t len;
/** real size occupied in memory by buffer */
size_t bufsize;
/** array of header field/value pairs */
struct {
struct {
/** offset index of 'buf' for the start of field or value */
size_t idx;
/** length of individual field or value */
size_t size;
} field, value;
} pairs[UA_MAX_HEADER_PAIRS];
/** amount of pairs initialized */
int n_pairs;
};
/** @brief Structure for storing the request's response body */
struct ua_resp_body {
/** response body buffer */
char *buf;
/** response body string length */
size_t len;
/** real size occupied in memory by buffer */
size_t bufsize;
};
/** @brief Informational handle received on request's completion */
struct ua_info {
/** the HTTP response code */
long httpcode;
/** @privatesection */
/** the response header */
struct ua_resp_header header;
/** the response body */
struct ua_resp_body body;
};
/**
* @brief Callback to be called on each libcurl's easy handle initialization
*
* @param ua the User-Handle created with ua_init()
* @param data user data to be passed along to `callback`
* @param callback the user callback
*/
void ua_set_opt(struct user_agent *ua,
void *data,
void (*callback)(struct ua_conn *conn, void *data));
/**
* @brief Initialize User-Agent handle
*
* @param logmod optional pre-initialized logging module
* @param fp file pointer for writing HTTP traces to
* @return the user agent handle
*/
struct user_agent *ua_init(struct logmod *logmod, FILE *fp);
/**
* @brief Cleanup User-Agent handle resources
*
* @param ua the User-Agent handle created with ua_init()
*/
void ua_cleanup(struct user_agent *ua);
/**
* @brief Set the request url
*
* @param ua the User-Agent handle created with ua_init()
* @param base_url the base request url
*/
void ua_set_url(struct user_agent *ua, const char base_url[]);
/**
* @brief Get the request url
*
* @param ua the User-Agent handle created with ua_init()
* @return the request url set with ua_set_url()
*/
const char *ua_get_url(struct user_agent *ua);
/** @brief Callback for object to be loaded by api response */
typedef void (*ua_load_obj_cb)(char *str, size_t len, void *p_obj);
/** @brief User callback to be called on request completion */
struct ua_resp_handle {
/** callback called when a successful transfer occurs */
ua_load_obj_cb ok_cb;
/** the pointer to be passed to ok_cb */
void *ok_obj;
/** callback called when a failed transfer occurs */
ua_load_obj_cb err_cb;
/** the pointer to be passed to err_cb */
void *err_obj;
};
/**
* @brief Get a connection handle and mark it as running
*
* @param conn the User-Agent handle created with ua_init()
* @return a connection handle
*/
struct ua_conn *ua_conn_start(struct user_agent *ua);
/**
* @brief Add a field/value pair to the request header
*
* @param conn the connection handle
* @param field header's field to be added
* @param value field's value
* @return CURLE_OK on success, otherwise an error code
*/
CURLcode ua_conn_add_header(struct ua_conn *conn,
const char field[],
const char value[]);
/**
* @brief Remove a header field
*
* @param conn the connection handle
* @param field header's field to be removed
* @return CURLE_OK on success, otherwise an error code
*/
CURLcode ua_conn_remove_header(struct ua_conn *conn, const char field[]);
/**
* @brief Fill a buffer with the request header
*
* @param conn the connection handle
* @param buf the user buffer to be filled
* @param bufsize the user buffer size in bytes
* @param log_filter headers to have its contents hidden when logging
* @return the user buffer
*/
char *ua_conn_print_header(struct ua_conn *conn,
char *buf,
size_t bufsize,
struct ua_log_filter *log_filter);
/**
* @brief Multipart creation callback for `conn`
* @see https://curl.se/libcurl/c/smtp-mime.html
*
* @param conn the connection handle to send multipart body
* @param data user data to be passed along to `callback`
* @param callback the user callback
*/
void ua_conn_set_mime(struct ua_conn *conn,
void *data,
void (*callback)(curl_mime *mime, void *data));
/**
* @brief Reset a connection handle fields
*
* @param conn connection handle to be reset
* @warning this won't deactivate the handle, for that purpose check
* ua_conn_stop()
*/
void ua_conn_reset(struct ua_conn *conn);
/**
* @brief Stop a connection handle and mark it as idle
*
* @param conn connection handle to be deactivated
*/
void ua_conn_stop(struct ua_conn *conn);
/**
* @brief Setup transfer attributes
*
* @param conn the connection handle
* @param attr attributes to be set for transfer
* @return CURLE_OK on success, otherwise an error code
*/
CURLcode ua_conn_setup(struct ua_conn *conn, struct ua_conn_attr *attr);
/**
* @brief Get libcurl's easy handle assigned to `conn`
*
* @param conn the connection handle
* @return the libcurl's easy handle
*/
CURL *ua_conn_get_easy_handle(struct ua_conn *conn);
/**
* @brief Extract information from `conn` previous request
*
* @param conn the connection handle
* @param info handle to store information on previous request
*/
void ua_info_extract(struct ua_conn *conn, struct ua_info *info);
/**
* @brief Cleanup informational handle
*
* @param info handle containing information on previous request
*/
void ua_info_cleanup(struct ua_info *info);
/**
* @brief Get a value's from the response header
*
* @param info handle containing information on previous request
* @param field the header field to fetch the value
* @return a @ref ua_szbuf_readonly containing the field's value
*/
struct ua_szbuf_readonly ua_info_get_header(struct ua_info *info,
char field[]);
/**
* @brief Get the response body
*
* @param info handle containing information on previous request
* @return a @ref ua_szbuf_readonly containing the response body
*/
struct ua_szbuf_readonly ua_info_get_body(struct ua_info *info);
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* USER_AGENT_H */
+115
View File
@@ -0,0 +1,115 @@
/**
* @file user.h
* @author Cogmasters
* @brief User public functions and datatypes
*/
#ifndef DISCORD_USER_H
#define DISCORD_USER_H
/** @defgroup DiscordAPIUser User
* @ingroup DiscordAPI
* @brief User's public API supported by Concord
* @{ */
/**
* @brief Get client's user
*
* @param client the client created with discord_from_token()
* @CCORD_ret_obj{ret,user}
* @CCORD_return
*/
CCORDcode discord_get_current_user(struct discord *client,
struct discord_ret_user *ret);
/**
* @brief Get user for a given id
*
* @param client the client created with discord_from_token()
* @param user_id user to be fetched
* @CCORD_ret_obj{ret,user}
* @CCORD_return
*/
CCORDcode discord_get_user(struct discord *client,
u64snowflake user_id,
struct discord_ret_user *ret);
/**
* @brief Modify client's user account settings
*
* @param client the client created with discord_from_token()
* @param params request parameters
* @CCORD_ret_obj{ret,user}
* @CCORD_return
*/
CCORDcode discord_modify_current_user(
struct discord *client,
struct discord_modify_current_user *params,
struct discord_ret_user *ret);
/**
* @brief Get guilds client is a member of
* @note Requires the `guilds` oauth2 scope
*
* @param client the client created with discord_from_token()
* @CCORD_ret_obj{ret,guilds}
* @CCORD_return
*/
CCORDcode discord_get_current_user_guilds(struct discord *client,
struct discord_ret_guilds *ret);
/**
* @brief Leave a guild
*
* @param client the client created with discord_from_token()
* @param guild_id guild to exit from
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_leave_guild(struct discord *client,
u64snowflake guild_id,
struct discord_ret *ret);
/**
* @brief Create a new DM channel with a given user
* @warning DMs should generally be initiated by a user action. If you open a
* significant amount of DMs too quickly, your bot may be rate limited
* or blocked from opening new ones
*
* @param client the client created with discord_from_token()
* @param params the request parameters
* @CCORD_ret_obj{ret,channel}
* @CCORD_return
*/
CCORDcode discord_create_dm(struct discord *client,
struct discord_create_dm *params,
struct discord_ret_channel *ret);
/**
* @brief Create a new group DM channel with multiple users
* @note DMs created with this function will not be shown in the Discord client
* @note Limited to 10 active group DMs
*
* @param client the client created with discord_from_token()
* @param params the request parameters
* @CCORD_ret_obj{ret,channel}
* @CCORD_return
*/
CCORDcode discord_create_group_dm(struct discord *client,
struct discord_create_group_dm *params,
struct discord_ret_channel *ret);
/**
* @brief Get a list of connection objects
* @note Requires the `connections` oauth2 scope
*
* @param client the client created with discord_from_token()
* @CCORD_ret_obj{ret,connections}
* @CCORD_return
*/
CCORDcode discord_get_user_connections(struct discord *client,
struct discord_ret_connections *ret);
/** @} DiscordAPIUser */
#endif /* DISCORD_USER_H */
+31
View File
@@ -0,0 +1,31 @@
/**
* @file voice.h
* @author Cogmasters
* @brief Voice public functions and datatypes
*/
#ifndef DISCORD_VOICE_H
#define DISCORD_VOICE_H
/** @defgroup DiscordAPIVoice Voice
* @ingroup DiscordAPI
* @brief Voice's public API supported by Concord
* @{ */
/**
* @brief Get voice regions that can be used when setting a
* voice or stage channel's `rtc_region`
*
* @param client the client created with discord_from_token()
* @CCORD_ret_obj{ret,voice_regions}
* @CCORD_return
*/
CCORDcode discord_list_voice_regions(struct discord *client,
struct discord_ret_voice_regions *ret);
/** @example voice.c
* Demonstrates a couple use cases of the Voice API */
/** @} DiscordAPIVoice */
#endif /* DISCORD_VOICE_H */
+211
View File
@@ -0,0 +1,211 @@
/**
* @file webhook.h
* @author Cogmasters
* @brief Webhook public functions and datatypes
*/
#ifndef DISCORD_WEBHOOK_H
#define DISCORD_WEBHOOK_H
/** @defgroup DiscordAPIWebhook Webhook
* @ingroup DiscordAPI
* @brief Webhook's public API supported by Concord
* @{ */
/**
* @brief Create a new webhook
* @note Requires the MANAGE_WEBHOOKS permission
*
* @param client the client created with discord_from_token()
* @param channel_id the channel that the webhook belongs to
* @param params request parameters
* @CCORD_ret_obj{ret,webhook}
* @CCORD_return
*/
CCORDcode discord_create_webhook(struct discord *client,
u64snowflake channel_id,
struct discord_create_webhook *params,
struct discord_ret_webhook *ret);
/**
* @brief Get webhooks from a given channel
* @note Requires the MANAGE_WEBHOOKS permission
*
* @param client the client created with discord_from_token()
* @param channel_id the channel that the webhooks belongs to
* @CCORD_ret_obj{ret,webhooks}
* @CCORD_return
*/
CCORDcode discord_get_channel_webhooks(struct discord *client,
u64snowflake channel_id,
struct discord_ret_webhooks *ret);
/**
* @brief Get webhooks from a given guild webhook objects
* @note Requires the MANAGE_WEBHOOKS permission
*
* @param client the client created with discord_from_token()
* @param guild_id the guild that the webhooks belongs to
* @CCORD_ret_obj{ret,webhooks}
* @CCORD_return
*/
CCORDcode discord_get_guild_webhooks(struct discord *client,
u64snowflake guild_id,
struct discord_ret_webhooks *ret);
/**
* @brief Get the new webhook object for the given id
*
* @param client the client created with discord_from_token()
* @param webhook_id the webhook itself
* @CCORD_ret_obj{ret,webhook}
* @CCORD_return
*/
CCORDcode discord_get_webhook(struct discord *client,
u64snowflake webhook_id,
struct discord_ret_webhook *ret);
/**
* Same as discord_get_webhook(), except this call does not require
* authentication and returns no user in the webhook object
* @param client the client created with discord_from_token()
* @param webhook_id the webhook itself
* @param webhook_token the webhook token
* @CCORD_ret_obj{ret,webhook}
* @CCORD_return
*/
CCORDcode discord_get_webhook_with_token(struct discord *client,
u64snowflake webhook_id,
const char webhook_token[],
struct discord_ret_webhook *ret);
/**
* @brief Modify a webhook
* @note Requires the MANAGE_WEBHOOKS permission
*
* @param client the client created with discord_from_token()
* @param webhook_id the webhook itself
* @param params request parameters
* @CCORD_ret_obj{ret,webhook}
* @CCORD_return
*/
CCORDcode discord_modify_webhook(struct discord *client,
u64snowflake webhook_id,
struct discord_modify_webhook *params,
struct discord_ret_webhook *ret);
/**
* Same discord_modify_webhook(), except this call does not require
* authentication and returns no user in the webhook object
* @param client the client created with discord_from_token()
* @param webhook_id the webhook itself
* @param webhook_token the webhook token
* @param params request parameters
* @CCORD_ret_obj{ret,webhook}
* @CCORD_return
*/
CCORDcode discord_modify_webhook_with_token(
struct discord *client,
u64snowflake webhook_id,
const char webhook_token[],
struct discord_modify_webhook_with_token *params,
struct discord_ret_webhook *ret);
/**
* Delete a webhook permanently. Requires the MANAGE_WEBHOOKS permission
* @param client the client created with discord_from_token()
* @param webhook_id the webhook itself
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_webhook(struct discord *client,
u64snowflake webhook_id,
struct discord_delete_webhook *params,
struct discord_ret *ret);
/**
* Same discord_delete_webhook(), except this call does not require
* authentication
* @param client the client created with discord_from_token()
* @param webhook_id the webhook itself
* @param webhook_token the webhook token
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_webhook_with_token(struct discord *client,
u64snowflake webhook_id,
const char webhook_token[],
struct discord_ret *ret);
/**
* @param client the client created with discord_from_token()
* @param webhook_id the webhook itself
* @param webhook_token the webhook token
* @param params request parameters
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_execute_webhook(struct discord *client,
u64snowflake webhook_id,
const char webhook_token[],
struct discord_execute_webhook *params,
struct discord_ret *ret);
/**
* @brief Get previously-sent webhook message from the same token
*
* @param client the client created with discord_from_token()
* @param webhook_id the webhook itself
* @param webhook_token the webhook token
* @param message_id the message the webhook belongs to
* @CCORD_ret_obj{ret,message}
* @CCORD_return
*/
CCORDcode discord_get_webhook_message(struct discord *client,
u64snowflake webhook_id,
const char webhook_token[],
u64snowflake message_id,
struct discord_ret_message *ret);
/**
* @brief Edits a previously-sent webhook message from the same token
*
* @param client the client created with discord_from_token()
* @param webhook_id the webhook itself
* @param webhook_token the webhook token
* @param message_id the message the webhook belongs to
* @param params request parameters
* @CCORD_ret_obj{ret,message}
* @CCORD_return
*/
CCORDcode discord_edit_webhook_message(
struct discord *client,
u64snowflake webhook_id,
const char webhook_token[],
u64snowflake message_id,
struct discord_edit_webhook_message *params,
struct discord_ret_message *ret);
/**
* @brief Deletes a message that was created by the webhook
*
* @param client the client created with discord_from_token()
* @param webhook_id the webhook itself
* @param webhook_token the webhook token
* @param message_id the message the webhook belongs to
* @CCORD_ret{ret}
* @CCORD_return
*/
CCORDcode discord_delete_webhook_message(struct discord *client,
u64snowflake webhook_id,
const char webhook_token[],
u64snowflake message_id,
struct discord_ret *ret);
/** @example webhook.c
* Demonstrates a couple use cases of the Webhook API */
/** @} DiscordAPIWebhook */
#endif /* DISCORD_WEBHOOK_H */
+326
View File
@@ -0,0 +1,326 @@
/**
* @file websockets.h
*/
#ifndef WEBSOCKETS_H
#define WEBSOCKETS_H
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
#include <stdint.h>
#include <curl/curl.h>
/**
* @struct websockets
* @brief Opaque handler for WebSockets
*
* @see ws_init(), ws_cleanup()
*/
struct websockets;
/* forward declaration */
struct logmod;
/**/
/**
* @brief The WebSockets client status
*
* @see ws_get_status()
*/
enum ws_status {
/** client disconnected from ws */
WS_DISCONNECTED = 0,
/** client connected to ws */
WS_CONNECTED,
/** client in the process of disconnecting from ws */
WS_DISCONNECTING,
/** client in the process of connecting to ws */
WS_CONNECTING,
};
/**
* @brief WebSockets CLOSE opcodes
* @see ws_close_opcode_print()
* @see https://tools.ietf.org/html/rfc6455#section-7.4.1
*/
enum ws_close_reason {
WS_CLOSE_REASON_NORMAL = 1000,
WS_CLOSE_REASON_GOING_AWAY = 1001,
WS_CLOSE_REASON_PROTOCOL_ERROR = 1002,
WS_CLOSE_REASON_UNEXPECTED_DATA = 1003,
WS_CLOSE_REASON_NO_REASON = 1005,
WS_CLOSE_REASON_ABRUPTLY = 1006,
WS_CLOSE_REASON_INCONSISTENT_DATA = 1007,
WS_CLOSE_REASON_POLICY_VIOLATION = 1008,
WS_CLOSE_REASON_TOO_BIG = 1009,
WS_CLOSE_REASON_MISSING_EXTENSION = 1010,
WS_CLOSE_REASON_SERVER_ERROR = 1011,
WS_CLOSE_REASON_IANA_REGISTRY_START = 3000,
WS_CLOSE_REASON_IANA_REGISTRY_END = 3999,
WS_CLOSE_REASON_PRIVATE_START = 4000,
WS_CLOSE_REASON_PRIVATE_END = 4999
};
/** @brief WebSockets callbacks */
struct ws_callbacks {
/**
* @brief Called upon connection
*/
void (*on_connect)(void *data, struct websockets *ws);
/**
* @brief Reports UTF-8 text messages.
*
* @note it's guaranteed to be NULL (\0) terminated, but the UTF-8 is
* not validated. If it's invalid, consider closing the connection
* with WS_CLOSE_REASON_INCONSISTENT_DATA.
*/
void (*on_text)(void *data,
struct websockets *ws,
const char *text,
size_t len);
/** @brief reports binary data. */
void (*on_binary)(void *data,
struct websockets *ws,
const void *mem,
size_t len);
/**
* @brief reports PING.
*
* @note if provided you should reply with ws_pong(). If not
* provided, pong is sent with the same message payload.
*/
void (*on_ping)(void *data,
struct websockets *ws,
const char *reason,
size_t len);
/** @brief reports PONG. */
void (*on_pong)(void *data,
struct websockets *ws,
const char *reason,
size_t len);
/**
* @brief reports server closed the connection with the given reason.
*
* Clients should not transmit any more data after the server is
* closed
*/
void (*on_close)(void *data,
struct websockets *ws,
enum ws_close_reason wscode,
const char *reason,
size_t len);
/** @brief user arbitrary data to be passed around callbacks */
void *data;
};
/**
* @brief Check if a WebSockets connection is alive
*
* This will only return true if the connection status is
* different than WS_DISCONNECTED
* @param ws the WebSockets handle created with ws_init()
* @return `true` if WebSockets status is different than
* WS_DISCONNECTED, `false` otherwise.
*/
#define ws_is_alive(ws) (ws_get_status(ws) != WS_DISCONNECTED)
/**
* @brief Check if WebSockets connection is functional
*
* This will only return true if the connection status is
* WS_CONNECTED
* @param ws the WebSockets handle created with ws_init()
* @return `true` if is functional, `false` otherwise
*/
#define ws_is_functional(ws) (ws_get_status(ws) == WS_CONNECTED)
/**
* @brief Create a new (CURL-based) WebSockets handle
*
* @param cbs set of functions to call back when server report events.
* @param mhandle user-owned curl_multi handle for performing non-blocking
* transfers
* @param logmod optional pre-initialized logging handler
* @param fp file pointer for writing WebSockets traces to
* @return newly created WebSockets handle, free with ws_cleanup()
*/
struct websockets *ws_init(struct ws_callbacks *cbs,
CURLM *mhandle,
struct logmod *logmod,
FILE *fp);
/**
* @brief Free a WebSockets handle created with ws_init()
*
* @param ws the WebSockets handle created with ws_init()
*/
void ws_cleanup(struct websockets *ws);
/**
* @brief Set the URL for the WebSockets handle to connect
*
* @param ws the WebSockets handle created with ws_init()
* @param base_url the URL to connect, such as ws://echo.websockets.org
*/
void ws_set_url(struct websockets *ws, const char base_url[]);
/**
* @brief Send a binary message of given size.
*
* Binary messages do not need to include the null terminator (\0), they
* will be read up to @a msglen.
*
* @param ws the WebSockets handle created with ws_init()
* @param msg the pointer to memory (linear) to send.
* @param msglen the length in bytes of @a msg.
* @return true if sent, false on errors.
*/
_Bool ws_send_binary(struct websockets *ws, const char msg[], size_t msglen);
/**
* @brief Send a text message of given size.
*
* Text messages do not need to include the null terminator (\0), they
* will be read up to @a len.
*
* @param ws the WebSockets handle created with ws_init()
* @param text the pointer to memory (linear) to send.
* @param len the length in bytes of @a text.
* @return true if sent, false on errors.
*/
_Bool ws_send_text(struct websockets *ws, const char text[], size_t len);
/**
* @brief Send a PING (opcode 0x9) frame with @a reason as payload.
*
* @param ws the WebSockets handle created with ws_init()
* @param reason NULL or some UTF-8 string null ('\0') terminated.
* @param len the length of @a reason in bytes. If SIZE_MAX, uses
* strlen() on @a reason if it's not NULL.
* @return true if sent, false on errors.
*/
_Bool ws_ping(struct websockets *ws, const char reason[], size_t len);
/**
* @brief Send a PONG (opcode 0xA) frame with @a reason as payload.
*
* Note that pong is sent automatically if no "on_ping" callback is
* defined. If one is defined you must send pong manually.
*
* @param ws the WebSockets handle created with ws_init()
* @param reason NULL or some UTF-8 string null ('\0') terminated.
* @param len the length of @a reason in bytes. If SIZE_MAX, uses
* strlen() on @a reason if it's not NULL.
* @return true if sent, false on errors.
*/
_Bool ws_pong(struct websockets *ws, const char reason[], size_t len);
/**
* @brief Signals connecting state before entering the WebSockets event loop
*
* @param ws the WebSockets handle created with ws_init()
* @return the WebSockets easy_handle that is free'd at ws_end()
*/
CURL *ws_start(struct websockets *ws);
/**
* @brief Cleanup and reset `ws` connection resources
*
* @param ws the WebSockets handle created with ws_init()
*/
void ws_end(struct websockets *ws);
/**
* @brief Reads/Write available data from WebSockets
* @note Helper over curl_multi_wait()
*
* @param ws the WebSockets handle created with ws_init()
* @param wait_ms limit amount in milliseconds to wait for until activity
* @param tstamp get current timestamp for this iteration
* @return `true` if connection is still alive, `false` otherwise
* @note This is an easy, yet highly abstracted way of performing transfers.
* If a higher control is necessary, users are better of using
* ws_multi_socket_run()
*/
_Bool ws_easy_run(struct websockets *ws, uint64_t wait_ms, uint64_t *tstamp);
/**
* @brief Reads/Write available data from WebSockets
* @note I/O is driven by io_poller per-socket-event via curl_multi_socket_action()
*
* @param ws the WebSockets handle created with ws_init()
* @param tstamp get current timestamp for this iteration
* @return `true` if connection is still alive, `false` otherwise
*/
_Bool ws_multi_socket_run(struct websockets *ws, uint64_t *tstamp);
/**
* @brief Returns the WebSockets handle connection status
*
* @param ws the WebSockets handle created with ws_init()
* @return a ws_status opcode
*/
enum ws_status ws_get_status(struct websockets *ws);
/**
* @brief Returns a enum ws_close_reason opcode in a string format
*
* @param opcode the opcode to be converted to string
* @return a read-only string literal of the opcode
*/
const char *ws_close_opcode_print(enum ws_close_reason opcode);
/**
* @brief The WebSockets event-loop concept of "now"
*
* @param ws the WebSockets handle created with ws_init()
* @return the timestamp in milliseconds from when ws_timestamp_update() was
* last called
* @note the timestamp is updated at the start of each event-loop iteration
*/
uint64_t ws_timestamp(struct websockets *ws);
/**
* @brief Update the WebSockets event-loop concept of "now"
*
* @param ws the WebSockets handle created with ws_init()
* @return the timestamp in milliseconds
*/
uint64_t ws_timestamp_update(struct websockets *ws);
/**
* @brief Thread-safe way to stop websockets connection
*
* This will activate a internal WS_USER_CMD_EXIT flag that will
* force disconnect when the next iteration begins.
* @note it will create a copy of the reason string
* @param ws the WebSockets handle created with ws_init()
* @param code the WebSockets CLOSE opcode
* @param reason the close reason
* @param len the reason length
*/
void ws_close(struct websockets *ws,
const enum ws_close_reason code,
const char reason[],
const size_t len);
/**
* @brief Add a header field/value pair
*
* @param ws the WebSockets handle created with ws_init()
* @param field the header field
* @param value the header value
*/
void ws_add_header(struct websockets *ws,
const char field[],
const char value[]);
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* WEBSOCKETS_H */
+872
View File
@@ -0,0 +1,872 @@
/*
vector.h - Vectors for C
Version: 1.0.0
Description:
Header-only library for vectors in C. Memory safe against the mistakes it can
detect, but not thread-safe (you must implement your own synchronization
mechanisms).
Designed for C23 onwards, but should be compatible with older standards (tested
from C99 onwards).
Guarantees:
- A vector_t must never be duplicated by copying the struct. Two vector_t values
sharing one data pointer both believe they own it, and whichever is destroyed
or moved from first leaves the other holding freed memory. Copying the pointer
(vector_t* b = a;) is fine - that is one vector with two names, and it must be
destroyed exactly once.
- To hand a vector's data to another vector, use vector_move(). The source is
emptied, freed and its pointer set to NULL, so there is only ever one owner.
- To get a second, independent vector with the same contents, use
vector_deep_copy(), which copies the data as well as the vector itself. Deep
copies are only available for vectors without an element destructor, as a
byte-wise copy of owning elements would result in a double free.
- No gaps in the raw data array.
- When a vector is resized, the data is reallocated to a new memory location,
so any pointers into the old data are invalidated. There is no way to
preserve them; re-fetch with vector_get() after the resize.
- Order of elements is preserved.
- It is safe to pass a pointer into a vector's own data as the source element
of vector_push_back(), vector_insert() and vector_set(). Each of them handles
the aliasing in whichever way suits it, so the value survives the reallocation
or the shifting of elements. On a vector with an element destructor this is
rejected with -1 instead, as a byte-wise copy would leave two slots owning the
same memory. To duplicate an owning element, copy what it owns yourself and
push that.
- vector_take_at() and vector_take_back() transfer ownership of the element to the
caller, and do not call the destructor on it. It is the caller's responsibility
to free whatever the element owns.
Element destructors:
A vector may be given an element destructor with vector_set_destructor(). It is
called for every element that leaves the vector, i.e. by vector_pop_back(),
vector_pop_at(), vector_set() (on the element being overwritten), vector_clear()
and vector_destroy().
The destructor is NOT called by vector_take_back() or vector_take_at(), as these
functions transfer ownership of the element to the caller. It is the caller's
responsibility to free whatever the element owns.
The destructor receives a pointer to the element's slot inside the vector's data
array - NOT a pointer that was returned by malloc(). It must free whatever the
element owns, and must never free the pointer it was handed, as that memory
belongs to the vector. For a vector of char* this means:
void free_str(void* element) { free(*(char**)element); }
vector_t* vec = vector_create(sizeof(char*));
vector_set_destructor(vec, free_str);
// ... use the vector ...
vector_destroy(&vec); // Takes the address of your pointer, and NULLs it
A warning regarding misuse:
The vector manages its own buffer correctly: no leaks, no double frees of the data
array, no use of stale pointers internally. It cannot reason about what your
elements own that is what the destructor is for, and it is your responsibility
to set one and to avoid duplicating owned pointers between slots.
License:
MIT License
Copyright (c) 2026 DcruBro
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef VECTOR_H
#define VECTOR_H
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
typedef void (*vector_destructor_t)(void* element); // Function pointer type for element destructor
/*
A vector. Do not construct, copy or modify this struct directly - the fields are
visible only because the functions below are inline. Create vectors with
vector_create() and use the accessors. A vector_t that did not come from
vector_create() is not a valid vector, and the functions will reject it where
they can and misbehave where they cannot.
*/
typedef struct {
size_t size; // Number of elements in the vector
size_t capacity; // Allocated capacity of the vector
size_t element_size; // Size of each element in the vector
void *data; // Pointer to the raw data array
vector_destructor_t destructor; // Function pointer to the element destructor
} vector_t;
/* Forward declarations */
static inline int vector_is_aliased(const vector_t* vec, const void* ptr);
static inline vector_t* vector_create(size_t element_size);
static inline int vector_set_destructor(vector_t* vec, vector_destructor_t destructor);
static inline vector_destructor_t vector_get_destructor(const vector_t* vec);
static inline int vector_reserve(vector_t* vec, size_t new_capacity);
static inline int vector_grow(vector_t* vec);
static inline int vector_prune(vector_t* vec);
static inline int vector_push_back(vector_t* vec, const void* element);
static inline int vector_pop_back(vector_t* vec);
static inline int vector_pop_at(vector_t* vec, size_t index);
static inline int vector_take_at(vector_t* vec, size_t index, void* out);
static inline int vector_take_back(vector_t* vec, void* out);
static inline int vector_insert(vector_t* vec, size_t index, const void* element);
static inline int vector_clear(vector_t* vec);
static inline int vector_set(vector_t* vec, size_t index, const void* element);
static inline int vector_is_empty(const vector_t* vec);
static inline void* vector_front(vector_t* vec);
static inline const void* vector_front_const(const vector_t* vec);
static inline void* vector_back(vector_t* vec);
static inline const void* vector_back_const(const vector_t* vec);
static inline void* vector_get(vector_t* vec, size_t index);
static inline const void* vector_get_const(const vector_t* vec, size_t index);
static inline size_t vector_size(const vector_t* vec);
static inline size_t vector_capacity(const vector_t* vec);
static inline size_t vector_element_size(const vector_t* vec);
static inline const void* vector_as_c_array(const vector_t* vec);
static inline void* vector_as_c_array_mutable(vector_t* vec);
static inline int vector_move(vector_t* dest, vector_t** src);
static inline vector_t* vector_deep_copy(const vector_t* vec);
static inline int vector_destroy(vector_t** vec);
/*
@brief Checks whether a pointer points inside the vector's own data array. Used internally to make the write functions safe against self-referential input.
@param vec A pointer to the vector to check against.
@param ptr The pointer to check.
@return 1 if the pointer lies within the vector's allocated buffer, 0 otherwise.
@attention This is an internal helper. You are not expected to call it directly, but it is harmless if you do.
*/
static inline int vector_is_aliased(const vector_t* vec, const void* ptr) {
if (!vec || !ptr || !vec->data) {
return 0; // Nothing to alias
}
// Compared as integers rather than pointers, as comparing pointers into
// different objects is not well defined.
uintptr_t base = (uintptr_t)vec->data;
uintptr_t end = base + (vec->capacity * vec->element_size);
uintptr_t target = (uintptr_t)ptr;
return target >= base && target < end;
}
/*
@brief Creates a new vector with the specified element size.
@param element_size The size of each element in the vector. Call with sizeof(type)
@return A pointer to the newly created vector, or NULL if allocation fails. The vector lives on the heap.
@attention The vector must be destroyed with vector_destroy() to free its memory. Failing to do so will result in a memory leak.
@attention The vector's initial capacity is set to 10. If you want to change the initial capacity globally, set the DLIBC_VECTOR_INITIAL_CAPACITY macro before including this header file. The initial capacity must be greater than 0.
@attention The vector's element size must be greater than 0. If you pass 0, the function will return NULL.
@attention The vector is created without an element destructor. If your elements own memory of their own, set one with vector_set_destructor().
*/
static inline vector_t* vector_create(size_t element_size) {
if (element_size == 0) {
return NULL; // Invalid element size
}
#ifndef DLIBC_VECTOR_INITIAL_CAPACITY
size_t initial_capacity = 10; // Default initial capacity
#else
#if DLIBC_VECTOR_INITIAL_CAPACITY <= 0
#error "DLIBC_VECTOR_INITIAL_CAPACITY must be greater than 0"
#endif
size_t initial_capacity = DLIBC_VECTOR_INITIAL_CAPACITY; // Use the macro if defined
#endif
if (SIZE_MAX / element_size < initial_capacity) {
return NULL; // Prevent overflow
}
vector_t* vec = (vector_t*)malloc(sizeof(vector_t));
if (!vec) {
return NULL; // Allocation failed
}
vec->size = 0;
vec->capacity = initial_capacity;
vec->element_size = element_size;
vec->destructor = NULL; // Initialize destructor to NULL
vec->data = malloc(vec->capacity * vec->element_size);
if (!vec->data) {
free(vec);
return NULL; // Allocation failed
}
return vec;
}
/*
@brief Sets the destructor function for the vector's elements. This function will be called on each element as it leaves the vector, allowing for custom cleanup of dynamically allocated memory within the elements.
@param vec A pointer to the vector for which to set the destructor.
@param destructor A function pointer to the destructor function, or NULL to remove the current one. The function should take a single void* parameter, which will be a pointer to the element to be destroyed.
@return 0 on success, -1 if the vector is NULL.
@attention If you do not set a destructor function, the vector will not automatically free any dynamically allocated memory within its elements when it is destroyed. You must ensure that you free any such memory manually before destroying the vector to avoid memory leaks.
@attention The destructor is passed a pointer to the element's slot inside the vector's data array, not a pointer returned by malloc(). It must free what the element owns and must never free the pointer it is given. For a vector of char*, the destructor body is free(*(char**)element);
@attention This assumes that your destructor function is valid and safe to call. If it isn't, bad things will happen and it will not be nice to watch.
@attention Set the destructor before adding any elements. Setting it on a vector that already holds elements is allowed, but elements that were removed beforehand will not have been destroyed.
*/
static inline int vector_set_destructor(vector_t* vec, vector_destructor_t destructor) {
if (!vec) {
return -1; // Invalid vector
}
vec->destructor = destructor;
return 0; // Success
}
/*
@brief Gets the destructor function currently set on the vector.
@param vec A pointer to the vector whose destructor is to be retrieved.
@return The vector's destructor function pointer, or NULL if the vector is NULL or has no destructor set.
*/
static inline vector_destructor_t vector_get_destructor(const vector_t* vec) {
if (!vec) {
return NULL; // Invalid vector
}
return vec->destructor;
}
/*
@brief Ensures the vector has room for at least the specified number of elements.
@param vec A pointer to the vector for which to reserve space.
@param new_capacity The minimum capacity the vector should have.
@return 0 on success, -1 if the vector is NULL, its element size is zero, or allocation fails.
@attention Capacity never decreases. If the vector already has room, this is a no-op and reports success.
@attention If the buffer does grow, it is reallocated, so any pointers into the vector's data are invalidated. Use vector_prune() to release unused memory.
*/
static inline int vector_reserve(vector_t* vec, size_t new_capacity) {
if (!vec) {
return -1; // Invalid vector
}
if (vec->element_size == 0) {
return -1; // Not a usable vector; also guards the division below
}
if (new_capacity <= vec->capacity) {
return 0; // Already have the room. Never shrink
}
if (new_capacity > SIZE_MAX / vec->element_size) {
return -1; // Prevent overflow
}
void* new_data = realloc(vec->data, new_capacity * vec->element_size);
if (!new_data) {
return -1; // Allocation failed
}
vec->data = new_data;
vec->capacity = new_capacity;
return 0; // Success
}
/*
@brief Grows the vector's capacity to make room for at least one more element. Used internally by the functions that add elements.
@param vec A pointer to the vector to grow.
@return 0 on success, -1 if the vector is NULL or allocation fails.
@attention This is an internal helper. You are not expected to call it directly.
*/
static inline int vector_grow(vector_t* vec) {
if (!vec) {
return -1; // Invalid vector
}
if (vec->capacity > SIZE_MAX / 2) {
return -1; // Prevent overflow
}
size_t new_capacity = vec->capacity > 0 ? vec->capacity * 2 : 1; // Double the capacity, or set to 1 if it was 0
return vector_reserve(vec, new_capacity);
}
/*
@brief Prunes the vector to free unused memory. If the vector's size is less than its capacity, this function will reallocate the vector's data array to match its size, freeing any unused memory.
@param vec A pointer to the vector to be pruned.
@return 0 on success, -1 if the vector is NULL or allocation fails.
@attention After calling this function, the vector's capacity will be equal to its size. Any pointers to the old data will be invalidated.
@attention Capacity may never drop below 1, even if the vector is empty.
*/
static inline int vector_prune(vector_t* vec) {
if (!vec) {
return -1; // Invalid vector
}
if (vec->element_size == 0) {
return -1; // Not a usable vector; also guards the division below
}
if (vec->size < vec->capacity) {
size_t new_capacity = vec->size > 0 ? vec->size : 1; // Ensure capacity is at least 1
if (new_capacity > SIZE_MAX / vec->element_size) {
return -1; // Prevent overflow
}
void* new_data = realloc(vec->data, new_capacity * vec->element_size);
if (!new_data) {
return -1; // Allocation failed
}
vec->data = new_data;
vec->capacity = new_capacity;
}
return 0; // Success
}
/*
@brief Adds an element to the end of the vector.
@param vec A pointer to the vector to which the element will be added.
@param element A pointer to the element to be added. The element will be copied into the vector's data array.
@return 0 on success, -1 if the vector is NULL, the element is NULL, if the element is an owning element that is aliased in the vector and a destructor is set, or if reservation fails.
@attention The element must be a pointer to a valid memory location containing data of the same type as the vector's element type. The vector will make a copy of the data, so the original element can be modified or freed after this function returns.
@attention The element may point into the vector's own data array. If growing the vector moves the data, the pointer is followed to its new location automatically.
*/
static inline int vector_push_back(vector_t* vec, const void* element) {
if (!vec || !element) {
return -1; // Invalid vector or element
}
if (vec->destructor && vector_is_aliased(vec, element)) {
return -1; // Refuse to push an owning element that lives inside the vector
}
if (vec->size >= vec->capacity) {
// If the element lives inside our own data array, remember where it sits
// so that we can find it again after the data has been reallocated.
// This must happen before the growth, while the old buffer is still valid
size_t offset = 0;
int aliased = vector_is_aliased(vec, element);
if (aliased) {
offset = (size_t)((const char*)element - (const char*)vec->data);
}
if (vector_grow(vec) != 0) {
return -1; // Failed to grow the vector
}
if (aliased) {
element = (const char*)vec->data + offset;
}
}
// Copy the new element into the vector's data array
memmove((char*)vec->data + (vec->size * vec->element_size), element, vec->element_size);
vec->size++;
return 0; // Success
}
/*
@brief Removes the last element from the vector.
@param vec A pointer to the vector from which the element will be removed.
@return 0 on success, -1 if the vector is NULL or empty.
@attention After calling this function, the vector's size will be reduced by one. The memory occupied by the removed element will not be freed automatically unless a destructor is set (in which case, the destructor will be called on the element being removed if it is set). Use vector_take_back() to retrieve the last element, remove it from the vector, and NOT call the destructor on it (hands ownership to the caller).
*/
static inline int vector_pop_back(vector_t* vec) {
if (!vec || vec->size == 0) {
return -1; // Invalid vector or empty vector
}
if (vec->destructor) {
void* element = (char*)vec->data + ((vec->size - 1) * vec->element_size);
vec->destructor(element); // Call the destructor for the last element
}
vec->size--;
return 0; // Success
}
/*
@brief Removes the element at the specified index from the vector.
@param vec A pointer to the vector from which the element will be removed.
@param index The index of the element to be removed.
@return 0 on success, -1 if the vector is NULL or the index is out of bounds.
@attention After calling this function, the vector's size will be reduced by one. The memory occupied by the removed element will not be freed automatically unless a destructor is set (in which case, the destructor will be called on the element being removed if it is set). Use vector_take_at() to retrieve the element at the specified index, remove it from the vector, and NOT call the destructor on it (hands ownership to the caller).
*/
static inline int vector_pop_at(vector_t* vec, size_t index) {
if (!vec || index >= vec->size) {
return -1; // Invalid vector or index out of bounds
}
if (vec->destructor) {
void* element = (char*)vec->data + (index * vec->element_size);
vec->destructor(element); // Call the destructor for the element to be removed
}
// Move elements after the index one position to the left
if (index + 1 < vec->size) {
memmove((char*)vec->data + (index * vec->element_size),
(char*)vec->data + ((index + 1) * vec->element_size),
(vec->size - index - 1) * vec->element_size);
}
vec->size--;
return 0; // Success
}
/*
@brief Removes the element at the specified index and transfers ownership of it to the caller.
@param vec A pointer to the vector from which the element will be taken.
@param index The index of the element to take.
@param out A pointer to a buffer of at least vector_element_size(vec) bytes, which the element is copied into.
@return 0 on success, -1 if the vector is NULL, out is NULL, out points into the vector's own data, or the index is out of bounds.
@attention The element destructor is deliberately NOT called. Whatever the element owns becomes the caller's responsibility to free.
@attention out must not point into the vector's own data array. Doing so would leave two slots owning the same memory and is rejected with -1.
*/
static inline int vector_take_at(vector_t* vec, size_t index, void* out) {
if (!vec || index >= vec->size || !out) {
return -1; // Invalid vector, index out of bounds, or output pointer is NULL
}
if (vector_is_aliased(vec, out)) {
return -1; // Refuse to take an element into a pointer that lives inside the vector
}
void* slot = (char*)vec->data + (index * vec->element_size);
memcpy(out, slot, vec->element_size); // Copy the element to the output pointer
// Move elements after the index one position to the left
if (index + 1 < vec->size) {
memmove(slot,
(char*)vec->data + ((index + 1) * vec->element_size),
(vec->size - index - 1) * vec->element_size);
}
vec->size--;
return 0; // Success
}
/*
@brief Removes the last element from the vector and transfers ownership of it to the caller.
@param vec A pointer to the vector from which the element will be taken.
@param out A pointer to a buffer of at least vector_element_size(vec) bytes, which the element is copied into.
@return 0 on success, -1 if the vector is NULL, empty, if out aliases the vector's data, or out is NULL.
@attention The element destructor is deliberately NOT called. Whatever the element owns becomes the caller's responsibility to free.
*/
static inline int vector_take_back(vector_t* vec, void* out) {
if (!vec || vec->size == 0 || !out) {
return -1; // Invalid vector, empty vector, or output pointer is NULL
}
return vector_take_at(vec, vec->size - 1, out); // Take the last element
}
/*
@brief Inserts an element at the specified index in the vector.
@param vec A pointer to the vector into which the element will be inserted.
@param index The index at which to insert the element.
@param element A pointer to the element to be inserted. The element will be copied into the vector's data array.
@return 0 on success, -1 if the vector is NULL, the element is NULL, the index is out of bounds, if the element is an owning element that is aliased in the vector and a destructor is set, or if reservation fails.
@attention After calling this function, the vector's size will be increased by one and every element from the index onwards will have moved one position to the right. Any pointers into the vector are invalidated.
@attention The element may point into the vector's own data array. Its value is copied aside first, so that neither the reallocation nor the shifting of elements can corrupt it.
*/
static inline int vector_insert(vector_t* vec, size_t index, const void* element) {
if (!vec || !element || index > vec->size) {
return -1; // Invalid vector, element, or index out of bounds
}
if (vec->destructor && vector_is_aliased(vec, element)) {
return -1; // Refuse to insert an owning element that lives inside the vector
}
// If the element lives inside our own data array, take a copy of it now. Both
// the reservation below and the shifting of elements would otherwise destroy
// the value before it can be read
void* temp = NULL;
if (vector_is_aliased(vec, element)) {
temp = malloc(vec->element_size);
if (!temp) {
return -1; // Allocation failed
}
memcpy(temp, element, vec->element_size);
element = temp;
}
if (vec->size >= vec->capacity) {
if (vector_grow(vec) != 0) {
free(temp);
return -1; // Failed to grow the vector
}
}
// Move elements after the index one position to the right
memmove((char*)vec->data + ((index + 1) * vec->element_size),
(char*)vec->data + (index * vec->element_size),
(vec->size - index) * vec->element_size);
// Copy the new element into the vector's data array at the specified index
memcpy((char*)vec->data + (index * vec->element_size), element, vec->element_size);
vec->size++;
free(temp); // Harmless if no copy was needed, as temp is NULL in that case
return 0; // Success
}
/*
@brief Clears all elements from the vector.
@param vec A pointer to the vector to be cleared.
@return 0 on success, -1 if the vector is NULL.
@attention After calling this function, the vector's size will be zero. The memory occupied by the elements will not be freed automatically unless a destructor is set.
@attention The vector's capacity is left untouched, so the vector may be refilled without reallocating. Call vector_prune() afterwards if you want the memory back.
*/
static inline int vector_clear(vector_t* vec) {
if (!vec) {
return -1; // Invalid vector
}
if (vec->destructor) {
for (size_t i = 0; i < vec->size; ++i) {
void* element = (char*)vec->data + (i * vec->element_size);
vec->destructor(element); // Call the destructor for each element
}
}
vec->size = 0;
return 0; // Success
}
/*
@brief Sets the element at the specified index in the vector, overwriting whatever was there before.
@param vec A pointer to the vector in which to set the element.
@param index The index of the element to set.
@param element A pointer to the element to set. The element will be copied into the vector's data array.
@return 0 on success, -1 if the vector is NULL, the element is NULL, if the element is an owning element that is aliased in the vector and a destructor is set, or if the index is out of bounds.
@attention This never grows the vector. The index must be below the current size.
@attention If a destructor is set, it is called on the element being overwritten before the new value is copied in.
@attention Setting an element to itself is a no-op and reports success.
@attention On a vector without a destructor, the element may point into the
vector's own data array; the copy handles any overlap. On a vector with a
destructor this is refused with -1, as it would leave two slots owning the
same memory.
*/
static inline int vector_set(vector_t* vec, size_t index, const void* element) {
if (!vec || !element || index >= vec->size) {
return -1; // Invalid vector, element, or index out of bounds
}
void* slot = (char*)vec->data + (index * vec->element_size);
if (slot == element) {
return 0; // Setting the element to itself, no action needed
}
if (vec->destructor && vector_is_aliased(vec, element)) {
return -1; // Refuse to set an owning element that lives inside the vector
}
if (vec->destructor) {
vec->destructor(slot); // Call the destructor for the element being overwritten
}
memmove(slot, element, vec->element_size); // memmove, as the element may overlap the slot
return 0; // Success
}
/*
@brief Checks if the vector is empty.
@param vec A pointer to the vector to check.
@return 1 if the vector is empty, 0 if it is not empty.
@attention If the vector is NULL, this function will return 1 (is empty) to indicate that the vector is invalid.
*/
static inline int vector_is_empty(const vector_t* vec) {
if (!vec) {
return 1; // Invalid vector
}
return vec->size == 0;
}
/*
@brief Gets a pointer to the first element in the vector.
@param vec A pointer to the vector from which to get the element.
@return A pointer to the first element in the vector, or NULL if the vector is NULL or empty.
@attention The returned pointer is a generic. You should cast it to the appropriate type before using it. The pointer will become invalid if the vector is resized or destroyed.
*/
static inline void* vector_front(vector_t* vec) {
if (!vec || vec->size == 0) {
return NULL; // Invalid vector or empty vector
}
return vec->data;
}
/*
@brief Gets a const pointer to the first element in the vector. Cannot be used to modify the element.
@param vec A pointer to the vector from which to get the element.
@return A constant pointer to the first element in the vector, or NULL if the vector is NULL or empty.
@attention The returned pointer is a generic. You should cast it to the appropriate type before using it. The pointer will become invalid if the vector is resized or destroyed.
*/
static inline const void* vector_front_const(const vector_t* vec) {
if (!vec || vec->size == 0) {
return NULL; // Invalid vector or empty vector
}
return vec->data;
}
/*
@brief Gets a pointer to the last element in the vector.
@param vec A pointer to the vector from which to get the element.
@return A pointer to the last element in the vector, or NULL if the vector is NULL or empty.
@attention The returned pointer is a generic. You should cast it to the appropriate type before using it. The pointer will become invalid if the vector is resized or destroyed.
*/
static inline void* vector_back(vector_t* vec) {
if (!vec || vec->size == 0) {
return NULL; // Invalid vector or empty vector
}
return (char*)vec->data + ((vec->size - 1) * vec->element_size);
}
/*
@brief Gets a const pointer to the last element in the vector. Cannot be used to modify the element.
@param vec A pointer to the vector from which to get the element.
@return A constant pointer to the last element in the vector, or NULL if the vector is NULL or empty.
@attention The returned pointer is a generic. You should cast it to the appropriate type before using it. The pointer will become invalid if the vector is resized or destroyed.
*/
static inline const void* vector_back_const(const vector_t* vec) {
if (!vec || vec->size == 0) {
return NULL; // Invalid vector or empty vector
}
return (const char*)vec->data + ((vec->size - 1) * vec->element_size);
}
/*
@brief Gets a pointer to the element at the specified index in the vector.
@param vec A pointer to the vector from which to get the element.
@param index The index of the element to get.
@return A pointer to the element at the specified index, or NULL if the vector is NULL or the index is out of bounds.
@attention The returned pointer is a generic. You should cast it to the appropriate type before using it. The pointer will become invalid if the vector is resized or destroyed.
@attention For most read-only operations, consider using vector_get_const() instead, which returns a const pointer to the element.
*/
static inline void* vector_get(vector_t* vec, size_t index) {
if (!vec || index >= vec->size) {
return NULL; // Invalid vector or index out of bounds
}
return (char*)vec->data + (index * vec->element_size);
}
/*
@brief Gets a constant pointer to the element at the specified index in the vector. Cannot be used to modify the element.
@param vec A pointer to the vector from which to get the element.
@param index The index of the element to get.
@return A constant pointer to the element at the specified index, or NULL if the vector is NULL or the index is out of bounds.
@attention The returned pointer is a generic. You should cast it to the appropriate type before using it. The pointer will become invalid if the vector is resized or destroyed.
*/
static inline const void* vector_get_const(const vector_t* vec, size_t index) {
if (!vec || index >= vec->size) {
return NULL; // Invalid vector or index out of bounds
}
return (const char*)vec->data + (index * vec->element_size);
}
/*
@brief Gets the size of the vector.
@param vec A pointer to the vector whose size is to be retrieved.
@return The size of the vector, or 0 if the vector is NULL.
*/
static inline size_t vector_size(const vector_t* vec) {
if (!vec) {
return 0; // Invalid vector
}
return vec->size;
}
/*
@brief Gets the capacity of the vector.
@param vec A pointer to the vector whose capacity is to be retrieved.
@return The capacity of the vector, or 0 if the vector is NULL.
*/
static inline size_t vector_capacity(const vector_t* vec) {
if (!vec) {
return 0; // Invalid vector
}
return vec->capacity;
}
/*
@brief Gets the size of each element in the vector.
@param vec A pointer to the vector whose element size is to be retrieved.
@return The size of each element in the vector, or 0 if the vector is NULL.
*/
static inline size_t vector_element_size(const vector_t* vec) {
if (!vec) {
return 0; // Invalid vector
}
return vec->element_size;
}
/*
@brief Gets a pointer to the underlying C array of the vector. Cannot modify the vector through this pointer.
@param vec A pointer to the vector whose underlying array is to be retrieved.
@return A pointer to the underlying C array, or NULL if the vector is NULL.
@attention The returned pointer is a generic. You should cast it to the appropriate type before using it. The pointer will become invalid if the vector is resized or destroyed.
*/
static inline const void* vector_as_c_array(const vector_t* vec) {
if (!vec) {
return NULL; // Invalid vector
}
return vec->data;
}
/*
@brief Gets a pointer to the underlying C array of the vector. Can be used to modify the vector's elements.
@param vec A pointer to the vector whose underlying array is to be retrieved.
@return A pointer to the underlying C array, or NULL if the vector is NULL.
@attention The returned pointer is a generic. You should cast it to the appropriate type before using it. The pointer will become invalid if the vector is resized or destroyed.
*/
static inline void* vector_as_c_array_mutable(vector_t* vec) {
if (!vec) {
return NULL; // Invalid vector
}
return vec->data;
}
/*
@brief Moves a vector to another vector, transferring ownership of the data. The destination vector will take ownership of the source vector's data, its element size and its destructor.
@param dest A pointer to the destination vector.
@param src A pointer to the pointer holding the source vector. It will be set to NULL.
@return 0 on success, -1 if the destination vector is NULL, the source pointer is NULL, the source vector is NULL, or both vectors share the same data pointer.
@attention After calling this function, the source vector will be freed (excluding data) and the source pointer will be set to NULL, so it cannot be used again.
@attention The destination vector's existing elements will be destroyed (using the destination's own destructor, if it has one) and its data freed. Ensure that you do not need the existing data before calling this function.
@attention Moving a vector onto itself is a no-op and reports success, leaving the vector untouched.
@attention Two vectors can only share a data pointer if a vector_t was duplicated by copying the struct, which is never valid. The move is refused rather than freeing a buffer the source still points at.
*/
static inline int vector_move(vector_t* dest, vector_t** src) {
if (!dest || !src || !*src) {
return -1; // Invalid vectors
}
if (dest == *src) {
return 0; // Moving to itself, no action needed
}
if (dest->data == (*src)->data) {
return -1; // Refuse to move a vector onto another vector that shares its data
// Realistically, this is unreachable via legal use, but... safety.
// Or something. You WILL trigger this if you do shallow copies. DO NOT!
}
// Destroy the destination vector's existing elements with its own destructor,
// then free its data, as it is about to be replaced
vector_clear(dest);
free(dest->data);
// Transfer ownership of the source vector's data to the destination vector
dest->size = (*src)->size;
dest->capacity = (*src)->capacity;
dest->element_size = (*src)->element_size;
dest->data = (*src)->data;
dest->destructor = (*src)->destructor; // The elements keep the cleanup they came with
// Reset the source vector to an empty state
(*src)->size = 0;
(*src)->capacity = 0;
(*src)->element_size = 0;
(*src)->data = NULL;
(*src)->destructor = NULL;
free(*src); // Free the source vector structure, but not its data (ownership transferred)
*src = NULL;
return 0; // Success
}
/*
@brief Creates a deep copy of the vector, including its data.
@param vec A pointer to the vector to be copied.
@return A pointer to the newly created deep copy of the vector, or NULL if allocation fails, if the input vector is NULL, or if the input vector has a destructor set.
@attention The returned vector must be destroyed with vector_destroy() to free its memory. Failing to do so will result in a memory leak.
@attention Vectors with a destructor cannot be deep copied and this function will return NULL for them. The elements are copied byte for byte, so any memory they own would end up owned by both vectors and freed twice. If you need to copy such a vector, do it by hand: create a new vector and push copies of the elements into it yourself.
*/
static inline vector_t* vector_deep_copy(const vector_t* vec) {
if (!vec) {
return NULL; // Invalid vector
}
if (vec->destructor) {
return NULL; // Refuse to byte-copy elements that own memory
}
vector_t* new_vec = vector_create(vec->element_size);
if (!new_vec) {
return NULL; // Allocation failed
}
int r = vector_reserve(new_vec, vec->capacity);
if (r != 0) {
vector_destroy(&new_vec);
return NULL; // Allocation failed
}
if (vec->size > 0) {
memcpy(new_vec->data, vec->data, vec->size * vec->element_size);
new_vec->size = vec->size;
} // vector_create() already sets size to 0 by default
return new_vec;
}
/*
@brief Destroys the vector and frees its memory.
@param vec A pointer to the pointer holding the vector to be destroyed. It will be set to NULL.
@return 0 on success, or if the vector was already NULL. -1 only if the pointer itself is NULL.
@attention After calling this function, the vector pointer should not be used again (It will be set to NULL). Accessing it after destruction will lead to undefined behavior.
@attention Only the pointer you pass in is set to NULL. Copies of that pointer held elsewhere are left dangling and must not be used.
@attention If stored elements own memory of their own, set a destructor with vector_set_destructor() to have it cleaned up here. Otherwise, the vector will only free the memory allocated for the data array and the vector structure itself, but not any dynamically allocated memory within the elements.
*/
static inline int vector_destroy(vector_t** vec) {
if (!vec) {
return -1; // Invalid pointer
}
if (!(*vec)) {
return 0; // Already NULL, nothing to destroy
}
vector_t* target = *vec;
if (target->destructor) {
for (size_t i = 0; i < target->size; ++i) {
void* element = (char*)target->data + (i * target->element_size);
target->destructor(element); // Call the destructor for each element
}
}
free(target->data);
free(target);
*vec = NULL; // Set the pointer to NULL to avoid dangling references
return 0; // Success
}
#endif // VECTOR_H