diff --git a/src/json.c b/src/json.c index 59d26a4..6029bed 100644 --- a/src/json.c +++ b/src/json.c @@ -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; @@ -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. */ @@ -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; @@ -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); } diff --git a/src/king.c b/src/king.c index 08c0a52..3e6c5e2 100644 --- a/src/king.c +++ b/src/king.c @@ -19,6 +19,25 @@ #define GATEWAY_PATH "/?v=10&encoding=json" #define BUF_SIZE 65536 +/* Discord Gateway opcodes we care about. */ +enum { + OP_DISPATCH = 0, + OP_HEARTBEAT = 1, + OP_IDENTIFY = 2, + OP_RECONNECT = 7, + OP_INVALID_SESSION = 9, + OP_HELLO = 10, + OP_HEARTBEAT_ACK = 11, +}; + +/* GUILD_MESSAGES (512) | MESSAGE_CONTENT (32768) */ +#define GATEWAY_INTENTS 33280 + +#define HB_MAX_RETRIES 3 +#define HB_RETRY_USEC_START 100000 + +#define DEFAULT_REACTION "\xf0\x9f\x91\x91" /* 👑 */ + /* ── Monotonic clock ──────────────────────────────────────────── */ static long long now_ms(void) { @@ -54,17 +73,20 @@ static void send_message(const char *auth, const char *channel_id, const char *c char body[4096]; char path[256]; char resp[4096]; + json_escape(content, esc, sizeof(esc)); snprintf(body, sizeof(body), "{\"content\":\"%s\"}", esc); snprintf(path, sizeof(path), "/api/v10/channels/%s/messages", channel_id); + if (api_request(auth, "POST", path, body, resp, sizeof(resp)) != 0) { log_error("Failed to send message"); - } else { - int status = http_status_code(resp); - log_info("Message send status %d", status); - if (status >= 400) { - log_error("Message send failed: %.*s", 200, resp); - } + return; + } + + int status = http_status_code(resp); + log_info("Message send status %d", status); + if (status >= 400) { + log_error("Message send failed: %.*s", 200, resp); } } @@ -73,17 +95,20 @@ static void send_reaction(const char *auth, const char *channel_id, const char * if (!emoji || !*emoji) { return; } + char enc[256]; char path[512]; char resp[4096]; + url_encode(emoji, enc, sizeof(enc)); snprintf(path, sizeof(path), "/api/v10/channels/%s/messages/%s/reactions/%s/@me", channel_id, message_id, enc); + if (api_request(auth, "PUT", path, "", resp, sizeof(resp)) != 0) { log_error("Failed to add reaction"); - } else { - log_info("Reaction add status %d", http_status_code(resp)); + return; } + log_info("Reaction add status %d", http_status_code(resp)); } /* ── Gateway operations ───────────────────────────────────────── */ @@ -95,18 +120,30 @@ typedef struct { long long next_heartbeat; } gateway_state; +/* Split DISCORD_TOKEN into REST Authorization header and raw gateway token. */ +static void normalize_token(const char *token, char *auth, size_t auth_cap, char *gateway_token, + size_t gateway_cap) { + if (strncmp(token, "Bot ", 4) == 0) { + snprintf(auth, auth_cap, "%s", token); + snprintf(gateway_token, gateway_cap, "%s", token + 4); + } else { + snprintf(auth, auth_cap, "Bot %s", token); + snprintf(gateway_token, gateway_cap, "%s", token); + } +} + static int send_identify(tls_conn *c, const char *token) { char payload[1024]; int n = snprintf(payload, sizeof(payload), - "{\"op\":2,\"d\":{" + "{\"op\":%d,\"d\":{" "\"token\":\"%s\"," - "\"intents\":33280," /* GUILD_MESSAGES + MESSAGE_CONTENT */ + "\"intents\":%d," "\"properties\":{" "\"os\":\"cosmo\"," "\"browser\":\"king-cosmo\"," "\"device\":\"king-cosmo\"" "}}}", - token); + OP_IDENTIFY, token, GATEWAY_INTENTS); if (n <= 0 || (size_t)n >= sizeof(payload)) { return -1; } @@ -117,9 +154,9 @@ static int send_heartbeat(tls_conn *c, long long seq) { char payload[64]; int n; if (seq >= 0) { - n = snprintf(payload, sizeof(payload), "{\"op\":1,\"d\":%lld}", seq); + n = snprintf(payload, sizeof(payload), "{\"op\":%d,\"d\":%lld}", OP_HEARTBEAT, seq); } else { - n = snprintf(payload, sizeof(payload), "{\"op\":1,\"d\":null}"); + n = snprintf(payload, sizeof(payload), "{\"op\":%d,\"d\":null}", OP_HEARTBEAT); } if (n <= 0 || (size_t)n >= sizeof(payload)) { return -1; @@ -127,9 +164,29 @@ static int send_heartbeat(tls_conn *c, long long seq) { return ws_send_text(c, payload, (size_t)n); } +static int heartbeat_with_retry(gateway_state *s) { + int delay = HB_RETRY_USEC_START; + + for (int attempt = 0; attempt < HB_MAX_RETRIES; attempt++) { + if (send_heartbeat(&s->conn, s->sequence) == 0) { + log_debug("Heartbeat sent"); + s->next_heartbeat = now_ms() + s->heartbeat_interval; + return 0; + } + if (attempt + 1 < HB_MAX_RETRIES) { + log_error("Heartbeat failed, retrying..."); + usleep(delay); + delay *= 2; + } + } + log_error("Heartbeat failed"); + return -1; +} + static int handle_hello(gateway_state *s, const char *token, const char *payload) { long long interval = 0; json_object_key hb_key = {"d", "heartbeat_interval"}; + if (json_get_int_in_object(payload, &hb_key, &interval)) { s->heartbeat_interval = interval; } @@ -151,30 +208,27 @@ static void handle_message_create(const char *auth, const char *reply, const cha char content[2048]; char channel_id[64]; char message_id[64]; - json_object_key ck = {"d", "content"}; - json_object_key chk = {"d", "channel_id"}; - json_object_key mk = {"d", "id"}; + json_object_key content_key = {"d", "content"}; + json_object_key channel_key = {"d", "channel_id"}; + json_object_key id_key = {"d", "id"}; - if (!json_get_string_in_object(payload, &ck, content, sizeof(content))) { - return; - } - if (!json_get_string_in_object(payload, &chk, channel_id, sizeof(channel_id))) { + if (!json_get_string_in_object(payload, &content_key, content, sizeof(content)) || + !json_get_string_in_object(payload, &channel_key, channel_id, sizeof(channel_id)) || + !json_get_string_in_object(payload, &id_key, message_id, sizeof(message_id))) { return; } - if (!json_get_string_in_object(payload, &mk, message_id, sizeof(message_id))) { + + if (!contains_case(content, "king")) { + log_debug("Message ignored"); return; } - if (contains_case(content, "king")) { - log_info("Matched 'king' in channel %s", channel_id); - if (reply && *reply) { - send_message(auth, channel_id, reply); - } - if (reaction && *reaction) { - send_reaction(auth, channel_id, message_id, reaction); - } - } else { - log_debug("Message ignored"); + log_info("Matched 'king' in channel %s", channel_id); + if (reply && *reply) { + send_message(auth, channel_id, reply); + } + if (reaction && *reaction) { + send_reaction(auth, channel_id, message_id, reaction); } } @@ -182,21 +236,19 @@ static int wait_for_hello(gateway_state *s, const char *token, char *buf, size_t size_t len = 0; while (ws_read_text(&s->conn, buf, buf_cap, &len) == 0) { long long op = -1; - if (!json_get_int(buf, "op", &op)) { - continue; - } - if (op == 10) { + if (json_get_int(buf, "op", &op) && op == OP_HELLO) { return handle_hello(s, token, buf); } } return -1; } +/* Returns 0 = payload ready, 1 = idle/timeout, -1 = fatal. */ static int poll_and_read(gateway_state *s, char *buf, size_t buf_cap) { struct pollfd pfd = {.fd = s->conn.fd, .events = POLLIN}; - long long now = now_ms(); int timeout = 1000; + if (s->next_heartbeat > 0) { long long diff = s->next_heartbeat - now; timeout = diff < 0 ? 0 : (int)diff; @@ -210,23 +262,9 @@ static int poll_and_read(gateway_state *s, char *buf, size_t buf_cap) { now = now_ms(); if (s->next_heartbeat > 0 && now >= s->next_heartbeat) { - int hb_ok = 0; - int delay = 100000; - for (int retries = 0; retries < 3 && !hb_ok; retries++) { - if (send_heartbeat(&s->conn, s->sequence) == 0) { - hb_ok = 1; - } else if (retries < 2) { - log_error("Heartbeat failed, retrying..."); - usleep(delay); - delay *= 2; - } - } - if (!hb_ok) { - log_error("Heartbeat failed"); + if (heartbeat_with_retry(s) != 0) { return -1; } - log_debug("Heartbeat sent"); - s->next_heartbeat = now + s->heartbeat_interval; } if (rc == 0 || !(pfd.revents & POLLIN)) { @@ -241,6 +279,38 @@ static int poll_and_read(gateway_state *s, char *buf, size_t buf_cap) { return 0; } +/* Handle one gateway JSON payload. Returns -1 if the connection should drop. */ +static int handle_gateway_payload(gateway_state *s, const char *auth, const char *reply, + const char *reaction, const char *buf) { + long long op = -1; + if (json_get_int(buf, "op", &op)) { + if (op == OP_HEARTBEAT_ACK) { + log_debug("Heartbeat ACK"); + return 0; + } + if (op == OP_RECONNECT || op == OP_INVALID_SESSION) { + log_error("Reconnect requested"); + return -1; + } + } + + long long seq = -1; + if (json_get_int(buf, "s", &seq)) { + s->sequence = seq; + } + + char event[64]; + if (!json_get_string(buf, "t", event, sizeof(event))) { + return 0; + } + if (strcmp(event, "MESSAGE_CREATE") == 0) { + handle_message_create(auth, reply, reaction, buf); + } else if (strcmp(event, "READY") == 0) { + log_info("READY event received"); + } + return 0; +} + /* ── Main ─────────────────────────────────────────────────────── */ int main(void) { @@ -255,19 +325,12 @@ int main(void) { return 1; } if (!reaction) { - reaction = "\xf0\x9f\x91\x91"; /* crown emoji UTF-8 */ + reaction = DEFAULT_REACTION; } - /* Build auth header token (REST needs "Bot " prefix). */ char auth[512]; char gateway_token[512]; - if (strncmp(token, "Bot ", 4) == 0) { - snprintf(auth, sizeof(auth), "%s", token); - snprintf(gateway_token, sizeof(gateway_token), "%s", token + 4); - } else { - snprintf(auth, sizeof(auth), "Bot %s", token); - snprintf(gateway_token, sizeof(gateway_token), "%s", token); - } + normalize_token(token, auth, sizeof(auth), gateway_token, sizeof(gateway_token)); if (tls_init_ca() != 0) { fprintf(stderr, "Failed to init CA certificates\n"); @@ -301,36 +364,10 @@ int main(void) { break; } if (rc > 0) { - continue; - } - - /* Handle gateway opcodes */ - long long op = -1; - if (json_get_int(buf, "op", &op)) { - if (op == 11) { - log_debug("Heartbeat ACK"); - continue; - } - if (op == 7 || op == 9) { - log_error("Reconnect requested"); - break; - } + continue; /* idle / heartbeat-only tick */ } - - /* Update sequence */ - long long seq = -1; - if (json_get_int(buf, "s", &seq)) { - state.sequence = seq; - } - - /* Dispatch events */ - char event[64]; - if (json_get_string(buf, "t", event, sizeof(event))) { - if (strcmp(event, "MESSAGE_CREATE") == 0) { - handle_message_create(auth, reply, reaction, buf); - } else if (strcmp(event, "READY") == 0) { - log_info("READY event received"); - } + if (handle_gateway_payload(&state, auth, reply, reaction, buf) != 0) { + break; } } diff --git a/src/log.h b/src/log.h index bb55020..8beead6 100644 --- a/src/log.h +++ b/src/log.h @@ -27,16 +27,21 @@ static inline void log_init_from_env(void) { } } -#if LOG_LEVEL_COMPILE >= LOG_LEVEL_ERROR -__attribute__((format(printf, 1, 2))) static inline void log_error(const char *fmt, ...) { - if (g_log_level < LOG_LEVEL_ERROR) { +/* Shared writer — keeps the three public log_* helpers DRY. */ +static inline void log_write(int level, const char *tag, const char *fmt, va_list ap) { + if (g_log_level < level) { return; } - va_list ap; - va_start(ap, fmt); - fprintf(stderr, "[ERROR] "); + fprintf(stderr, "%s ", tag); vfprintf(stderr, fmt, ap); fputc('\n', stderr); +} + +#if LOG_LEVEL_COMPILE >= LOG_LEVEL_ERROR +__attribute__((format(printf, 1, 2))) static inline void log_error(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + log_write(LOG_LEVEL_ERROR, "[ERROR]", fmt, ap); va_end(ap); } #else @@ -47,14 +52,9 @@ __attribute__((format(printf, 1, 2))) static inline void log_error(const char *f #if LOG_LEVEL_COMPILE >= LOG_LEVEL_INFO __attribute__((format(printf, 1, 2))) static inline void log_info(const char *fmt, ...) { - if (g_log_level < LOG_LEVEL_INFO) { - return; - } va_list ap; va_start(ap, fmt); - fprintf(stderr, "[INFO] "); - vfprintf(stderr, fmt, ap); - fputc('\n', stderr); + log_write(LOG_LEVEL_INFO, "[INFO]", fmt, ap); va_end(ap); } #else @@ -65,14 +65,9 @@ __attribute__((format(printf, 1, 2))) static inline void log_info(const char *fm #if LOG_LEVEL_COMPILE >= LOG_LEVEL_DEBUG __attribute__((format(printf, 1, 2))) static inline void log_debug(const char *fmt, ...) { - if (g_log_level < LOG_LEVEL_DEBUG) { - return; - } va_list ap; va_start(ap, fmt); - fprintf(stderr, "[DEBUG] "); - vfprintf(stderr, fmt, ap); - fputc('\n', stderr); + log_write(LOG_LEVEL_DEBUG, "[DEBUG]", fmt, ap); va_end(ap); } #else diff --git a/src/net.c b/src/net.c index c6c276f..a9952cb 100644 --- a/src/net.c +++ b/src/net.c @@ -53,10 +53,6 @@ static int tcp_connect(const char *host, const char *port) { return fd; } -static int entropy_func(void *data, unsigned char *output, size_t len) { - return mbedtls_entropy_func(data, output, len); -} - /* Custom BIO callbacks for mbedtls over a raw socket fd. */ static int bio_send(void *ctx, const unsigned char *buf, size_t len) { int fd = *(int *)ctx; @@ -90,8 +86,8 @@ int tls_connect(tls_conn *c, const char *host, const char *port) { mbedtls_ctr_drbg_init(&c->drbg); mbedtls_entropy_init(&c->entropy); - if (mbedtls_ctr_drbg_seed(&c->drbg, entropy_func, &c->entropy, (const unsigned char *)"king", - 4) != 0) { + if (mbedtls_ctr_drbg_seed(&c->drbg, mbedtls_entropy_func, &c->entropy, + (const unsigned char *)"king", 4) != 0) { goto fail; } diff --git a/src/net.h b/src/net.h index 03636f5..35eb08f 100644 --- a/src/net.h +++ b/src/net.h @@ -8,14 +8,13 @@ #include #include -/* Opaque TLS connection handle. */ +/* Live TLS session over a TCP socket (mbedtls state owned here). */ typedef struct { int fd; mbedtls_ssl_context ssl; mbedtls_ssl_config conf; mbedtls_ctr_drbg_context drbg; mbedtls_entropy_context entropy; - mbedtls_x509_crt ca; } tls_conn; /* Initialize the global CA certificate store. Call once at startup. */ diff --git a/src/ws.c b/src/ws.c index 257aa3e..a41364b 100644 --- a/src/ws.c +++ b/src/ws.c @@ -6,6 +6,12 @@ #include #include +enum { + WS_OPCODE_TEXT = 0x1, + WS_OPCODE_CLOSE = 0x8, + WS_FIN_TEXT = 0x81, /* FIN bit + text opcode */ +}; + static int random_bytes(unsigned char *buf, size_t len) { return (getrandom(buf, len, 0) == (ssize_t)len) ? 0 : -1; } @@ -67,7 +73,7 @@ int ws_handshake(tls_conn *c, const char *host, const char *path) { int ws_send_text(tls_conn *c, const char *text, size_t len) { unsigned char header[14]; size_t hlen = 0; - header[0] = 0x81; /* FIN + text opcode */ + header[0] = WS_FIN_TEXT; if (len <= 125) { header[1] = 0x80 | (unsigned char)len; @@ -106,6 +112,19 @@ int ws_send_text(tls_conn *c, const char *text, size_t len) { return rc; } +/* Discard `plen` bytes from the TLS stream (oversized / unwanted payload). */ +static int drain_payload(tls_conn *c, size_t plen) { + unsigned char tmp[512]; + while (plen > 0) { + size_t chunk = plen > sizeof(tmp) ? sizeof(tmp) : plen; + if (tls_read_exact(c, tmp, chunk) != 0) { + return -1; + } + plen -= chunk; + } + return 0; +} + int ws_read_text(tls_conn *c, char *out, size_t out_cap, size_t *out_len) { unsigned char hdr[2]; if (tls_read_exact(c, hdr, 2) != 0) { @@ -139,16 +158,7 @@ int ws_read_text(tls_conn *c, char *out, size_t out_cap, size_t *out_len) { } if (plen + 1 > out_cap) { - /* Drain oversized payload */ - unsigned char tmp[512]; - size_t rem = (size_t)plen; - while (rem > 0) { - size_t chunk = rem > sizeof(tmp) ? sizeof(tmp) : rem; - if (tls_read_exact(c, tmp, chunk) != 0) { - return -1; - } - rem -= chunk; - } + (void)drain_payload(c, (size_t)plen); return -1; } @@ -166,12 +176,7 @@ int ws_read_text(tls_conn *c, char *out, size_t out_cap, size_t *out_len) { *out_len = (size_t)plen; } - /* Close frame */ - if (opcode == 0x8) { - return -1; - } - /* Only accept text frames */ - if (opcode != 0x1) { + if (opcode == WS_OPCODE_CLOSE || opcode != WS_OPCODE_TEXT) { return -1; } return 0;