291 lines
9.8 KiB
C
291 lines
9.8 KiB
C
/*
|
|
* caching_relay - config parsing + persistent state (in the .jsonc file itself)
|
|
*/
|
|
#define _GNU_SOURCE
|
|
#include "config.h"
|
|
#include "jsonc_strip.h"
|
|
#include "debug.h"
|
|
|
|
#include "../nostr_core_lib/cjson/cJSON.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
|
|
static int read_file(const char *path, char **out, size_t *out_len) {
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f) {
|
|
DEBUG_ERROR("cannot open config '%s': %s", path, strerror(errno));
|
|
return -1;
|
|
}
|
|
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return -1; }
|
|
long sz = ftell(f);
|
|
if (sz < 0) { fclose(f); return -1; }
|
|
rewind(f);
|
|
char *buf = malloc(sz + 1);
|
|
if (!buf) { fclose(f); return -1; }
|
|
size_t n = fread(buf, 1, sz, f);
|
|
fclose(f);
|
|
buf[n] = '\0';
|
|
*out = buf;
|
|
if (out_len) *out_len = n;
|
|
return 0;
|
|
}
|
|
|
|
static void copy_string_array(cJSON *arr, char *dst, int max, int *count_out,
|
|
int elem_len) {
|
|
int count = 0;
|
|
if (arr && cJSON_IsArray(arr)) {
|
|
cJSON *item;
|
|
cJSON_ArrayForEach(item, arr) {
|
|
if (count >= max) break;
|
|
if (!cJSON_IsString(item)) continue;
|
|
const char *s = cJSON_GetStringValue(item);
|
|
if (!s) continue;
|
|
strncpy(dst + count * elem_len, s, elem_len - 1);
|
|
dst[count * elem_len + (elem_len - 1)] = '\0';
|
|
count++;
|
|
}
|
|
}
|
|
*count_out = count;
|
|
}
|
|
|
|
static void copy_int_array(cJSON *arr, int *dst, int max, int *count_out) {
|
|
int count = 0;
|
|
if (arr && cJSON_IsArray(arr)) {
|
|
cJSON *item;
|
|
cJSON_ArrayForEach(item, arr) {
|
|
if (count >= max) break;
|
|
if (cJSON_IsNumber(item)) {
|
|
dst[count++] = (int)cJSON_GetNumberValue(item);
|
|
}
|
|
}
|
|
}
|
|
*count_out = count;
|
|
}
|
|
|
|
static void copy_long_array(cJSON *arr, long *dst, int max, int *count_out) {
|
|
int count = 0;
|
|
if (arr && cJSON_IsArray(arr)) {
|
|
cJSON *item;
|
|
cJSON_ArrayForEach(item, arr) {
|
|
if (count >= max) break;
|
|
if (cJSON_IsNumber(item)) {
|
|
dst[count++] = (long)cJSON_GetNumberValue(item);
|
|
}
|
|
}
|
|
}
|
|
*count_out = count;
|
|
}
|
|
|
|
int cr_config_load(cr_config_t *cfg, const char *path) {
|
|
memset(cfg, 0, sizeof(*cfg));
|
|
strncpy(cfg->path, path, sizeof(cfg->path) - 1);
|
|
|
|
char *raw = NULL;
|
|
size_t raw_len = 0;
|
|
if (read_file(path, &raw, &raw_len) != 0) return -1;
|
|
|
|
char *stripped = jsonc_strip_comments(raw, raw_len);
|
|
free(raw);
|
|
if (!stripped) {
|
|
DEBUG_ERROR("failed to strip comments from config");
|
|
return -1;
|
|
}
|
|
|
|
cJSON *root = cJSON_Parse(stripped);
|
|
free(stripped);
|
|
if (!root) {
|
|
DEBUG_ERROR("failed to parse config JSON");
|
|
return -1;
|
|
}
|
|
|
|
/* root_npubs */
|
|
copy_string_array(cJSON_GetObjectItem(root, "root_npubs"),
|
|
(char *)cfg->root_npubs, CR_MAX_ROOT_NPUBS,
|
|
&cfg->root_npub_count, CR_NPUB_LEN);
|
|
|
|
/* upstream_relays */
|
|
copy_string_array(cJSON_GetObjectItem(root, "upstream_relays"),
|
|
(char *)cfg->upstream_relays, CR_MAX_UPSTREAM,
|
|
&cfg->upstream_count, CR_URL_LEN);
|
|
|
|
/* local_relay */
|
|
cJSON *local = cJSON_GetObjectItem(root, "local_relay");
|
|
if (local && cJSON_IsString(local)) {
|
|
strncpy(cfg->local_relay, cJSON_GetStringValue(local), CR_URL_LEN - 1);
|
|
}
|
|
|
|
/* kinds */
|
|
copy_int_array(cJSON_GetObjectItem(root, "kinds"),
|
|
cfg->kinds, CR_MAX_KINDS, &cfg->kind_count);
|
|
|
|
/* admin_kinds - kinds to follow specifically for root (admin) npubs.
|
|
* Supports [*] to mean "all kinds" (no kind filter for admin). */
|
|
cJSON *ak = cJSON_GetObjectItem(root, "admin_kinds");
|
|
if (ak && cJSON_IsArray(ak)) {
|
|
cJSON *item;
|
|
cJSON_ArrayForEach(item, ak) {
|
|
if (cJSON_IsString(item)) {
|
|
const char *s = cJSON_GetStringValue(item);
|
|
if (s && strcmp(s, "*") == 0) {
|
|
cfg->admin_all_kinds = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!cfg->admin_all_kinds) {
|
|
copy_int_array(ak, cfg->admin_kinds, CR_MAX_KINDS, &cfg->admin_kind_count);
|
|
}
|
|
}
|
|
|
|
/* backfill */
|
|
cJSON *bf = cJSON_GetObjectItem(root, "backfill");
|
|
if (bf) {
|
|
cfg->backfill.enabled = cJSON_IsTrue(cJSON_GetObjectItem(bf, "enabled"));
|
|
cJSON *ept = cJSON_GetObjectItem(bf, "events_per_tick");
|
|
if (ept) cfg->backfill.events_per_tick = (int)cJSON_GetNumberValue(ept);
|
|
cJSON *tis = cJSON_GetObjectItem(bf, "tick_interval_seconds");
|
|
if (tis) cfg->backfill.tick_interval_seconds = (int)cJSON_GetNumberValue(tis);
|
|
}
|
|
|
|
/* live */
|
|
cJSON *lv = cJSON_GetObjectItem(root, "live");
|
|
if (lv) {
|
|
cfg->live.enabled = cJSON_IsTrue(cJSON_GetObjectItem(lv, "enabled"));
|
|
cJSON *rsi = cJSON_GetObjectItem(lv, "resubscribe_interval_seconds");
|
|
if (rsi) cfg->live.resubscribe_interval_seconds = (int)cJSON_GetNumberValue(rsi);
|
|
}
|
|
|
|
/* follow_graph_refresh_seconds */
|
|
cJSON *fgr = cJSON_GetObjectItem(root, "follow_graph_refresh_seconds");
|
|
if (fgr) cfg->follow_graph_refresh_seconds = (int)cJSON_GetNumberValue(fgr);
|
|
|
|
/* state */
|
|
cJSON *st = cJSON_GetObjectItem(root, "state");
|
|
if (st) {
|
|
cJSON *bu = cJSON_GetObjectItem(st, "backfilled_until");
|
|
if (bu) cfg->state.backfilled_until = (long)cJSON_GetNumberValue(bu);
|
|
}
|
|
|
|
cJSON_Delete(root);
|
|
|
|
/* Defaults if missing. */
|
|
if (cfg->backfill.events_per_tick == 0) cfg->backfill.events_per_tick = 500;
|
|
if (cfg->backfill.tick_interval_seconds == 0) cfg->backfill.tick_interval_seconds = 5;
|
|
if (cfg->live.resubscribe_interval_seconds == 0) cfg->live.resubscribe_interval_seconds = 300;
|
|
if (cfg->follow_graph_refresh_seconds == 0) cfg->follow_graph_refresh_seconds = 600;
|
|
|
|
/* Validation. */
|
|
if (cfg->root_npub_count == 0) {
|
|
DEBUG_ERROR("config: at least one root_npub required");
|
|
return -1;
|
|
}
|
|
if (cfg->upstream_count == 0) {
|
|
DEBUG_ERROR("config: at least one upstream_relay required");
|
|
return -1;
|
|
}
|
|
if (cfg->local_relay[0] == '\0') {
|
|
DEBUG_ERROR("config: local_relay required");
|
|
return -1;
|
|
}
|
|
if (cfg->kind_count == 0) {
|
|
DEBUG_ERROR("config: at least one kind required");
|
|
return -1;
|
|
}
|
|
|
|
DEBUG_INFO("config loaded: %d root npubs, %d upstream relays, %d kinds",
|
|
cfg->root_npub_count, cfg->upstream_count, cfg->kind_count);
|
|
return 0;
|
|
}
|
|
|
|
/* Build a fresh cJSON tree from the in-memory config and write it out.
|
|
* We re-emit plain JSON (comments are not preserved on rewrite). */
|
|
int cr_config_save_state(cr_config_t *cfg) {
|
|
cJSON *root = cJSON_CreateObject();
|
|
|
|
cJSON *npubs = cJSON_CreateArray();
|
|
for (int i = 0; i < cfg->root_npub_count; i++)
|
|
cJSON_AddItemToArray(npubs, cJSON_CreateString(cfg->root_npubs[i]));
|
|
cJSON_AddItemToObject(root, "root_npubs", npubs);
|
|
|
|
cJSON *upstream = cJSON_CreateArray();
|
|
for (int i = 0; i < cfg->upstream_count; i++)
|
|
cJSON_AddItemToArray(upstream, cJSON_CreateString(cfg->upstream_relays[i]));
|
|
cJSON_AddItemToObject(root, "upstream_relays", upstream);
|
|
|
|
cJSON_AddItemToObject(root, "local_relay", cJSON_CreateString(cfg->local_relay));
|
|
|
|
cJSON *kinds = cJSON_CreateArray();
|
|
for (int i = 0; i < cfg->kind_count; i++)
|
|
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(cfg->kinds[i]));
|
|
cJSON_AddItemToObject(root, "kinds", kinds);
|
|
|
|
/* admin_kinds */
|
|
cJSON *akinds = cJSON_CreateArray();
|
|
if (cfg->admin_all_kinds) {
|
|
cJSON_AddItemToArray(akinds, cJSON_CreateString("*"));
|
|
} else {
|
|
for (int i = 0; i < cfg->admin_kind_count; i++)
|
|
cJSON_AddItemToArray(akinds, cJSON_CreateNumber(cfg->admin_kinds[i]));
|
|
}
|
|
cJSON_AddItemToObject(root, "admin_kinds", akinds);
|
|
|
|
cJSON *bf = cJSON_CreateObject();
|
|
cJSON_AddItemToObject(bf, "enabled", cJSON_CreateBool(cfg->backfill.enabled));
|
|
cJSON_AddItemToObject(bf, "events_per_tick", cJSON_CreateNumber(cfg->backfill.events_per_tick));
|
|
cJSON_AddItemToObject(bf, "tick_interval_seconds", cJSON_CreateNumber(cfg->backfill.tick_interval_seconds));
|
|
cJSON_AddItemToObject(root, "backfill", bf);
|
|
|
|
cJSON *lv = cJSON_CreateObject();
|
|
cJSON_AddItemToObject(lv, "enabled", cJSON_CreateBool(cfg->live.enabled));
|
|
cJSON_AddItemToObject(lv, "resubscribe_interval_seconds", cJSON_CreateNumber(cfg->live.resubscribe_interval_seconds));
|
|
cJSON_AddItemToObject(root, "live", lv);
|
|
|
|
cJSON_AddItemToObject(root, "follow_graph_refresh_seconds",
|
|
cJSON_CreateNumber(cfg->follow_graph_refresh_seconds));
|
|
|
|
cJSON *st = cJSON_CreateObject();
|
|
cJSON_AddItemToObject(st, "backfilled_until", cJSON_CreateNumber(cfg->state.backfilled_until));
|
|
cJSON_AddItemToObject(root, "state", st);
|
|
|
|
char *json = cJSON_Print(root);
|
|
cJSON_Delete(root);
|
|
if (!json) {
|
|
DEBUG_ERROR("config save: failed to serialize");
|
|
return -1;
|
|
}
|
|
|
|
/* Atomic write: temp file + rename. */
|
|
char tmp[1100];
|
|
snprintf(tmp, sizeof(tmp), "%s.tmp", cfg->path);
|
|
int fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
|
if (fd < 0) {
|
|
DEBUG_ERROR("config save: cannot open tmp '%s': %s", tmp, strerror(errno));
|
|
free(json);
|
|
return -1;
|
|
}
|
|
size_t jlen = strlen(json);
|
|
ssize_t w = write(fd, json, jlen);
|
|
close(fd);
|
|
free(json);
|
|
if (w < 0 || (size_t)w != jlen) {
|
|
DEBUG_ERROR("config save: short write");
|
|
return -1;
|
|
}
|
|
if (rename(tmp, cfg->path) != 0) {
|
|
DEBUG_ERROR("config save: rename failed: %s", strerror(errno));
|
|
return -1;
|
|
}
|
|
DEBUG_LOG("config state saved: backfilled_until=%ld",
|
|
cfg->state.backfilled_until);
|
|
return 0;
|
|
}
|
|
|
|
void cr_config_free(cr_config_t *cfg) {
|
|
(void)cfg;
|
|
}
|