Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ DISCORD_TOKEN=your_bot_token_here
#DISCORD_REPLY=
#DISCORD_REACTION=👑
#LOG_LEVEL=info
#LOG_SAMPLE_RATE=1.0
#KING_VERSION=0.1
#KING_COMMIT=
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Built with Cosmopolitan Libc and mbedtls for TLS. ~1000 lines of C in `src/`.
| `src/net.c` | TCP + mbedtls TLS connect, custom BIO callbacks, HTTPS request helper |
| `src/ws.c` | Minimal WebSocket client (masked client frames, text frames only) |
| `src/json.c` | Targeted key extraction — not a general-purpose parser |
| `src/log.h` | Header-only runtime log levels; compile-time stripping via `LOG_LEVEL_COMPILE` |
| `src/log.h` / `src/log.c` | Wide-event NDJSON logger (`log_emit`); info/error; env: `LOG_LEVEL`, `LOG_SAMPLE_RATE`, `KING_VERSION`, `KING_COMMIT` |
| `src/cacerts.h` | Embedded root CA certificates (PEM) |

### Key structs
Expand Down
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ MBEDTLS_LIBS = $(MBEDTLS_LIB)/libmbedtls.a \

# ── bot sources ──────────────────────────────────────────────────
BIN = king
SRCS = src/king.c src/net.c src/ws.c src/json.c src/str.c
SRCS = src/king.c src/net.c src/ws.c src/json.c src/str.c src/log.c
OBJS = $(SRCS:.c=.o)

# ── targets ──────────────────────────────────────────────────────
Expand All @@ -79,7 +79,7 @@ $(MBEDTLS_LIBS):

# ── tests ────────────────────────────────────────────────────────
TEST_BIN = test_king
TEST_SRCS = src/test_king.c src/json.c src/str.c
TEST_SRCS = src/test_king.c src/json.c src/str.c src/log.c
TEST_OBJS = $(TEST_SRCS:.c=.o)

test: $(TEST_BIN)
Expand All @@ -89,15 +89,15 @@ $(TEST_BIN): $(TEST_OBJS)
$(CC) $(LDFLAGS) -o $@ $(TEST_OBJS)

# ── lint ─────────────────────────────────────────────────────────
LINT_SRCS = src/king.c src/net.c src/ws.c src/json.c src/str.c src/test_king.c
LINT_SRCS = src/king.c src/net.c src/ws.c src/json.c src/str.c src/log.c src/test_king.c
LINT_HDRS = src/json.h src/str.h src/net.h src/ws.h src/log.h src/test.h src/cacerts.h

lint:
clang-format --dry-run --Werror $(LINT_SRCS) $(LINT_HDRS)
clang-tidy $(LINT_SRCS) -- -Isrc -I$(MBEDTLS_INC) -Wno-implicit-function-declaration

clean:
rm -f $(BIN) $(TEST_BIN) $(OBJS) src/test_king.o *.com.dbg *.aarch64.elf
rm -f $(BIN) $(TEST_BIN) $(OBJS) src/test_king.o src/log.o *.com.dbg *.aarch64.elf
rm -rf .aarch64

mbedclean:
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,14 @@ You need **Message Content Intent** enabled in the [Discord Developer Portal](ht
| `DISCORD_TOKEN` | *(required)* | Bot token (no `Bot ` prefix) |
| `DISCORD_REPLY` | *(disabled)* | Reply text, omit to disable |
| `DISCORD_REACTION` | `👑` | Reaction emoji |
| `LOG_LEVEL` | `info` | `error`, `info`, or `debug` |
| `LOG_LEVEL` | `info` | `error` or `info` (structured NDJSON on stderr) |
| `LOG_SAMPLE_RATE` | `1.0` | Tail-sample rate for non-error / non-match events (`0.0`–`1.0`) |
| `KING_VERSION` | `0.1` | Included on every log event |
| `KING_COMMIT` | *(empty)* | Optional deploy/commit id on every event |

### Logging

Logs follow [wide events / canonical log lines](https://www.loggingsucks.com): one JSON object per unit of work (session, hello, ready, matched message, failures) — not a step-by-step diary. Errors and `matched_king` events are always kept; other info events respect `LOG_SAMPLE_RATE`. Tokens are never logged.

## Test / lint

Expand Down
84 changes: 34 additions & 50 deletions src/json.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,28 @@ static const char *skip_ws(const char *p) {
return p;
}

/* Parse a JSON number at *pp into *out. Advances nothing; returns 0 on failure. */
static int parse_int_value(const char *p, long long *out) {
if (!p) {
return 0;
}
int neg = 0;
if (*p == '-') {
neg = 1;
p++;
}
if (!isdigit((unsigned char)*p)) {
return 0;
}
long long v = 0;
while (isdigit((unsigned char)*p)) {
v = (v * 10) + (*p - '0');
p++;
}
*out = neg ? -v : v;
return 1;
}

static int read_string_value(const char *c, char *out, size_t cap) {
if (!c || *c != '"') {
return 0;
Expand Down Expand Up @@ -92,25 +114,7 @@ int json_get_string(const char *json, const char *key, char *out, size_t cap) {
}

int json_get_int(const char *json, const char *key, long long *out) {
const char *p = json_find_key(json, key);
if (!p) {
return 0;
}
int neg = 0;
if (*p == '-') {
neg = 1;
p++;
}
if (!isdigit((unsigned char)*p)) {
return 0;
}
long long v = 0;
while (isdigit((unsigned char)*p)) {
v = (v * 10) + (*p - '0');
p++;
}
*out = neg ? -v : v;
return 1;
return parse_int_value(json_find_key(json, key), out);
}

/* Find the byte range of a JSON object value. */
Expand Down Expand Up @@ -174,6 +178,7 @@ static const char *find_string_end(const char *s, const char *end) {
return NULL;
}

/* Scan for `key` at object depth 1 within [start, end); set *val to its value. */
static int scan_key(const char *start, const char *end, const char *key, const char **val) {
size_t klen = strlen(key);
int depth = 0;
Expand Down Expand Up @@ -206,45 +211,24 @@ static int scan_key(const char *start, const char *end, const char *key, const c
return 0;
}

int json_get_string_in_object(const char *json, const json_object_key *key, char *out, size_t cap) {
/* Resolve object_key.member_key to a value pointer inside json. */
static const char *value_in_object(const char *json, const json_object_key *key) {
const char *start;
const char *end;
const char *val = NULL;
if (!find_object_range(json, key->object_key, &start, &end)) {
return 0;
return NULL;
}
if (!scan_key(start, end, key->member_key, &val)) {
return 0;
return NULL;
}
return read_string_value(val, out, cap);
return val;
}

int json_get_string_in_object(const char *json, const json_object_key *key, char *out, size_t cap) {
return read_string_value(value_in_object(json, key), out, cap);
}

int json_get_int_in_object(const char *json, const json_object_key *key, long long *out) {
const char *start;
const char *end;
const char *val = NULL;
if (!find_object_range(json, key->object_key, &start, &end)) {
return 0;
}
if (!scan_key(start, end, key->member_key, &val)) {
return 0;
}
if (!val) {
return 0;
}
int neg = 0;
if (*val == '-') {
neg = 1;
val++;
}
if (!isdigit((unsigned char)*val)) {
return 0;
}
long long v = 0;
while (isdigit((unsigned char)*val)) {
v = (v * 10) + (*val - '0');
val++;
}
*out = neg ? -v : v;
return 1;
return parse_int_value(value_in_object(json, key), out);
}
Loading
Loading