3050 lines
113 KiB
C
3050 lines
113 KiB
C
// Define _GNU_SOURCE to ensure all POSIX features are available
|
||
#define _GNU_SOURCE
|
||
|
||
// Forward declaration for query logging (defined in main.c)
|
||
extern void log_query_execution(const char* query_type, const char* sub_id,
|
||
const char* client_ip, const char* sql,
|
||
long elapsed_us, int rows_returned);
|
||
|
||
// API module for serving embedded web content and admin API functions
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <pthread.h>
|
||
#include <libwebsockets.h>
|
||
#include <cjson/cJSON.h>
|
||
int get_active_connection_count(void);
|
||
#include <time.h>
|
||
#include <sys/stat.h>
|
||
#include <sys/select.h>
|
||
#include <unistd.h>
|
||
#include <fcntl.h>
|
||
#include <strings.h>
|
||
#include <stdbool.h>
|
||
#include "api.h"
|
||
#include "embedded_web_content.h"
|
||
#include "config.h"
|
||
#include "debug.h"
|
||
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
||
#include "../nostr_core_lib/nostr_core/nip017.h"
|
||
#include "../nostr_core_lib/nostr_core/nip044.h"
|
||
#include "subscriptions.h"
|
||
#include "db_ops.h"
|
||
|
||
// External subscription manager (from main.c via subscriptions.c)
|
||
extern subscription_manager_t g_subscription_manager;
|
||
extern struct lws_context *ws_context;
|
||
|
||
// Global variables for config change system
|
||
static pending_config_change_t* pending_changes_head = NULL;
|
||
static int pending_changes_count = 0;
|
||
#define CONFIG_CHANGE_TIMEOUT 300 // 5 minutes
|
||
|
||
// Forward declarations for database functions
|
||
int store_event(cJSON* event);
|
||
int broadcast_event_to_subscriptions(cJSON* event);
|
||
// Forward declarations for config functions
|
||
char* get_relay_private_key(void);
|
||
const char* get_config_value(const char* key);
|
||
int get_config_bool(const char* key, int default_value);
|
||
int update_config_in_table(const char* key, const char* value);
|
||
|
||
// Monitoring system state (throttling now handled per-function)
|
||
|
||
// Forward declaration for monitoring helper function
|
||
int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_func)(void));
|
||
|
||
// Forward declaration for CPU metrics query function
|
||
cJSON* query_cpu_metrics(void);
|
||
|
||
static int api_open_temp_db_connection(void** out_db) {
|
||
return db_open_worker_connection(db_get_database_path(), out_db);
|
||
}
|
||
|
||
static void api_close_temp_db_connection(void* db) {
|
||
db_close_worker_connection(db);
|
||
}
|
||
|
||
// Monitoring system helper functions
|
||
int get_monitoring_throttle_seconds(void) {
|
||
return get_config_int("kind_24567_reporting_throttle_sec", 5);
|
||
}
|
||
|
||
// Forward declaration for the round-robin monitoring generator (defined below).
|
||
static int generate_round_robin_monitoring_event(void);
|
||
|
||
// Forward declaration for has_any_subscription_for_kind (subscriptions.c).
|
||
// Returns 1 if any active subscription would receive the given event kind
|
||
// (explicit kind filter match OR no kind filter at all).
|
||
int has_any_subscription_for_kind(int event_kind);
|
||
|
||
// Forward declaration for the periodic kind-1 status post generator.
|
||
int generate_and_post_status_event(void);
|
||
|
||
// =====================================================================
|
||
// api-worker thread (Phase 1 + 2 + 5 of api_upgrade_plan.md)
|
||
//
|
||
// The worker owns ALL monitoring event generation on a dedicated thread
|
||
// with its own PG connection. It is NOT triggered by event storage or
|
||
// subscription changes (Phase 2 removed those hooks). Instead it runs on
|
||
// a timer (throttle_sec) and, on PostgreSQL, wakes reactively via
|
||
// LISTEN/NOTIFY on the 'event_stored' channel (Phase 5).
|
||
//
|
||
// Loop:
|
||
// - If no one is subscribed to kind 24567: condvar-timedwait for
|
||
// throttle_sec (zero DB work, zero notify polling). Wakeable for
|
||
// STATUS_POST jobs and shutdown via the self-pipe.
|
||
// - If subscribers exist: select() on {PG socket, self-pipe} with
|
||
// timeout = throttle_sec. On NOTIFY or timeout, run one round-robin
|
||
// monitoring tick. On self-pipe wake, process STATUS_POST.
|
||
//
|
||
// A self-pipe is used so api_worker_enqueue_status_post() and shutdown
|
||
// can wake the select() / condvar wait from another thread safely.
|
||
// =====================================================================
|
||
|
||
typedef enum {
|
||
API_WORK_JOB_STATUS_POST = 1
|
||
} api_work_job_type_t;
|
||
|
||
typedef struct api_work_completion {
|
||
api_work_job_type_t type;
|
||
int result;
|
||
struct api_work_completion* next;
|
||
} api_work_completion_t;
|
||
|
||
static pthread_mutex_t g_api_work_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||
static pthread_cond_t g_api_work_cond = PTHREAD_COND_INITIALIZER;
|
||
// Pending STATUS_POST request flag (protected by g_api_work_mutex).
|
||
static int g_api_status_post_pending = 0;
|
||
|
||
static pthread_mutex_t g_api_completion_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||
static api_work_completion_t* g_api_completion_head = NULL;
|
||
static api_work_completion_t* g_api_completion_tail = NULL;
|
||
|
||
static pthread_t g_api_worker_thread;
|
||
static volatile int g_api_worker_running = 0;
|
||
|
||
// Self-pipe fds for waking the worker's select()/condvar. [0]=read, [1]=write.
|
||
static int g_api_wake_pipe[2] = { -1, -1 };
|
||
|
||
static void api_worker_wake(void) {
|
||
if (g_api_wake_pipe[1] >= 0) {
|
||
char byte = 1;
|
||
ssize_t rc = write(g_api_wake_pipe[1], &byte, 1);
|
||
(void)rc;
|
||
}
|
||
}
|
||
|
||
static void api_worker_drain_wake_pipe(void) {
|
||
if (g_api_wake_pipe[0] < 0) return;
|
||
char buf[64];
|
||
while (read(g_api_wake_pipe[0], buf, sizeof(buf)) > 0) {
|
||
// drain
|
||
}
|
||
}
|
||
|
||
static void api_worker_push_completion(api_work_completion_t* completion) {
|
||
if (!completion) return;
|
||
pthread_mutex_lock(&g_api_completion_mutex);
|
||
completion->next = NULL;
|
||
if (!g_api_completion_tail) {
|
||
g_api_completion_head = completion;
|
||
g_api_completion_tail = completion;
|
||
} else {
|
||
g_api_completion_tail->next = completion;
|
||
g_api_completion_tail = completion;
|
||
}
|
||
pthread_mutex_unlock(&g_api_completion_mutex);
|
||
}
|
||
|
||
static api_work_completion_t* api_worker_pop_completion(void) {
|
||
pthread_mutex_lock(&g_api_completion_mutex);
|
||
api_work_completion_t* completion = g_api_completion_head;
|
||
if (completion) {
|
||
g_api_completion_head = completion->next;
|
||
if (!g_api_completion_head) {
|
||
g_api_completion_tail = NULL;
|
||
}
|
||
}
|
||
pthread_mutex_unlock(&g_api_completion_mutex);
|
||
return completion;
|
||
}
|
||
|
||
// Dequeue a pending STATUS_POST request (returns 1 if one was pending).
|
||
static int api_worker_take_status_post(void) {
|
||
pthread_mutex_lock(&g_api_work_mutex);
|
||
int pending = g_api_status_post_pending;
|
||
g_api_status_post_pending = 0;
|
||
pthread_mutex_unlock(&g_api_work_mutex);
|
||
return pending;
|
||
}
|
||
|
||
int api_worker_enqueue_status_post(void) {
|
||
if (!g_api_worker_running) {
|
||
return -1;
|
||
}
|
||
pthread_mutex_lock(&g_api_work_mutex);
|
||
g_api_status_post_pending = 1;
|
||
pthread_cond_broadcast(&g_api_work_cond);
|
||
pthread_mutex_unlock(&g_api_work_mutex);
|
||
api_worker_wake();
|
||
return 0;
|
||
}
|
||
|
||
static void* api_worker_main(void* arg) {
|
||
(void)arg;
|
||
pthread_setname_np(pthread_self(), "api-worker");
|
||
|
||
// Open a dedicated DB worker connection for the full api-worker lifetime.
|
||
void* worker_db = NULL;
|
||
if (api_open_temp_db_connection(&worker_db) != 0) {
|
||
DEBUG_WARN("api-worker: failed to open dedicated DB connection, using fallback behavior");
|
||
}
|
||
|
||
// CRITICAL: bind the worker connection to THIS thread. The __thread
|
||
// pointer g_thread_pg_conn is set by db_open_worker_connection() in the
|
||
// caller's thread (lws-main), not here. Without this rebind, monitoring
|
||
// query functions fall back to the shared g_pg_conn and race with the
|
||
// main thread — which is why the dashboard stopped receiving updates.
|
||
if (worker_db) {
|
||
db_set_thread_connection(worker_db);
|
||
}
|
||
|
||
// Phase 5: register LISTEN event_stored on the worker connection so PG
|
||
// can push notifications when new events are inserted. Returns -1 on
|
||
// SQLite / unsupported backends; the worker then falls back to pure
|
||
// timer-based polling.
|
||
int listen_active = 0;
|
||
if (worker_db) {
|
||
if (db_worker_listen(worker_db, "event_stored") == 0) {
|
||
listen_active = 1;
|
||
DEBUG_LOG("api-worker: LISTEN event_stored registered");
|
||
} else {
|
||
DEBUG_LOG("api-worker: LISTEN not supported on this backend; using timer-only mode");
|
||
}
|
||
}
|
||
|
||
while (g_api_worker_running) {
|
||
int throttle_sec = get_monitoring_throttle_seconds();
|
||
if (throttle_sec <= 0) {
|
||
// Monitoring disabled. Sleep briefly and re-check config.
|
||
struct timespec ts;
|
||
ts.tv_sec = 1;
|
||
ts.tv_nsec = 0;
|
||
nanosleep(&ts, NULL);
|
||
// Still honor a STATUS_POST request if one arrived.
|
||
if (api_worker_take_status_post()) {
|
||
int rc = generate_and_post_status_event();
|
||
api_work_completion_t* c = calloc(1, sizeof(*c));
|
||
if (c) { c->type = API_WORK_JOB_STATUS_POST; c->result = rc; api_worker_push_completion(c); }
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// Is anyone listening to kind 24567 monitoring events?
|
||
int has_subs = has_any_subscription_for_kind(24567);
|
||
|
||
if (!has_subs) {
|
||
// No subscribers: sleep on the condvar for throttle_sec. Zero DB
|
||
// work, zero notify polling. Wakeable for STATUS_POST / shutdown.
|
||
pthread_mutex_lock(&g_api_work_mutex);
|
||
if (g_api_worker_running && !g_api_status_post_pending) {
|
||
struct timespec deadline;
|
||
clock_gettime(CLOCK_REALTIME, &deadline);
|
||
deadline.tv_sec += throttle_sec;
|
||
pthread_cond_timedwait(&g_api_work_cond, &g_api_work_mutex, &deadline);
|
||
}
|
||
int do_status = g_api_status_post_pending;
|
||
g_api_status_post_pending = 0;
|
||
pthread_mutex_unlock(&g_api_work_mutex);
|
||
api_worker_drain_wake_pipe();
|
||
if (do_status) {
|
||
int rc = generate_and_post_status_event();
|
||
api_work_completion_t* c = calloc(1, sizeof(*c));
|
||
if (c) { c->type = API_WORK_JOB_STATUS_POST; c->result = rc; api_worker_push_completion(c); }
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// Subscribers exist. Wait for either a NOTIFY, the throttle timeout,
|
||
// or a self-pipe wake (STATUS_POST / shutdown) — whichever comes first.
|
||
if (listen_active && worker_db) {
|
||
int prc = db_worker_poll_notify(worker_db, throttle_sec * 1000);
|
||
// prc: 1 = notification, 0 = timeout, -1 = error
|
||
(void)prc;
|
||
} else {
|
||
// No LISTEN support: sleep for throttle_sec on the condvar.
|
||
pthread_mutex_lock(&g_api_work_mutex);
|
||
if (g_api_worker_running && !g_api_status_post_pending) {
|
||
struct timespec deadline;
|
||
clock_gettime(CLOCK_REALTIME, &deadline);
|
||
deadline.tv_sec += throttle_sec;
|
||
pthread_cond_timedwait(&g_api_work_cond, &g_api_work_mutex, &deadline);
|
||
}
|
||
pthread_mutex_unlock(&g_api_work_mutex);
|
||
}
|
||
|
||
if (!g_api_worker_running) {
|
||
break;
|
||
}
|
||
|
||
api_worker_drain_wake_pipe();
|
||
|
||
// Run one round-robin monitoring tick (builds + signs + broadcasts
|
||
// one kind 24567 event with the next d-tag in the schedule).
|
||
(void)generate_round_robin_monitoring_event();
|
||
|
||
// Honor any STATUS_POST request that arrived during the wait.
|
||
if (api_worker_take_status_post()) {
|
||
int rc = generate_and_post_status_event();
|
||
api_work_completion_t* c = calloc(1, sizeof(*c));
|
||
if (c) { c->type = API_WORK_JOB_STATUS_POST; c->result = rc; api_worker_push_completion(c); }
|
||
}
|
||
}
|
||
|
||
// Cleanup: unbind the thread connection before closing it.
|
||
if (worker_db) {
|
||
db_clear_thread_connection();
|
||
api_close_temp_db_connection(worker_db);
|
||
}
|
||
|
||
return NULL;
|
||
}
|
||
|
||
int start_api_worker(void) {
|
||
if (g_api_worker_running) {
|
||
return 0;
|
||
}
|
||
|
||
// Create the self-pipe used to wake select()/condvar waits.
|
||
if (g_api_wake_pipe[0] < 0) {
|
||
if (pipe(g_api_wake_pipe) != 0) {
|
||
g_api_wake_pipe[0] = g_api_wake_pipe[1] = -1;
|
||
DEBUG_WARN("api-worker: pipe() failed; wakeups will rely on condvar only");
|
||
} else {
|
||
// Non-blocking both ends so wake() never blocks and drain is safe.
|
||
int flags = fcntl(g_api_wake_pipe[1], F_GETFL, 0);
|
||
if (flags >= 0) fcntl(g_api_wake_pipe[1], F_SETFL, flags | O_NONBLOCK);
|
||
flags = fcntl(g_api_wake_pipe[0], F_GETFL, 0);
|
||
if (flags >= 0) fcntl(g_api_wake_pipe[0], F_SETFL, flags | O_NONBLOCK);
|
||
}
|
||
}
|
||
|
||
g_api_status_post_pending = 0;
|
||
g_api_worker_running = 1;
|
||
if (pthread_create(&g_api_worker_thread, NULL, api_worker_main, NULL) != 0) {
|
||
g_api_worker_running = 0;
|
||
return -1;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
void stop_api_worker(void) {
|
||
if (!g_api_worker_running) {
|
||
return;
|
||
}
|
||
|
||
pthread_mutex_lock(&g_api_work_mutex);
|
||
g_api_worker_running = 0;
|
||
g_api_status_post_pending = 0;
|
||
pthread_cond_broadcast(&g_api_work_cond);
|
||
pthread_mutex_unlock(&g_api_work_mutex);
|
||
api_worker_wake();
|
||
|
||
pthread_join(g_api_worker_thread, NULL);
|
||
|
||
api_work_completion_t* completion = NULL;
|
||
while ((completion = api_worker_pop_completion()) != NULL) {
|
||
free(completion);
|
||
}
|
||
|
||
if (g_api_wake_pipe[0] >= 0) { close(g_api_wake_pipe[0]); g_api_wake_pipe[0] = -1; }
|
||
if (g_api_wake_pipe[1] >= 0) { close(g_api_wake_pipe[1]); g_api_wake_pipe[1] = -1; }
|
||
}
|
||
|
||
void api_worker_process_completions(void) {
|
||
api_work_completion_t* completion = NULL;
|
||
while ((completion = api_worker_pop_completion()) != NULL) {
|
||
if (completion->result != 0) {
|
||
DEBUG_TRACE("api-worker job %d completed with rc=%d", (int)completion->type, completion->result);
|
||
}
|
||
free(completion);
|
||
}
|
||
}
|
||
|
||
// Query event kind distribution from database
|
||
cJSON* query_event_kind_distribution(void) {
|
||
void* temp_db = NULL;
|
||
if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) {
|
||
DEBUG_ERROR("Database not available for monitoring query");
|
||
return NULL;
|
||
}
|
||
|
||
const char* sql = "SELECT kind, COUNT(*) as count FROM events GROUP BY kind ORDER BY count DESC";
|
||
|
||
// Start timing
|
||
struct timespec query_start, query_end;
|
||
clock_gettime(CLOCK_MONOTONIC, &query_start);
|
||
|
||
long long total_events = 0;
|
||
cJSON* kinds_array = db_get_event_kind_distribution_rows(&total_events);
|
||
if (!kinds_array) {
|
||
DEBUG_ERROR("Failed to query event kind distribution rows");
|
||
return NULL;
|
||
}
|
||
|
||
cJSON* distribution = cJSON_CreateObject();
|
||
cJSON_AddStringToObject(distribution, "data_type", "event_kinds");
|
||
cJSON_AddNumberToObject(distribution, "timestamp", (double)time(NULL));
|
||
|
||
// Stop timing and log
|
||
clock_gettime(CLOCK_MONOTONIC, &query_end);
|
||
long elapsed_us = (query_end.tv_sec - query_start.tv_sec) * 1000000L +
|
||
(query_end.tv_nsec - query_start.tv_nsec) / 1000L;
|
||
|
||
int row_count = cJSON_GetArraySize(kinds_array);
|
||
log_query_execution("MONITOR", "event_kinds", NULL, sql, elapsed_us, row_count);
|
||
|
||
cJSON_AddNumberToObject(distribution, "total_events", total_events);
|
||
cJSON_AddItemToObject(distribution, "kinds", kinds_array);
|
||
|
||
api_close_temp_db_connection(temp_db);
|
||
return distribution;
|
||
}
|
||
|
||
// Query time-based statistics from database
|
||
cJSON* query_time_based_statistics(void) {
|
||
void* temp_db = NULL;
|
||
if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) {
|
||
DEBUG_ERROR("Database not available for time stats query");
|
||
return NULL;
|
||
}
|
||
|
||
time_t now = time(NULL);
|
||
cJSON* time_stats = cJSON_CreateObject();
|
||
cJSON_AddStringToObject(time_stats, "data_type", "time_stats");
|
||
cJSON_AddNumberToObject(time_stats, "timestamp", (double)now);
|
||
|
||
cJSON* periods_array = cJSON_CreateArray();
|
||
|
||
// Define time periods: 24h, 7d, 30d
|
||
struct {
|
||
const char* period;
|
||
time_t seconds;
|
||
const char* description;
|
||
} periods[] = {
|
||
{"last_24h", 86400, "Events in the last 24 hours"},
|
||
{"last_7d", 604800, "Events in the last 7 days"},
|
||
{"last_30d", 2592000, "Events in the last 30 days"},
|
||
{NULL, 0, NULL}
|
||
};
|
||
|
||
long long total_events = 0;
|
||
(void)db_get_total_event_count_ll(&total_events);
|
||
|
||
// Query each time period
|
||
for (int i = 0; periods[i].period != NULL; i++) {
|
||
time_t cutoff = now - periods[i].seconds;
|
||
long long count = 0;
|
||
if (db_get_event_count_since(cutoff, &count) != 0) {
|
||
DEBUG_ERROR("Failed to query time stats count");
|
||
continue;
|
||
}
|
||
|
||
cJSON* period_obj = cJSON_CreateObject();
|
||
cJSON_AddStringToObject(period_obj, "period", periods[i].period);
|
||
cJSON_AddNumberToObject(period_obj, "count", count);
|
||
cJSON_AddStringToObject(period_obj, "description", periods[i].description);
|
||
cJSON_AddItemToArray(periods_array, period_obj);
|
||
}
|
||
|
||
cJSON_AddItemToObject(time_stats, "periods", periods_array);
|
||
cJSON_AddNumberToObject(time_stats, "total_events", total_events);
|
||
|
||
api_close_temp_db_connection(temp_db);
|
||
return time_stats;
|
||
}
|
||
|
||
// Query top pubkeys by event count from database
|
||
cJSON* query_top_pubkeys(void) {
|
||
void* temp_db = NULL;
|
||
if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) {
|
||
DEBUG_ERROR("Database not available for top pubkeys query");
|
||
return NULL;
|
||
}
|
||
|
||
cJSON* top_pubkeys = cJSON_CreateObject();
|
||
cJSON_AddStringToObject(top_pubkeys, "data_type", "top_pubkeys");
|
||
cJSON_AddNumberToObject(top_pubkeys, "timestamp", (double)time(NULL));
|
||
|
||
cJSON* rows = db_get_top_pubkeys_rows(10);
|
||
if (!rows) {
|
||
cJSON_Delete(top_pubkeys);
|
||
api_close_temp_db_connection(temp_db);
|
||
DEBUG_ERROR("Failed to query top pubkeys rows");
|
||
return NULL;
|
||
}
|
||
|
||
cJSON* pubkeys_array = cJSON_CreateArray();
|
||
cJSON* row = NULL;
|
||
cJSON_ArrayForEach(row, rows) {
|
||
cJSON* pubkey_obj = cJSON_CreateObject();
|
||
cJSON* pubkey_item = cJSON_GetObjectItemCaseSensitive(row, "pubkey");
|
||
cJSON* count_item = cJSON_GetObjectItemCaseSensitive(row, "count");
|
||
|
||
cJSON_AddStringToObject(pubkey_obj, "pubkey",
|
||
cJSON_IsString(pubkey_item) ? pubkey_item->valuestring : "");
|
||
cJSON_AddNumberToObject(pubkey_obj, "event_count",
|
||
cJSON_IsNumber(count_item) ? count_item->valuedouble : 0.0);
|
||
cJSON_AddItemToArray(pubkeys_array, pubkey_obj);
|
||
}
|
||
cJSON_Delete(rows);
|
||
|
||
long long total_events = 0;
|
||
(void)db_get_total_event_count_ll(&total_events);
|
||
|
||
cJSON_AddItemToObject(top_pubkeys, "pubkeys", pubkeys_array);
|
||
cJSON_AddNumberToObject(top_pubkeys, "total_events", total_events);
|
||
|
||
api_close_temp_db_connection(temp_db);
|
||
return top_pubkeys;
|
||
}
|
||
|
||
|
||
// Query detailed subscription information from database log (ADMIN ONLY)
|
||
// Uses subscriptions table instead of in-memory iteration to avoid mutex contention
|
||
cJSON* query_subscription_details(void) {
|
||
void* temp_db = NULL;
|
||
if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) {
|
||
DEBUG_ERROR("Database not available for subscription details query");
|
||
return NULL;
|
||
}
|
||
|
||
const char* sql =
|
||
"SELECT * "
|
||
"FROM active_subscriptions_log "
|
||
"ORDER BY created_at DESC";
|
||
|
||
// DEBUG: Log the query results for debugging subscription_details
|
||
DEBUG_LOG("=== SUBSCRIPTION_DETAILS QUERY DEBUG ===");
|
||
DEBUG_LOG("Query: %s", sql);
|
||
|
||
// Start timing
|
||
struct timespec query_start, query_end;
|
||
clock_gettime(CLOCK_MONOTONIC, &query_start);
|
||
|
||
time_t current_time = time(NULL);
|
||
cJSON* subscriptions_data = cJSON_CreateObject();
|
||
cJSON_AddStringToObject(subscriptions_data, "data_type", "subscription_details");
|
||
cJSON_AddNumberToObject(subscriptions_data, "timestamp", (double)current_time);
|
||
|
||
cJSON* data = cJSON_CreateObject();
|
||
cJSON* subscriptions_array = cJSON_CreateArray();
|
||
|
||
cJSON* rows = db_get_subscription_details_rows();
|
||
if (!rows) {
|
||
cJSON_Delete(subscriptions_data);
|
||
cJSON_Delete(data);
|
||
cJSON_Delete(subscriptions_array);
|
||
api_close_temp_db_connection(temp_db);
|
||
DEBUG_ERROR("Failed to query subscription details rows");
|
||
return NULL;
|
||
}
|
||
|
||
// Iterate through query results
|
||
int row_count = 0;
|
||
cJSON* row = NULL;
|
||
cJSON_ArrayForEach(row, rows) {
|
||
row_count++;
|
||
cJSON* sub_obj = cJSON_CreateObject();
|
||
|
||
cJSON* sub_id_item = cJSON_GetObjectItemCaseSensitive(row, "id");
|
||
cJSON* client_ip_item = cJSON_GetObjectItemCaseSensitive(row, "client_ip");
|
||
cJSON* filter_json_item = cJSON_GetObjectItemCaseSensitive(row, "filter_json");
|
||
cJSON* events_sent_item = cJSON_GetObjectItemCaseSensitive(row, "events_sent");
|
||
cJSON* created_at_item = cJSON_GetObjectItemCaseSensitive(row, "created_at");
|
||
cJSON* duration_item = cJSON_GetObjectItemCaseSensitive(row, "duration_seconds");
|
||
cJSON* wsi_pointer_item = cJSON_GetObjectItemCaseSensitive(row, "wsi_pointer");
|
||
|
||
const char* sub_id = cJSON_IsString(sub_id_item) ? sub_id_item->valuestring : "";
|
||
const char* client_ip = cJSON_IsString(client_ip_item) ? client_ip_item->valuestring : "";
|
||
const char* filter_json = cJSON_IsString(filter_json_item) ? filter_json_item->valuestring : "[]";
|
||
long long events_sent = cJSON_IsNumber(events_sent_item) ? (long long)events_sent_item->valuedouble : 0;
|
||
long long created_at = cJSON_IsNumber(created_at_item) ? (long long)created_at_item->valuedouble : 0;
|
||
long long duration_seconds = cJSON_IsNumber(duration_item) ? (long long)duration_item->valuedouble : 0;
|
||
const char* wsi_pointer = cJSON_IsString(wsi_pointer_item) ? wsi_pointer_item->valuestring : "";
|
||
|
||
// DEBUG: Log each subscription found
|
||
DEBUG_LOG("Row %d: sub_id=%s, client_ip=%s, events_sent=%lld, created_at=%lld",
|
||
row_count, sub_id[0] ? sub_id : "NULL", client_ip[0] ? client_ip : "NULL",
|
||
events_sent, created_at);
|
||
|
||
// Add basic subscription info
|
||
cJSON_AddStringToObject(sub_obj, "id", sub_id);
|
||
cJSON_AddStringToObject(sub_obj, "client_ip", client_ip);
|
||
cJSON_AddNumberToObject(sub_obj, "created_at", (double)created_at);
|
||
cJSON_AddNumberToObject(sub_obj, "duration_seconds", (double)duration_seconds);
|
||
cJSON_AddNumberToObject(sub_obj, "events_sent", events_sent);
|
||
cJSON_AddBoolToObject(sub_obj, "active", 1); // All from this view are active
|
||
cJSON_AddStringToObject(sub_obj, "wsi_pointer", wsi_pointer[0] ? wsi_pointer : "N/A");
|
||
|
||
// Extract query stats from per_session_data if wsi is still valid
|
||
int db_queries = 0;
|
||
int db_rows = 0;
|
||
double query_rate = 0.0;
|
||
double row_rate = 0.0;
|
||
double avg_rows_per_query = 0.0;
|
||
|
||
if (wsi_pointer[0] != '\0' && strlen(wsi_pointer) > 2) { // Check for valid pointer string
|
||
// Parse wsi pointer from hex string
|
||
struct lws* wsi = NULL;
|
||
if (sscanf(wsi_pointer, "%p", (void**)&wsi) == 1 && wsi != NULL) {
|
||
// Resolve live session data only if this wsi is still tracked.
|
||
struct per_session_data* pss = NULL;
|
||
if (websocket_get_live_pss(wsi, &pss) && pss) {
|
||
db_queries = pss->db_queries_executed;
|
||
db_rows = pss->db_rows_returned;
|
||
|
||
// Calculate rates (per minute)
|
||
time_t connection_duration = current_time - pss->query_tracking_start;
|
||
if (connection_duration > 0) {
|
||
double minutes = connection_duration / 60.0;
|
||
query_rate = db_queries / minutes;
|
||
row_rate = db_rows / minutes;
|
||
}
|
||
|
||
// Calculate average rows per query
|
||
if (db_queries > 0) {
|
||
avg_rows_per_query = (double)db_rows / (double)db_queries;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Add query stats to subscription object
|
||
cJSON_AddNumberToObject(sub_obj, "db_queries_executed", db_queries);
|
||
cJSON_AddNumberToObject(sub_obj, "db_rows_returned", db_rows);
|
||
cJSON_AddNumberToObject(sub_obj, "query_rate_per_min", query_rate);
|
||
cJSON_AddNumberToObject(sub_obj, "row_rate_per_min", row_rate);
|
||
cJSON_AddNumberToObject(sub_obj, "avg_rows_per_query", avg_rows_per_query);
|
||
|
||
// Parse and add filter JSON if available
|
||
if (filter_json) {
|
||
cJSON* filters = cJSON_Parse(filter_json);
|
||
if (filters) {
|
||
cJSON_AddItemToObject(sub_obj, "filters", filters);
|
||
} else {
|
||
// If parsing fails, add empty array
|
||
cJSON_AddItemToObject(sub_obj, "filters", cJSON_CreateArray());
|
||
}
|
||
} else {
|
||
cJSON_AddItemToObject(sub_obj, "filters", cJSON_CreateArray());
|
||
}
|
||
|
||
cJSON_AddItemToArray(subscriptions_array, sub_obj);
|
||
}
|
||
cJSON_Delete(rows);
|
||
|
||
// Add subscriptions array and count to data
|
||
cJSON_AddItemToObject(data, "subscriptions", subscriptions_array);
|
||
cJSON_AddNumberToObject(data, "total_count", cJSON_GetArraySize(subscriptions_array));
|
||
|
||
cJSON_AddItemToObject(subscriptions_data, "data", data);
|
||
|
||
// Stop timing and log
|
||
clock_gettime(CLOCK_MONOTONIC, &query_end);
|
||
long elapsed_us = (query_end.tv_sec - query_start.tv_sec) * 1000000L +
|
||
(query_end.tv_nsec - query_start.tv_nsec) / 1000L;
|
||
|
||
log_query_execution("MONITOR", "subscription_details", NULL, sql, elapsed_us, row_count);
|
||
|
||
// DEBUG: Log final summary
|
||
DEBUG_LOG("Total subscriptions found: %d", row_count);
|
||
DEBUG_LOG("=== END SUBSCRIPTION_DETAILS QUERY DEBUG ===");
|
||
|
||
api_close_temp_db_connection(temp_db);
|
||
return subscriptions_data;
|
||
}
|
||
|
||
static int generate_round_robin_monitoring_event(void) {
|
||
static int rr_index = 0;
|
||
|
||
struct monitor_event_def {
|
||
const char* d_tag;
|
||
cJSON* (*query_func)(void);
|
||
};
|
||
|
||
// Weighted schedule: event_kinds is emitted most frequently since it drives
|
||
// the New Events graph and Total Events counter in the admin UI.
|
||
// Sequence cadence at throttle=5s: event_kinds(0s), cpu(5s), event_kinds(10s),
|
||
// time_stats(15s), event_kinds(20s), top_pubkeys(25s)
|
||
// => event_kinds every ~15s, cpu every ~30s, others every ~30s.
|
||
static const struct monitor_event_def events[] = {
|
||
{"event_kinds", query_event_kind_distribution},
|
||
{"cpu_metrics", query_cpu_metrics},
|
||
{"event_kinds", query_event_kind_distribution},
|
||
{"time_stats", query_time_based_statistics},
|
||
{"event_kinds", query_event_kind_distribution},
|
||
{"top_pubkeys", query_top_pubkeys}
|
||
};
|
||
|
||
const int event_count = (int)(sizeof(events) / sizeof(events[0]));
|
||
const struct monitor_event_def* selected = &events[rr_index];
|
||
rr_index = (rr_index + 1) % event_count;
|
||
|
||
if (generate_monitoring_event_for_type(selected->d_tag, selected->query_func) != 0) {
|
||
DEBUG_ERROR("Failed to generate %s monitoring event", selected->d_tag);
|
||
return -1;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
// Generate event-driven monitoring events (triggered by event storage)
|
||
int generate_event_driven_monitoring(void) {
|
||
return generate_round_robin_monitoring_event();
|
||
}
|
||
|
||
// Generate subscription-driven monitoring events (triggered by subscription changes)
|
||
int generate_subscription_driven_monitoring(void) {
|
||
return generate_round_robin_monitoring_event();
|
||
}
|
||
|
||
// Generate and broadcast monitoring event (legacy function - now calls event-driven version)
|
||
int generate_monitoring_event(void) {
|
||
return generate_event_driven_monitoring();
|
||
}
|
||
|
||
// Helper function to generate monitoring event for a specific type
|
||
int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_func)(void)) {
|
||
// Query the monitoring data
|
||
cJSON* monitoring_data = query_func();
|
||
if (!monitoring_data) {
|
||
DEBUG_ERROR("Failed to query monitoring data for %s", d_tag_value);
|
||
return -1;
|
||
}
|
||
|
||
// Convert to JSON string for content
|
||
char* content_json = cJSON_Print(monitoring_data);
|
||
cJSON_Delete(monitoring_data);
|
||
|
||
if (!content_json) {
|
||
DEBUG_ERROR("Failed to serialize monitoring data for %s", d_tag_value);
|
||
return -1;
|
||
}
|
||
|
||
// Get relay keys for signing
|
||
const char* relay_pubkey = get_config_value("relay_pubkey");
|
||
char* relay_privkey_hex = get_relay_private_key();
|
||
if (!relay_pubkey || !relay_privkey_hex) {
|
||
free(content_json);
|
||
DEBUG_ERROR("Could not get relay keys for monitoring event (%s)", d_tag_value);
|
||
return -1;
|
||
}
|
||
|
||
// Convert relay private key to bytes
|
||
unsigned char relay_privkey[32];
|
||
if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) {
|
||
free(relay_privkey_hex);
|
||
free(content_json);
|
||
DEBUG_ERROR("Failed to convert relay private key for monitoring event (%s)", d_tag_value);
|
||
return -1;
|
||
}
|
||
free(relay_privkey_hex);
|
||
|
||
// Create monitoring event (kind 24567 - ephemeral)
|
||
cJSON* monitoring_event = cJSON_CreateObject();
|
||
cJSON_AddStringToObject(monitoring_event, "id", ""); // Will be set by signing
|
||
cJSON_AddStringToObject(monitoring_event, "pubkey", relay_pubkey);
|
||
cJSON_AddNumberToObject(monitoring_event, "created_at", (double)time(NULL));
|
||
cJSON_AddNumberToObject(monitoring_event, "kind", 24567);
|
||
cJSON_AddStringToObject(monitoring_event, "content", content_json);
|
||
|
||
// Create tags array with d tag for identification
|
||
cJSON* tags = cJSON_CreateArray();
|
||
|
||
// d tag for event identification
|
||
cJSON* d_tag = cJSON_CreateArray();
|
||
cJSON_AddItemToArray(d_tag, cJSON_CreateString("d"));
|
||
cJSON_AddItemToArray(d_tag, cJSON_CreateString(d_tag_value));
|
||
cJSON_AddItemToArray(tags, d_tag);
|
||
|
||
cJSON_AddItemToObject(monitoring_event, "tags", tags);
|
||
|
||
// Use the library function to create and sign the event
|
||
cJSON* signed_event = nostr_create_and_sign_event(
|
||
24567, // kind (ephemeral)
|
||
cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(monitoring_event, "content")), // content
|
||
tags, // tags
|
||
relay_privkey, // private key
|
||
(time_t)cJSON_GetNumberValue(cJSON_GetObjectItemCaseSensitive(monitoring_event, "created_at")) // timestamp
|
||
);
|
||
|
||
if (!signed_event) {
|
||
cJSON_Delete(monitoring_event);
|
||
free(content_json);
|
||
DEBUG_ERROR("Failed to create and sign monitoring event (%s)", d_tag_value);
|
||
return -1;
|
||
}
|
||
|
||
// Replace the unsigned event with the signed one
|
||
cJSON_Delete(monitoring_event);
|
||
monitoring_event = signed_event;
|
||
|
||
// Broadcast the ephemeral event to active subscriptions (no database storage)
|
||
broadcast_event_to_subscriptions(monitoring_event);
|
||
|
||
// If running on api-worker thread, wake the lws service loop so queued frames flush promptly.
|
||
if (ws_context) {
|
||
lws_cancel_service(ws_context);
|
||
}
|
||
|
||
cJSON_Delete(monitoring_event);
|
||
free(content_json);
|
||
|
||
DEBUG_LOG("Monitoring event broadcast (ephemeral kind 24567, type: %s)", d_tag_value);
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* Generate and post a kind 1 status event with relay statistics
|
||
* Called periodically from main event loop based on configuration
|
||
* Returns: 0 on success, -1 on error
|
||
*/
|
||
int generate_and_post_status_event(void) {
|
||
// Check if feature is enabled
|
||
int hours_interval = get_config_int("kind_1_status_posts_hours", 0);
|
||
if (hours_interval <= 0) {
|
||
return 0; // Feature disabled
|
||
}
|
||
|
||
// Generate statistics text content using existing function
|
||
char* stats_text = generate_stats_text();
|
||
if (!stats_text) {
|
||
DEBUG_ERROR("Failed to generate statistics text for status post");
|
||
return -1;
|
||
}
|
||
|
||
// Get relay private key
|
||
char* relay_privkey_hex = get_relay_private_key();
|
||
if (!relay_privkey_hex) {
|
||
DEBUG_ERROR("Failed to get relay private key for status post");
|
||
free(stats_text);
|
||
return -1;
|
||
}
|
||
|
||
// Convert hex private key to bytes
|
||
unsigned char relay_privkey_bytes[32];
|
||
if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey_bytes, 32) != 0) {
|
||
DEBUG_ERROR("Failed to convert relay private key to bytes");
|
||
free(stats_text);
|
||
return -1;
|
||
}
|
||
|
||
// Create empty tags array (kind 1 doesn't require special tags)
|
||
cJSON* tags = cJSON_CreateArray();
|
||
|
||
// Create and sign the kind 1 event
|
||
cJSON* signed_event = nostr_create_and_sign_event(
|
||
1, // kind 1 = text note
|
||
stats_text, // content = statistics
|
||
tags, // empty tags
|
||
relay_privkey_bytes, // relay's private key
|
||
time(NULL) // current timestamp
|
||
);
|
||
|
||
free(stats_text);
|
||
|
||
if (!signed_event) {
|
||
DEBUG_ERROR("Failed to create and sign status event");
|
||
return -1;
|
||
}
|
||
|
||
// Store event in database (kind 1 events MUST be stored)
|
||
if (store_event(signed_event) != 0) {
|
||
DEBUG_ERROR("Failed to store status event in database");
|
||
cJSON_Delete(signed_event);
|
||
return -1;
|
||
}
|
||
|
||
// Broadcast event to subscriptions
|
||
broadcast_event_to_subscriptions(signed_event);
|
||
|
||
DEBUG_LOG("Successfully posted kind 1 status event");
|
||
cJSON_Delete(signed_event);
|
||
|
||
return 0;
|
||
}
|
||
|
||
// Forward declaration for known_configs (defined in config.c)
|
||
typedef struct {
|
||
const char* key;
|
||
const char* type;
|
||
int min_val;
|
||
int max_val;
|
||
} config_definition_t;
|
||
|
||
// Define known_configs array locally in api.c
|
||
static const config_definition_t known_configs[] = {
|
||
// Authentication
|
||
{"auth_enabled", "bool", 0, 1},
|
||
{"nip42_auth_required", "bool", 0, 1},
|
||
{"nip42_auth_required_events", "bool", 0, 1},
|
||
{"nip42_auth_required_subscriptions", "bool", 0, 1},
|
||
{"nip42_auth_required_kinds", "string", 0, 255},
|
||
{"nip42_challenge_expiration", "int", 60, 3600},
|
||
{"nip42_challenge_timeout", "int", 60, 3600},
|
||
{"nip42_time_tolerance", "int", 60, 3600},
|
||
{"nip70_protected_events_enabled", "bool", 0, 1},
|
||
|
||
// Idle Connection Ban Settings
|
||
{"idle_connection_timeout_sec", "int", 0, 300},
|
||
{"idle_ban_enabled", "bool", 0, 1},
|
||
{"idle_ban_threshold", "int", 1, 100},
|
||
{"idle_ban_window_sec", "int", 10, 3600},
|
||
{"idle_ban_duration_sec", "int", 60, 86400},
|
||
|
||
// Server Core Settings
|
||
{"relay_port", "int", 1, 65535},
|
||
{"max_connections", "int", 1, 10000},
|
||
|
||
// NIP-11 Relay Information
|
||
{"relay_name", "string", 0, 256},
|
||
{"relay_description", "string", 0, 512},
|
||
{"relay_contact", "string", 0, 256},
|
||
{"relay_software", "string", 0, 256},
|
||
{"relay_version", "string", 0, 256},
|
||
{"supported_nips", "string", 0, 1024},
|
||
{"language_tags", "string", 0, 1024},
|
||
{"relay_countries", "string", 0, 1024},
|
||
{"posting_policy", "string", 0, 1024},
|
||
{"payments_url", "string", 0, 1024},
|
||
|
||
// NIP-13 Proof of Work
|
||
{"pow_min_difficulty", "int", 0, 32},
|
||
{"pow_mode", "string", 0, 32},
|
||
|
||
// NIP-40 Expiration Timestamp
|
||
{"nip40_expiration_enabled", "bool", 0, 1},
|
||
{"nip40_expiration_strict", "bool", 0, 1},
|
||
{"nip40_expiration_filter", "bool", 0, 1},
|
||
{"nip40_expiration_grace_period", "int", 0, 86400},
|
||
|
||
// Subscription Limits
|
||
{"max_subscriptions_per_client", "int", 1, 1000},
|
||
{"max_total_subscriptions", "int", 1, 100000},
|
||
{"max_filters_per_subscription", "int", 1, 100},
|
||
|
||
// Event Processing Limits
|
||
{"max_event_tags", "int", 1, 1000},
|
||
{"max_content_length", "int", 100, 1048576},
|
||
{"max_message_length", "int", 1024, 1048576},
|
||
|
||
// Performance Settings
|
||
{"default_limit", "int", 1, 50000},
|
||
{"max_limit", "int", 1, 50000},
|
||
|
||
// Relay keys
|
||
{"relay_pubkey", "string", 0, 65},
|
||
{"relay_privkey", "string", 0, 65},
|
||
{"admin_pubkey", "string", 0, ADMIN_PUBKEY_LIST_MAX_LENGTH},
|
||
|
||
// Kind 1 Status Posts
|
||
{"kind_1_status_posts_hours", "int", 0, 8760},
|
||
|
||
// Thread Pool Settings
|
||
{"thread_pool_enabled", "bool", 0, 1},
|
||
{"thread_pool_readers", "int", 1, 64},
|
||
{"thread_pool_writers", "int", 1, 32},
|
||
{"thread_pool_max_queue_depth", "int", 128, 65536},
|
||
|
||
// Sentinel
|
||
{NULL, NULL, 0, 0}
|
||
};
|
||
// External database connection (from main.c)
|
||
extern char g_database_path[512];
|
||
|
||
// Forward declarations for database functions
|
||
int store_event(cJSON* event);
|
||
int broadcast_event_to_subscriptions(cJSON* event);
|
||
|
||
// Forward declarations for config functions
|
||
char* get_relay_private_key(void);
|
||
const char* get_config_value(const char* key);
|
||
int get_config_bool(const char* key, int default_value);
|
||
int update_config_in_table(const char* key, const char* value);
|
||
|
||
// Forward declaration for monitoring helper function
|
||
int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_func)(void));
|
||
|
||
// Handle HTTP request for embedded files (assumes GET)
|
||
int handle_embedded_file_request(struct lws* wsi, const char* requested_uri) {
|
||
const char* file_path;
|
||
|
||
// Handle /api requests
|
||
char temp_path[256];
|
||
if (strcmp(requested_uri, "/api") == 0) {
|
||
// /api -> serve index.html
|
||
file_path = "/";
|
||
} else if (strncmp(requested_uri, "/api/", 5) == 0) {
|
||
// Extract file path from /api/ prefix and add leading slash for lookup
|
||
snprintf(temp_path, sizeof(temp_path), "/%s", requested_uri + 5); // Add leading slash
|
||
file_path = temp_path;
|
||
} else {
|
||
DEBUG_WARN("Embedded file request without /api prefix");
|
||
lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
|
||
return -1;
|
||
}
|
||
|
||
// Get embedded file
|
||
embedded_file_t* file = get_embedded_file(file_path);
|
||
if (!file) {
|
||
DEBUG_WARN("Embedded file not found");
|
||
lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
|
||
return -1;
|
||
}
|
||
|
||
// Allocate session data
|
||
struct embedded_file_session_data* session_data = malloc(sizeof(struct embedded_file_session_data));
|
||
if (!session_data) {
|
||
DEBUG_ERROR("Failed to allocate embedded file session data");
|
||
return -1;
|
||
}
|
||
|
||
session_data->type = 1; // Embedded file
|
||
session_data->data = file->data;
|
||
session_data->size = file->size;
|
||
session_data->content_type = file->content_type;
|
||
session_data->headers_sent = 0;
|
||
session_data->body_sent = 0;
|
||
|
||
// Store session data
|
||
lws_set_wsi_user(wsi, session_data);
|
||
|
||
// Prepare HTTP response headers
|
||
unsigned char buf[LWS_PRE + 1024];
|
||
unsigned char *p = &buf[LWS_PRE];
|
||
unsigned char *start = p;
|
||
unsigned char *end = &buf[sizeof(buf) - 1];
|
||
|
||
if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end)) {
|
||
free(session_data);
|
||
return -1;
|
||
}
|
||
|
||
if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)file->content_type, strlen(file->content_type), &p, end)) {
|
||
free(session_data);
|
||
return -1;
|
||
}
|
||
|
||
if (lws_add_http_header_content_length(wsi, file->size, &p, end)) {
|
||
free(session_data);
|
||
return -1;
|
||
}
|
||
|
||
// Add CORS headers (same as NIP-11 for consistency)
|
||
if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-origin:", (unsigned char*)"*", 1, &p, end)) {
|
||
free(session_data);
|
||
return -1;
|
||
}
|
||
if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-headers:", (unsigned char*)"content-type, accept", 20, &p, end)) {
|
||
free(session_data);
|
||
return -1;
|
||
}
|
||
if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-methods:", (unsigned char*)"GET, OPTIONS", 12, &p, end)) {
|
||
free(session_data);
|
||
return -1;
|
||
}
|
||
|
||
// Add Connection: close to ensure connection closes after response
|
||
if (lws_add_http_header_by_name(wsi, (unsigned char*)"connection:", (unsigned char*)"close", 5, &p, end)) {
|
||
free(session_data);
|
||
return -1;
|
||
}
|
||
|
||
if (lws_finalize_http_header(wsi, &p, end)) {
|
||
free(session_data);
|
||
return -1;
|
||
}
|
||
|
||
// Write headers
|
||
if (lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS) < 0) {
|
||
free(session_data);
|
||
return -1;
|
||
}
|
||
|
||
session_data->headers_sent = 1;
|
||
|
||
// Request callback for body transmission
|
||
lws_callback_on_writable(wsi);
|
||
|
||
return 0;
|
||
}
|
||
|
||
// Send admin response with request ID correlation
|
||
int send_admin_response(const char* sender_pubkey, const char* response_content, const char* request_id,
|
||
char* error_message, size_t error_size, struct lws* wsi) {
|
||
(void)wsi; // Suppress unused parameter warning
|
||
if (!sender_pubkey || !response_content || !request_id || !error_message) {
|
||
if (error_message) {
|
||
strncpy(error_message, "Admin response: Invalid parameters", error_size - 1);
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
// Get relay keys for signing
|
||
const char* relay_pubkey = get_config_value("relay_pubkey");
|
||
char* relay_privkey_hex = get_relay_private_key();
|
||
if (!relay_pubkey || !relay_privkey_hex) {
|
||
if (relay_privkey_hex) free(relay_privkey_hex);
|
||
strncpy(error_message, "Admin response: Could not get relay keys", error_size - 1);
|
||
return -1;
|
||
}
|
||
|
||
// Convert relay private key to bytes
|
||
unsigned char relay_privkey[32];
|
||
if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) {
|
||
free(relay_privkey_hex);
|
||
strncpy(error_message, "Admin response: Failed to convert relay private key", error_size - 1);
|
||
return -1;
|
||
}
|
||
free(relay_privkey_hex);
|
||
|
||
// Convert sender pubkey to bytes for NIP-44 encryption
|
||
unsigned char sender_pubkey_bytes[32];
|
||
if (nostr_hex_to_bytes(sender_pubkey, sender_pubkey_bytes, sizeof(sender_pubkey_bytes)) != 0) {
|
||
strncpy(error_message, "Admin response: Failed to convert sender pubkey", error_size - 1);
|
||
return -1;
|
||
}
|
||
|
||
// Encrypt response content using NIP-44
|
||
char encrypted_content[131072]; // Buffer for encrypted content (128KB to handle large SQL responses)
|
||
int encrypt_result = nostr_nip44_encrypt(
|
||
relay_privkey, // sender private key (bytes)
|
||
sender_pubkey_bytes, // recipient public key (bytes)
|
||
response_content, // plaintext
|
||
encrypted_content, // output buffer
|
||
sizeof(encrypted_content) // output buffer size
|
||
);
|
||
|
||
if (encrypt_result != 0) {
|
||
strncpy(error_message, "Admin response: Failed to encrypt response content", error_size - 1);
|
||
return -1;
|
||
}
|
||
|
||
// Create response event (kind 23457)
|
||
cJSON* response_event = cJSON_CreateObject();
|
||
cJSON_AddStringToObject(response_event, "id", ""); // Will be set by signing
|
||
cJSON_AddStringToObject(response_event, "pubkey", relay_pubkey);
|
||
cJSON_AddNumberToObject(response_event, "created_at", (double)time(NULL));
|
||
cJSON_AddNumberToObject(response_event, "kind", 23457);
|
||
cJSON_AddStringToObject(response_event, "content", encrypted_content);
|
||
|
||
// Create tags array with p tag and e tag for request correlation
|
||
cJSON* tags = cJSON_CreateArray();
|
||
|
||
// p tag for recipient
|
||
cJSON* p_tag = cJSON_CreateArray();
|
||
cJSON_AddItemToArray(p_tag, cJSON_CreateString("p"));
|
||
cJSON_AddItemToArray(p_tag, cJSON_CreateString(sender_pubkey));
|
||
cJSON_AddItemToArray(tags, p_tag);
|
||
|
||
// e tag for request event correlation
|
||
cJSON* e_tag = cJSON_CreateArray();
|
||
cJSON_AddItemToArray(e_tag, cJSON_CreateString("e"));
|
||
cJSON_AddItemToArray(e_tag, cJSON_CreateString(request_id));
|
||
cJSON_AddItemToArray(tags, e_tag);
|
||
|
||
cJSON_AddItemToObject(response_event, "tags", tags);
|
||
|
||
// Use the library function to create and sign the event
|
||
cJSON* signed_event = nostr_create_and_sign_event(
|
||
23457, // kind
|
||
cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(response_event, "content")), // content
|
||
tags, // tags
|
||
relay_privkey, // private key
|
||
(time_t)cJSON_GetNumberValue(cJSON_GetObjectItemCaseSensitive(response_event, "created_at")) // timestamp
|
||
);
|
||
|
||
if (!signed_event) {
|
||
cJSON_Delete(response_event);
|
||
strncpy(error_message, "Admin response: Failed to create and sign event", error_size - 1);
|
||
return -1;
|
||
}
|
||
|
||
// Replace the unsigned event with the signed one
|
||
cJSON_Delete(response_event);
|
||
response_event = signed_event;
|
||
|
||
// Broadcast the event
|
||
broadcast_event_to_subscriptions(response_event);
|
||
|
||
// Store in database
|
||
int store_result = store_event(response_event);
|
||
|
||
cJSON_Delete(response_event);
|
||
|
||
if (store_result != 0) {
|
||
strncpy(error_message, "Admin response: Failed to store response event", error_size - 1);
|
||
return -1;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
// =============================================================================
|
||
// SQL QUERY ADMIN FUNCTIONS
|
||
// =============================================================================
|
||
|
||
// Validate SQL query for security and safety
|
||
int validate_sql_query(const char* query, char* error_message, size_t error_size) {
|
||
if (!query || !error_message) {
|
||
return 0;
|
||
}
|
||
|
||
// Convert query to uppercase for case-insensitive keyword checking
|
||
char query_upper[4096];
|
||
size_t query_len = strlen(query);
|
||
if (query_len >= sizeof(query_upper)) {
|
||
snprintf(error_message, error_size, "Query too long (max %zu characters)", sizeof(query_upper) - 1);
|
||
return 0;
|
||
}
|
||
|
||
// Copy and convert to uppercase
|
||
for (size_t i = 0; i < query_len; i++) {
|
||
query_upper[i] = (query[i] >= 'a' && query[i] <= 'z') ? query[i] - 32 : query[i];
|
||
}
|
||
query_upper[query_len] = '\0';
|
||
|
||
// List of blocked keywords (case-insensitive)
|
||
const char* blocked_keywords[] = {
|
||
"INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER", "TRUNCATE",
|
||
"EXEC", "EXECUTE", "MERGE", "BULK", "BACKUP", "RESTORE",
|
||
"GRANT", "REVOKE", "DENY", "COMMIT", "ROLLBACK", "SAVEPOINT",
|
||
"SHUTDOWN", "PRAGMA", "VACUUM", "REINDEX", "ANALYZE",
|
||
NULL // Sentinel
|
||
};
|
||
|
||
// Check for blocked keywords
|
||
for (int i = 0; blocked_keywords[i] != NULL; i++) {
|
||
char keyword_pattern[64];
|
||
snprintf(keyword_pattern, sizeof(keyword_pattern), " %s ", blocked_keywords[i]);
|
||
|
||
if (strstr(query_upper, keyword_pattern) != NULL) {
|
||
snprintf(error_message, error_size, "Query blocked: %s statements not allowed", blocked_keywords[i]);
|
||
return 0;
|
||
}
|
||
|
||
// Also check at start of query
|
||
if (strncmp(query_upper, blocked_keywords[i], strlen(blocked_keywords[i])) == 0) {
|
||
snprintf(error_message, error_size, "Query blocked: %s statements not allowed", blocked_keywords[i]);
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
// Check for SELECT keyword (must be present)
|
||
if (strstr(query_upper, " SELECT ") == NULL && strncmp(query_upper, "SELECT ", 7) != 0) {
|
||
// Allow WITH clauses (CTEs) which can start queries
|
||
if (strstr(query_upper, " WITH ") == NULL && strncmp(query_upper, "WITH ", 5) != 0) {
|
||
snprintf(error_message, error_size, "Query blocked: Only SELECT statements and WITH clauses are allowed");
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
// Basic length check
|
||
if (query_len < 6) { // Minimum "SELECT" length
|
||
snprintf(error_message, error_size, "Query too short");
|
||
return 0;
|
||
}
|
||
|
||
return 1; // Query passed validation
|
||
}
|
||
|
||
// Execute SQL query with safety limits
|
||
char* execute_sql_query(const char* query, const char* request_id, char* error_message, size_t error_size) {
|
||
if (!query || !request_id || !error_message) {
|
||
return NULL;
|
||
}
|
||
|
||
if (!db_is_available()) {
|
||
snprintf(error_message, error_size, "Database not available");
|
||
return NULL;
|
||
}
|
||
|
||
return db_execute_readonly_query_json(query, request_id, error_message, error_size, 1000, 5000);
|
||
}
|
||
|
||
// Unified handler for SQL query commands
|
||
int handle_sql_query_unified(cJSON* event, const char* query, char* error_message, size_t error_size, struct lws* wsi) {
|
||
if (!event || !query || !error_message) {
|
||
return -1;
|
||
}
|
||
|
||
// Get request event ID for response correlation
|
||
cJSON* request_id_obj = cJSON_GetObjectItemCaseSensitive(event, "id");
|
||
if (!request_id_obj || !cJSON_IsString(request_id_obj)) {
|
||
snprintf(error_message, error_size, "Missing request event ID");
|
||
return -1;
|
||
}
|
||
const char* request_id = cJSON_GetStringValue(request_id_obj);
|
||
|
||
// Validate query
|
||
if (!validate_sql_query(query, error_message, error_size)) {
|
||
return -1;
|
||
}
|
||
|
||
// Execute query
|
||
char* result_json = execute_sql_query(query, request_id, error_message, error_size);
|
||
if (!result_json) {
|
||
return -1;
|
||
}
|
||
|
||
// Get sender pubkey for response
|
||
cJSON* sender_pubkey_obj = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
|
||
if (!sender_pubkey_obj || !cJSON_IsString(sender_pubkey_obj)) {
|
||
free(result_json);
|
||
snprintf(error_message, error_size, "Missing sender pubkey");
|
||
return -1;
|
||
}
|
||
const char* sender_pubkey = cJSON_GetStringValue(sender_pubkey_obj);
|
||
|
||
// Send response as kind 23457 event with request ID in tags
|
||
int send_result = send_admin_response(sender_pubkey, result_json, request_id, error_message, error_size, wsi);
|
||
free(result_json);
|
||
|
||
return send_result;
|
||
}
|
||
|
||
// Handle HTTP_WRITEABLE for embedded files
|
||
int handle_embedded_file_writeable(struct lws* wsi) {
|
||
struct embedded_file_session_data* session_data = (struct embedded_file_session_data*)lws_wsi_user(wsi);
|
||
if (!session_data || session_data->headers_sent == 0 || session_data->body_sent == 1) {
|
||
return 0;
|
||
}
|
||
|
||
// Allocate buffer for data transmission
|
||
unsigned char *buf = malloc(LWS_PRE + session_data->size);
|
||
if (!buf) {
|
||
DEBUG_ERROR("Failed to allocate buffer for embedded file transmission");
|
||
free(session_data);
|
||
lws_set_wsi_user(wsi, NULL);
|
||
return -1;
|
||
}
|
||
|
||
// Copy data to buffer
|
||
memcpy(buf + LWS_PRE, session_data->data, session_data->size);
|
||
|
||
// Write data
|
||
int write_result = lws_write(wsi, buf + LWS_PRE, session_data->size, LWS_WRITE_HTTP);
|
||
|
||
// Free the transmission buffer
|
||
free(buf);
|
||
|
||
if (write_result < 0) {
|
||
DEBUG_ERROR("Failed to write embedded file data");
|
||
free(session_data);
|
||
lws_set_wsi_user(wsi, NULL);
|
||
return -1;
|
||
}
|
||
|
||
// Mark as sent and clean up
|
||
session_data->body_sent = 1;
|
||
free(session_data);
|
||
lws_set_wsi_user(wsi, NULL);
|
||
|
||
return 0;
|
||
}
|
||
static void add_relay_thread_snapshot(cJSON* cpu_stats, pid_t relay_pid) {
|
||
if (!cpu_stats || relay_pid <= 0) return;
|
||
|
||
cJSON* threads = cJSON_CreateArray();
|
||
if (!threads) return;
|
||
|
||
char cmd[256];
|
||
snprintf(cmd, sizeof(cmd), "ps -T -p %d -o tid=,comm=,pcpu=,pmem=,etime=", relay_pid);
|
||
FILE* fp = popen(cmd, "r");
|
||
if (!fp) {
|
||
cJSON_Delete(threads);
|
||
return;
|
||
}
|
||
|
||
char line[512];
|
||
int row_count = 0;
|
||
while (fgets(line, sizeof(line), fp) && row_count < 32) {
|
||
int tid = 0;
|
||
char comm[64] = {0};
|
||
double pcpu = 0.0, pmem = 0.0;
|
||
char elapsed[64] = {0};
|
||
if (sscanf(line, "%d %63s %lf %lf %63s", &tid, comm, &pcpu, &pmem, elapsed) == 5) {
|
||
cJSON* row = cJSON_CreateObject();
|
||
if (row) {
|
||
cJSON_AddNumberToObject(row, "tid", tid);
|
||
cJSON_AddStringToObject(row, "thread", comm);
|
||
cJSON_AddNumberToObject(row, "cpu_percent", pcpu);
|
||
cJSON_AddNumberToObject(row, "mem_percent", pmem);
|
||
cJSON_AddStringToObject(row, "elapsed", elapsed);
|
||
cJSON_AddItemToArray(threads, row);
|
||
row_count++;
|
||
}
|
||
}
|
||
}
|
||
pclose(fp);
|
||
|
||
cJSON_AddItemToObject(cpu_stats, "relay_threads", threads);
|
||
}
|
||
|
||
static void add_postgres_backend_snapshot(cJSON* cpu_stats) {
|
||
if (!cpu_stats) return;
|
||
|
||
cJSON* backends = cJSON_CreateArray();
|
||
if (!backends) return;
|
||
|
||
FILE* fp = popen("ps -C postgres -o pid=,comm=,pcpu=,pmem=,etime=,args=", "r");
|
||
if (!fp) {
|
||
cJSON_Delete(backends);
|
||
return;
|
||
}
|
||
|
||
char line[1024];
|
||
int row_count = 0;
|
||
while (fgets(line, sizeof(line), fp) && row_count < 48) {
|
||
int pid = 0;
|
||
char comm[64] = {0};
|
||
double pcpu = 0.0, pmem = 0.0;
|
||
char elapsed[64] = {0};
|
||
char args[768] = {0};
|
||
if (sscanf(line, "%d %63s %lf %lf %63s %767[^\n]", &pid, comm, &pcpu, &pmem, elapsed, args) >= 5) {
|
||
cJSON* row = cJSON_CreateObject();
|
||
if (row) {
|
||
cJSON_AddNumberToObject(row, "pid", pid);
|
||
cJSON_AddStringToObject(row, "command", comm);
|
||
cJSON_AddNumberToObject(row, "cpu_percent", pcpu);
|
||
cJSON_AddNumberToObject(row, "mem_percent", pmem);
|
||
cJSON_AddStringToObject(row, "elapsed", elapsed);
|
||
if (args[0] != '\0') {
|
||
cJSON_AddStringToObject(row, "args", args);
|
||
if (strstr(args, " c-relay-pg ") || strstr(args, " c-relay-pg") || strstr(args, " crelay ")) {
|
||
cJSON_AddBoolToObject(row, "is_crelay_connection", 1);
|
||
}
|
||
}
|
||
cJSON_AddItemToArray(backends, row);
|
||
row_count++;
|
||
}
|
||
}
|
||
}
|
||
pclose(fp);
|
||
|
||
cJSON_AddItemToObject(cpu_stats, "postgres_backends", backends);
|
||
}
|
||
|
||
// Query CPU usage metrics
|
||
cJSON* query_cpu_metrics(void) {
|
||
cJSON* cpu_stats = cJSON_CreateObject();
|
||
cJSON_AddStringToObject(cpu_stats, "data_type", "cpu_metrics");
|
||
cJSON_AddNumberToObject(cpu_stats, "timestamp", (double)time(NULL));
|
||
|
||
// Read process CPU times from /proc/self/stat
|
||
FILE* proc_stat = fopen("/proc/self/stat", "r");
|
||
if (proc_stat) {
|
||
unsigned long utime, stime; // user and system CPU time in clock ticks
|
||
if (fscanf(proc_stat, "%*d %*s %*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %lu %lu", &utime, &stime) == 2) {
|
||
unsigned long total_proc_time = utime + stime;
|
||
|
||
// Get system CPU times from /proc/stat
|
||
FILE* sys_stat = fopen("/proc/stat", "r");
|
||
if (sys_stat) {
|
||
unsigned long user, nice, system, idle, iowait, irq, softirq;
|
||
if (fscanf(sys_stat, "cpu %lu %lu %lu %lu %lu %lu %lu", &user, &nice, &system, &idle, &iowait, &irq, &softirq) == 7) {
|
||
unsigned long total_sys_time = user + nice + system + idle + iowait + irq + softirq;
|
||
|
||
// Calculate CPU percentages (simplified - would need deltas for accuracy)
|
||
// For now, just store the raw values - frontend can calculate deltas
|
||
cJSON_AddNumberToObject(cpu_stats, "process_cpu_time", (double)total_proc_time);
|
||
cJSON_AddNumberToObject(cpu_stats, "system_cpu_time", (double)total_sys_time);
|
||
cJSON_AddNumberToObject(cpu_stats, "system_idle_time", (double)idle);
|
||
}
|
||
fclose(sys_stat);
|
||
}
|
||
|
||
// Get current CPU core the process is running on
|
||
int current_core = sched_getcpu();
|
||
if (current_core >= 0) {
|
||
cJSON_AddNumberToObject(cpu_stats, "current_cpu_core", current_core);
|
||
}
|
||
}
|
||
fclose(proc_stat);
|
||
}
|
||
|
||
// Get process ID
|
||
pid_t pid = getpid();
|
||
cJSON_AddNumberToObject(cpu_stats, "process_id", (double)pid);
|
||
|
||
// Include relay thread/process snapshots (mirrors c-relay-pg-threads.sh style data)
|
||
add_relay_thread_snapshot(cpu_stats, pid);
|
||
add_postgres_backend_snapshot(cpu_stats);
|
||
|
||
// Get active WebSocket connection count
|
||
cJSON_AddNumberToObject(cpu_stats, "active_connections", (double)get_active_connection_count());
|
||
|
||
// Get memory usage from /proc/self/status and calculate MEM%
|
||
unsigned long rss_kb = 0;
|
||
FILE* mem_stat = fopen("/proc/self/status", "r");
|
||
if (mem_stat) {
|
||
char line[256];
|
||
while (fgets(line, sizeof(line), mem_stat)) {
|
||
if (strncmp(line, "VmRSS:", 6) == 0) {
|
||
if (sscanf(line, "VmRSS: %lu kB", &rss_kb) == 1) {
|
||
double rss_mb = rss_kb / 1024.0;
|
||
cJSON_AddNumberToObject(cpu_stats, "memory_usage_mb", rss_mb);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
fclose(mem_stat);
|
||
}
|
||
|
||
// Get total system memory from /proc/meminfo for MEM%
|
||
if (rss_kb > 0) {
|
||
FILE* meminfo = fopen("/proc/meminfo", "r");
|
||
if (meminfo) {
|
||
char line[256];
|
||
while (fgets(line, sizeof(line), meminfo)) {
|
||
if (strncmp(line, "MemTotal:", 9) == 0) {
|
||
unsigned long total_kb;
|
||
if (sscanf(line, "MemTotal: %lu kB", &total_kb) == 1 && total_kb > 0) {
|
||
double mem_pct = (rss_kb * 100.0) / total_kb;
|
||
cJSON_AddNumberToObject(cpu_stats, "memory_percent", mem_pct);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
fclose(meminfo);
|
||
}
|
||
}
|
||
|
||
return cpu_stats;
|
||
}
|
||
|
||
// Generate stats JSON from database queries
|
||
char* generate_stats_json(void) {
|
||
void* temp_db = NULL;
|
||
if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) {
|
||
DEBUG_ERROR("Database not available for stats generation");
|
||
return NULL;
|
||
}
|
||
|
||
// Build response with database statistics
|
||
cJSON* response = cJSON_CreateObject();
|
||
cJSON_AddStringToObject(response, "query_type", "stats_query");
|
||
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
|
||
|
||
// Get database storage size (PostgreSQL only)
|
||
long long db_size = 0;
|
||
#ifdef DB_BACKEND_POSTGRES
|
||
db_stmt_t* db_size_stmt = NULL;
|
||
if (db_prepare("SELECT pg_database_size(current_database())", &db_size_stmt) == DB_OK) {
|
||
if (db_step_stmt(db_size_stmt) == DB_ROW) {
|
||
db_size = db_column_int64_value(db_size_stmt, 0);
|
||
}
|
||
db_finalize_stmt(db_size_stmt);
|
||
}
|
||
#endif
|
||
cJSON_AddNumberToObject(response, "database_size_bytes", db_size);
|
||
|
||
// Get active subscriptions count from in-memory manager
|
||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||
int active_subs = g_subscription_manager.total_subscriptions;
|
||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||
cJSON_AddNumberToObject(response, "active_subscriptions", active_subs);
|
||
|
||
long long total_events = 0;
|
||
if (db_get_total_event_count_ll(&total_events) == 0) {
|
||
cJSON_AddNumberToObject(response, "total_events", total_events);
|
||
}
|
||
|
||
// Query event kinds distribution
|
||
cJSON* event_kinds = cJSON_CreateArray();
|
||
long long kinds_total = 0;
|
||
cJSON* kind_rows = db_get_event_kind_distribution_rows(&kinds_total);
|
||
if (kind_rows) {
|
||
cJSON* kind_row = NULL;
|
||
cJSON_ArrayForEach(kind_row, kind_rows) {
|
||
cJSON* kind_obj = cJSON_CreateObject();
|
||
cJSON* kind = cJSON_GetObjectItemCaseSensitive(kind_row, "kind");
|
||
cJSON* count = cJSON_GetObjectItemCaseSensitive(kind_row, "count");
|
||
double count_val = cJSON_IsNumber(count) ? count->valuedouble : 0.0;
|
||
double pct = (kinds_total > 0) ? ((count_val * 100.0) / (double)kinds_total) : 0.0;
|
||
|
||
cJSON_AddNumberToObject(kind_obj, "kind", cJSON_IsNumber(kind) ? kind->valuedouble : 0.0);
|
||
cJSON_AddNumberToObject(kind_obj, "count", count_val);
|
||
cJSON_AddNumberToObject(kind_obj, "percentage", pct);
|
||
cJSON_AddItemToArray(event_kinds, kind_obj);
|
||
}
|
||
cJSON_Delete(kind_rows);
|
||
}
|
||
cJSON_AddItemToObject(response, "event_kinds", event_kinds);
|
||
|
||
// Query time-based statistics
|
||
cJSON* time_stats = cJSON_CreateObject();
|
||
time_t now = time(NULL);
|
||
long long c24h = 0, c7d = 0, c30d = 0;
|
||
if (db_get_event_count_since(now - 86400, &c24h) == 0) {
|
||
cJSON_AddNumberToObject(time_stats, "last_24h", c24h);
|
||
}
|
||
if (db_get_event_count_since(now - 604800, &c7d) == 0) {
|
||
cJSON_AddNumberToObject(time_stats, "last_7d", c7d);
|
||
}
|
||
if (db_get_event_count_since(now - 2592000, &c30d) == 0) {
|
||
cJSON_AddNumberToObject(time_stats, "last_30d", c30d);
|
||
}
|
||
cJSON_AddNumberToObject(time_stats, "total", total_events);
|
||
cJSON_AddItemToObject(response, "time_stats", time_stats);
|
||
|
||
// Query top pubkeys
|
||
cJSON* top_pubkeys = cJSON_CreateArray();
|
||
cJSON* pubkey_rows = db_get_top_pubkeys_rows(10);
|
||
if (pubkey_rows) {
|
||
cJSON* row = NULL;
|
||
cJSON_ArrayForEach(row, pubkey_rows) {
|
||
cJSON* pubkey_obj = cJSON_CreateObject();
|
||
cJSON* pubkey_item = cJSON_GetObjectItemCaseSensitive(row, "pubkey");
|
||
cJSON* count_item = cJSON_GetObjectItemCaseSensitive(row, "count");
|
||
double count_val = cJSON_IsNumber(count_item) ? count_item->valuedouble : 0.0;
|
||
double pct = (total_events > 0) ? ((count_val * 100.0) / (double)total_events) : 0.0;
|
||
|
||
cJSON_AddStringToObject(pubkey_obj, "pubkey", cJSON_IsString(pubkey_item) ? pubkey_item->valuestring : "");
|
||
cJSON_AddNumberToObject(pubkey_obj, "event_count", count_val);
|
||
cJSON_AddNumberToObject(pubkey_obj, "percentage", pct);
|
||
cJSON_AddItemToArray(top_pubkeys, pubkey_obj);
|
||
}
|
||
cJSON_Delete(pubkey_rows);
|
||
}
|
||
cJSON_AddItemToObject(response, "top_pubkeys", top_pubkeys);
|
||
|
||
// Get database creation/latest timestamps
|
||
long long min_created_at = 0;
|
||
long long max_created_at = 0;
|
||
if (db_get_event_time_bounds(&min_created_at, &max_created_at) == 0) {
|
||
if (min_created_at > 0) {
|
||
cJSON_AddNumberToObject(response, "database_created_at", (double)min_created_at);
|
||
}
|
||
if (max_created_at > 0) {
|
||
cJSON_AddNumberToObject(response, "latest_event_at", (double)max_created_at);
|
||
}
|
||
}
|
||
|
||
// Convert to JSON string
|
||
char* json_string = cJSON_Print(response);
|
||
cJSON_Delete(response);
|
||
|
||
if (!json_string) {
|
||
DEBUG_ERROR("Failed to generate stats JSON");
|
||
}
|
||
|
||
api_close_temp_db_connection(temp_db);
|
||
return json_string;
|
||
}
|
||
|
||
// Unified NIP-17 response sender - handles all common response logic
|
||
int send_nip17_response(const char* sender_pubkey, const char* response_content,
|
||
char* error_message, size_t error_size) {
|
||
if (!sender_pubkey || !response_content || !error_message) {
|
||
if (error_message) {
|
||
strncpy(error_message, "NIP-17: Invalid parameters for response", error_size - 1);
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
// Get relay keys for signing
|
||
const char* relay_pubkey = get_config_value("relay_pubkey");
|
||
char* relay_privkey_hex = get_relay_private_key();
|
||
if (!relay_pubkey || !relay_privkey_hex) {
|
||
if (relay_privkey_hex) free(relay_privkey_hex);
|
||
strncpy(error_message, "NIP-17: Could not get relay keys for response", error_size - 1);
|
||
return -1;
|
||
}
|
||
|
||
// Convert relay private key to bytes
|
||
unsigned char relay_privkey[32];
|
||
if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) {
|
||
free(relay_privkey_hex);
|
||
strncpy(error_message, "NIP-17: Failed to convert relay private key for response", error_size - 1);
|
||
return -1;
|
||
}
|
||
free(relay_privkey_hex);
|
||
|
||
// Create DM response event using library function
|
||
cJSON* dm_response = nostr_nip17_create_chat_event(
|
||
response_content, // message content
|
||
(const char**)&sender_pubkey, // recipient pubkeys
|
||
1, // num recipients
|
||
NULL, // subject (optional)
|
||
NULL, // reply_to_event_id (optional)
|
||
NULL, // reply_relay_url (optional)
|
||
relay_pubkey // sender pubkey
|
||
);
|
||
|
||
if (!dm_response) {
|
||
strncpy(error_message, "NIP-17: Failed to create DM response event", error_size - 1);
|
||
return -1;
|
||
}
|
||
|
||
// Get timestamp delay configuration
|
||
long max_delay_sec = get_config_int("nip59_timestamp_max_delay_sec", 0);
|
||
|
||
// Create and sign gift wrap using library function
|
||
cJSON* gift_wraps[1];
|
||
int send_result = nostr_nip17_send_dm(
|
||
dm_response, // dm_event
|
||
(const char**)&sender_pubkey, // recipient_pubkeys
|
||
1, // num_recipients
|
||
relay_privkey, // sender_private_key
|
||
gift_wraps, // gift_wraps_out
|
||
1, // max_gift_wraps
|
||
max_delay_sec // max_delay_sec
|
||
);
|
||
|
||
cJSON_Delete(dm_response);
|
||
|
||
if (send_result != 1 || !gift_wraps[0]) {
|
||
strncpy(error_message, "NIP-17: Failed to create and sign response gift wrap", error_size - 1);
|
||
return -1;
|
||
}
|
||
|
||
// Fix the p tag in the gift wrap - library function may use wrong pubkey
|
||
cJSON* gift_wrap_tags = cJSON_GetObjectItemCaseSensitive(gift_wraps[0], "tags");
|
||
if (gift_wrap_tags && cJSON_IsArray(gift_wrap_tags)) {
|
||
// Find and replace the p tag with the correct user pubkey
|
||
cJSON* tag = NULL;
|
||
cJSON_ArrayForEach(tag, gift_wrap_tags) {
|
||
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
|
||
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||
if (tag_name && cJSON_IsString(tag_name) &&
|
||
strcmp(cJSON_GetStringValue(tag_name), "p") == 0) {
|
||
// Replace the p tag value with the correct user pubkey
|
||
cJSON_ReplaceItemInArray(tag, 1, cJSON_CreateString(sender_pubkey));
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Broadcast FIRST before storing (broadcasting needs the event intact)
|
||
// Make a copy for broadcasting to avoid use-after-free issues
|
||
cJSON* gift_wrap_copy = cJSON_Duplicate(gift_wraps[0], 1);
|
||
if (!gift_wrap_copy) {
|
||
cJSON_Delete(gift_wraps[0]);
|
||
strncpy(error_message, "NIP-17: Failed to duplicate gift wrap for broadcast", error_size - 1);
|
||
return -1;
|
||
}
|
||
|
||
// Broadcast the copy to active subscriptions
|
||
broadcast_event_to_subscriptions(gift_wrap_copy);
|
||
|
||
// Store the original in database
|
||
int store_result = store_event(gift_wraps[0]);
|
||
|
||
// Clean up both copies
|
||
cJSON_Delete(gift_wrap_copy);
|
||
cJSON_Delete(gift_wraps[0]);
|
||
|
||
if (store_result != 0) {
|
||
strncpy(error_message, "NIP-17: Failed to store response gift wrap", error_size - 1);
|
||
return -1;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
// Generate config text from database
|
||
char* generate_config_text(void) {
|
||
if (!db_is_available()) {
|
||
DEBUG_ERROR("NIP-17: Database not available for config query");
|
||
return NULL;
|
||
}
|
||
|
||
// Build comprehensive config text from database
|
||
char* config_text = malloc(8192);
|
||
if (!config_text) {
|
||
DEBUG_ERROR("NIP-17: Failed to allocate memory for config text");
|
||
return NULL;
|
||
}
|
||
|
||
int offset = 0;
|
||
|
||
// Header
|
||
offset += snprintf(config_text + offset, 8192 - offset,
|
||
"🔧 Relay Configuration\n"
|
||
"━━━━━━━━━━━━━━━━━━━━━━━━\n");
|
||
|
||
cJSON* rows = db_get_all_config_rows();
|
||
if (!rows) {
|
||
free(config_text);
|
||
DEBUG_ERROR("NIP-17: Failed to query config from database");
|
||
return NULL;
|
||
}
|
||
|
||
cJSON* row = NULL;
|
||
cJSON_ArrayForEach(row, rows) {
|
||
if (offset >= 8192 - 200) break;
|
||
|
||
cJSON* key_item = cJSON_GetObjectItemCaseSensitive(row, "key");
|
||
cJSON* value_item = cJSON_GetObjectItemCaseSensitive(row, "value");
|
||
const char* key = cJSON_IsString(key_item) ? key_item->valuestring : NULL;
|
||
const char* value = cJSON_IsString(value_item) ? value_item->valuestring : NULL;
|
||
|
||
if (key && value) {
|
||
offset += snprintf(config_text + offset, 8192 - offset,
|
||
"%s: %s\n", key, value);
|
||
}
|
||
}
|
||
cJSON_Delete(rows);
|
||
|
||
// Footer
|
||
offset += snprintf(config_text + offset, 8192 - offset, "\n");
|
||
|
||
return config_text;
|
||
}
|
||
|
||
// Helper function to format numbers with commas
|
||
void format_number_with_commas(long long num, char* buffer, size_t buffer_size) {
|
||
char temp[32];
|
||
int len = snprintf(temp, sizeof(temp), "%lld", num);
|
||
int comma_count = (len - 1) / 3;
|
||
int result_len = len + comma_count;
|
||
|
||
if (result_len >= (int)buffer_size) {
|
||
buffer[0] = '\0';
|
||
return;
|
||
}
|
||
|
||
int j = result_len - 1;
|
||
int k = len - 1;
|
||
int comma_pos = 0;
|
||
|
||
while (k >= 0) {
|
||
buffer[j--] = temp[k--];
|
||
comma_pos++;
|
||
if (comma_pos % 3 == 0 && k >= 0) {
|
||
buffer[j--] = ',';
|
||
}
|
||
}
|
||
buffer[result_len] = '\0';
|
||
}
|
||
|
||
// Generate human-readable stats text
|
||
char* generate_stats_text(void) {
|
||
char* stats_json = generate_stats_json();
|
||
if (!stats_json) {
|
||
DEBUG_ERROR("NIP-17: Failed to generate stats for plain text command");
|
||
return NULL;
|
||
}
|
||
|
||
// Parse the JSON to extract values for human-readable format
|
||
cJSON* stats_obj = cJSON_Parse(stats_json);
|
||
char* stats_text = malloc(16384); // Increased buffer size for comprehensive stats
|
||
if (!stats_text) {
|
||
free(stats_json);
|
||
if (stats_obj) cJSON_Delete(stats_obj);
|
||
return NULL;
|
||
}
|
||
|
||
if (stats_obj) {
|
||
// Extract basic metrics
|
||
cJSON* total_events = cJSON_GetObjectItemCaseSensitive(stats_obj, "total_events");
|
||
cJSON* db_size = cJSON_GetObjectItemCaseSensitive(stats_obj, "database_size_bytes");
|
||
cJSON* oldest_event = cJSON_GetObjectItemCaseSensitive(stats_obj, "database_created_at");
|
||
cJSON* newest_event = cJSON_GetObjectItemCaseSensitive(stats_obj, "latest_event_at");
|
||
cJSON* time_stats = cJSON_GetObjectItemCaseSensitive(stats_obj, "time_stats");
|
||
cJSON* event_kinds = cJSON_GetObjectItemCaseSensitive(stats_obj, "event_kinds");
|
||
// cJSON* top_pubkeys = cJSON_GetObjectItemCaseSensitive(stats_obj, "top_pubkeys");
|
||
|
||
long long total = total_events ? (long long)cJSON_GetNumberValue(total_events) : 0;
|
||
long long db_bytes = db_size ? (long long)cJSON_GetNumberValue(db_size) : 0;
|
||
double db_mb = db_bytes / (1024.0 * 1024.0);
|
||
|
||
// Get active subscriptions count from in-memory manager
|
||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||
int active_subs = g_subscription_manager.total_subscriptions;
|
||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||
|
||
// Format timestamps
|
||
char oldest_str[64] = "-";
|
||
char newest_str[64] = "-";
|
||
if (oldest_event && cJSON_GetNumberValue(oldest_event) > 0) {
|
||
time_t oldest_ts = (time_t)cJSON_GetNumberValue(oldest_event);
|
||
struct tm* tm_info = localtime(&oldest_ts);
|
||
strftime(oldest_str, sizeof(oldest_str), "%m/%d/%Y, %I:%M:%S %p", tm_info);
|
||
}
|
||
if (newest_event && cJSON_GetNumberValue(newest_event) > 0) {
|
||
time_t newest_ts = (time_t)cJSON_GetNumberValue(newest_event);
|
||
struct tm* tm_info = localtime(&newest_ts);
|
||
strftime(newest_str, sizeof(newest_str), "%m/%d/%Y, %I:%M:%S %p", tm_info);
|
||
}
|
||
|
||
// Extract time-based stats
|
||
long long last_24h = 0, last_7d = 0, last_30d = 0;
|
||
if (time_stats) {
|
||
cJSON* h24 = cJSON_GetObjectItemCaseSensitive(time_stats, "last_24h");
|
||
cJSON* d7 = cJSON_GetObjectItemCaseSensitive(time_stats, "last_7d");
|
||
cJSON* d30 = cJSON_GetObjectItemCaseSensitive(time_stats, "last_30d");
|
||
last_24h = h24 ? (long long)cJSON_GetNumberValue(h24) : 0;
|
||
last_7d = d7 ? (long long)cJSON_GetNumberValue(d7) : 0;
|
||
last_30d = d30 ? (long long)cJSON_GetNumberValue(d30) : 0;
|
||
}
|
||
|
||
// Start building the comprehensive stats text
|
||
int offset = 0;
|
||
|
||
// Header
|
||
offset += snprintf(stats_text + offset, 16384 - offset,
|
||
"\nRelay Statistics\n"
|
||
"━━━━━━━━━━━━━━━━━━━━\n");
|
||
|
||
// Database Overview section
|
||
offset += snprintf(stats_text + offset, 16384 - offset,
|
||
|
||
"Database Size %.2f MB\n"
|
||
"Total Events %lld\n"
|
||
"Active Subscriptions %d\n"
|
||
"Oldest Event %s\n"
|
||
"Newest Event %s\n"
|
||
"\n",
|
||
db_mb, total, active_subs, oldest_str, newest_str);
|
||
|
||
// Event Kind Distribution section
|
||
offset += snprintf(stats_text + offset, 16384 - offset,
|
||
"Event_Kind_Distribution:\n");
|
||
|
||
if (event_kinds && cJSON_IsArray(event_kinds)) {
|
||
cJSON* kind_item = NULL;
|
||
cJSON_ArrayForEach(kind_item, event_kinds) {
|
||
cJSON* kind = cJSON_GetObjectItemCaseSensitive(kind_item, "kind");
|
||
cJSON* count = cJSON_GetObjectItemCaseSensitive(kind_item, "count");
|
||
cJSON* percentage = cJSON_GetObjectItemCaseSensitive(kind_item, "percentage");
|
||
|
||
if (kind && count && percentage) {
|
||
// Format event kind (right-justified, minimum 5 chars wide with underscores)
|
||
char kind_str[16];
|
||
int kind_val = (int)cJSON_GetNumberValue(kind);
|
||
char temp_kind[16];
|
||
snprintf(temp_kind, sizeof(temp_kind), "%d", kind_val);
|
||
|
||
// Calculate padding needed (minimum 5 chars, but more if kind is longer)
|
||
int kind_len = strlen(temp_kind);
|
||
int min_width = 5;
|
||
int total_width = (kind_len > min_width) ? kind_len : min_width;
|
||
int padding = total_width - kind_len;
|
||
|
||
// Create padded string with underscores
|
||
memset(kind_str, '_', padding);
|
||
strcpy(kind_str + padding, temp_kind);
|
||
|
||
// Format count with commas (right-justified, 11 chars wide with underscores)
|
||
char count_str[16];
|
||
format_number_with_commas((long long)cJSON_GetNumberValue(count), count_str, sizeof(count_str));
|
||
char count_formatted[16];
|
||
snprintf(count_formatted, sizeof(count_formatted), "%11s", count_str);
|
||
// Replace spaces with underscores for alignment
|
||
for (int i = 0; count_formatted[i]; i++) {
|
||
if (count_formatted[i] == ' ') count_formatted[i] = '_';
|
||
}
|
||
|
||
// Format percentage (right-justified, 7 chars wide including %, with underscores)
|
||
double pct = cJSON_GetNumberValue(percentage);
|
||
char pct_str[8];
|
||
snprintf(pct_str, sizeof(pct_str), "%.1f%%", pct);
|
||
// Pad with underscores to make 7 chars total (right-justified)
|
||
char pct_formatted[8];
|
||
int pct_len = strlen(pct_str);
|
||
int underscores_needed = 7 - pct_len;
|
||
memset(pct_formatted, '_', underscores_needed);
|
||
strcpy(pct_formatted + underscores_needed, pct_str);
|
||
|
||
offset += snprintf(stats_text + offset, 16384 - offset,
|
||
"%s%s_%s\n", kind_str, count_formatted, pct_formatted);
|
||
}
|
||
}
|
||
} else {
|
||
offset += snprintf(stats_text + offset, 16384 - offset,
|
||
"No event data available\n");
|
||
}
|
||
|
||
offset += snprintf(stats_text + offset, 16384 - offset, "\n");
|
||
|
||
// Time-based Statistics section
|
||
offset += snprintf(stats_text + offset, 16384 - offset,
|
||
"Time-based Statistics:\n"
|
||
|
||
"%lld\tEvents in the last day\n"
|
||
"%lld\tEvents in the last week\n"
|
||
"%lld\tEvents in the last month\n"
|
||
"\n",
|
||
last_24h, last_7d, last_30d);
|
||
|
||
// Top Pubkeys section
|
||
// offset += snprintf(stats_text + offset, 16384 - offset,
|
||
// "Top Pubkeys by Event Count:\n"
|
||
// "Rank\tPubkey\tEvent Count\tPercentage\n");
|
||
|
||
// if (top_pubkeys && cJSON_IsArray(top_pubkeys)) {
|
||
// int rank = 1;
|
||
// cJSON* pubkey_item = NULL;
|
||
// cJSON_ArrayForEach(pubkey_item, top_pubkeys) {
|
||
// cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(pubkey_item, "pubkey");
|
||
// cJSON* event_count = cJSON_GetObjectItemCaseSensitive(pubkey_item, "event_count");
|
||
// cJSON* percentage = cJSON_GetObjectItemCaseSensitive(pubkey_item, "percentage");
|
||
|
||
// if (pubkey && event_count && percentage) {
|
||
// const char* pubkey_str = cJSON_GetStringValue(pubkey);
|
||
// char short_pubkey[20] = "...";
|
||
// if (pubkey_str && strlen(pubkey_str) >= 16) {
|
||
// snprintf(short_pubkey, sizeof(short_pubkey), "%.16s...", pubkey_str);
|
||
// }
|
||
|
||
// offset += snprintf(stats_text + offset, 16384 - offset,
|
||
// "%d\t%s\t%lld\t%.1f%%\n",
|
||
// rank++,
|
||
// short_pubkey,
|
||
// (long long)cJSON_GetNumberValue(event_count),
|
||
// cJSON_GetNumberValue(percentage));
|
||
// }
|
||
// }
|
||
// } else {
|
||
// offset += snprintf(stats_text + offset, 16384 - offset,
|
||
// "No pubkey data available\n");
|
||
// }
|
||
|
||
// Footer
|
||
offset += snprintf(stats_text + offset, 16384 - offset, "\n");
|
||
|
||
cJSON_Delete(stats_obj);
|
||
} else {
|
||
// Fallback if JSON parsing fails
|
||
snprintf(stats_text, 16384,
|
||
"📊 Relay Statistics\n"
|
||
"━━━━━━━━━━━━━━━━━━━━\n"
|
||
"Raw data: %s\n"
|
||
"\n"
|
||
"⚠️ Could not parse statistics data",
|
||
stats_json
|
||
);
|
||
}
|
||
|
||
free(stats_json);
|
||
return stats_text;
|
||
}
|
||
|
||
// =============================================================================
|
||
// CONFIGURATION CHANGE IMPLEMENTATION
|
||
// =============================================================================
|
||
|
||
// Parse configuration command from natural language
|
||
// Supports patterns like:
|
||
// - "auth_enabled true"
|
||
// - "set auth_enabled to true"
|
||
// - "change auth_enabled true"
|
||
// - "auth_enabled = true"
|
||
// - "auth_enabled: true"
|
||
// - "enable auth" / "disable auth"
|
||
int parse_config_command(const char* message, char* key, char* value) {
|
||
if (!message || !key || !value) {
|
||
return 0;
|
||
}
|
||
|
||
// Clean up the message - convert to lowercase and trim
|
||
char clean_msg[512];
|
||
size_t msg_len = strlen(message);
|
||
size_t copy_len = msg_len < sizeof(clean_msg) - 1 ? msg_len : sizeof(clean_msg) - 1;
|
||
memcpy(clean_msg, message, copy_len);
|
||
clean_msg[copy_len] = '\0';
|
||
|
||
// Convert to lowercase
|
||
for (size_t i = 0; i < copy_len; i++) {
|
||
if (clean_msg[i] >= 'A' && clean_msg[i] <= 'Z') {
|
||
clean_msg[i] = clean_msg[i] + 32;
|
||
}
|
||
}
|
||
|
||
// Remove leading/trailing whitespace
|
||
char* start = clean_msg;
|
||
while (*start == ' ' || *start == '\t') start++;
|
||
char* end = start + strlen(start) - 1;
|
||
while (end > start && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) {
|
||
*end = '\0';
|
||
end--;
|
||
}
|
||
|
||
// Pattern 1: "enable auth" -> "auth_enabled true"
|
||
if (strstr(start, "enable auth") == start) {
|
||
strcpy(key, "auth_enabled");
|
||
strcpy(value, "true");
|
||
return 1;
|
||
}
|
||
|
||
// Pattern 2: "disable auth" -> "auth_enabled false"
|
||
if (strstr(start, "disable auth") == start) {
|
||
strcpy(key, "auth_enabled");
|
||
strcpy(value, "false");
|
||
return 1;
|
||
}
|
||
|
||
// Pattern 3: "enable nip42" -> "nip42_auth_required true"
|
||
if (strstr(start, "enable nip42") == start) {
|
||
strcpy(key, "nip42_auth_required");
|
||
strcpy(value, "true");
|
||
return 1;
|
||
}
|
||
|
||
// Pattern 4: "disable nip42" -> "nip42_auth_required false"
|
||
if (strstr(start, "disable nip42") == start) {
|
||
strcpy(key, "nip42_auth_required");
|
||
strcpy(value, "false");
|
||
return 1;
|
||
}
|
||
|
||
// Pattern 5: "set KEY to VALUE" or "change KEY to VALUE"
|
||
char* set_pos = strstr(start, "set ");
|
||
char* change_pos = strstr(start, "change ");
|
||
char* to_pos = strstr(start, " to ");
|
||
|
||
if ((set_pos == start || change_pos == start) && to_pos) {
|
||
char* key_start = (set_pos == start) ? start + 4 : start + 7;
|
||
size_t key_len = to_pos - key_start;
|
||
if (key_len > 0 && key_len < 127) {
|
||
memcpy(key, key_start, key_len);
|
||
key[key_len] = '\0';
|
||
|
||
// Trim key
|
||
char* key_end = key + strlen(key) - 1;
|
||
while (key_end > key && (*key_end == ' ' || *key_end == '\t')) {
|
||
*key_end = '\0';
|
||
key_end--;
|
||
}
|
||
|
||
char* value_start = to_pos + 4;
|
||
strcpy(value, value_start);
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
// Pattern 6: "KEY = VALUE"
|
||
char* equals_pos = strstr(start, " = ");
|
||
if (equals_pos) {
|
||
size_t key_len = equals_pos - start;
|
||
if (key_len > 0 && key_len < 127) {
|
||
memcpy(key, start, key_len);
|
||
key[key_len] = '\0';
|
||
strcpy(value, equals_pos + 3);
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
// Pattern 7: "KEY: VALUE" (colon-separated)
|
||
char* colon_pos = strstr(start, ": ");
|
||
if (colon_pos) {
|
||
size_t key_len = colon_pos - start;
|
||
if (key_len > 0 && key_len < 127) {
|
||
memcpy(key, start, key_len);
|
||
key[key_len] = '\0';
|
||
strcpy(value, colon_pos + 2);
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
// Pattern 8: "KEY VALUE" (simple space-separated)
|
||
char* space_pos = strchr(start, ' ');
|
||
if (space_pos) {
|
||
size_t key_len = space_pos - start;
|
||
if (key_len > 0 && key_len < 127) {
|
||
memcpy(key, start, key_len);
|
||
key[key_len] = '\0';
|
||
strcpy(value, space_pos + 1);
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
return 0; // No pattern matched
|
||
}
|
||
|
||
// Validate configuration key and value
|
||
int validate_config_change(const char* key, const char* value) {
|
||
if (!key || !value) {
|
||
return 0;
|
||
}
|
||
|
||
|
||
// Find the configuration key
|
||
int found = 0;
|
||
const char* expected_type = NULL;
|
||
int min_val = 0, max_val = 0;
|
||
|
||
for (int i = 0; known_configs[i].key != NULL; i++) {
|
||
if (strcmp(key, known_configs[i].key) == 0) {
|
||
found = 1;
|
||
expected_type = known_configs[i].type;
|
||
min_val = known_configs[i].min_val;
|
||
max_val = known_configs[i].max_val;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!found) {
|
||
return 0; // Unknown configuration key
|
||
}
|
||
|
||
// Validate value based on type
|
||
if (strcmp(expected_type, "bool") == 0) {
|
||
if (strcmp(value, "true") == 0 || strcmp(value, "false") == 0 ||
|
||
strcmp(value, "1") == 0 || strcmp(value, "0") == 0 ||
|
||
strcmp(value, "yes") == 0 || strcmp(value, "no") == 0 ||
|
||
strcmp(value, "on") == 0 || strcmp(value, "off") == 0) {
|
||
return 1;
|
||
}
|
||
return 0;
|
||
} else if (strcmp(expected_type, "int") == 0) {
|
||
char* endptr;
|
||
long val = strtol(value, &endptr, 10);
|
||
if (*endptr != '\0') {
|
||
return 0; // Not a valid integer
|
||
}
|
||
if (val < min_val || val > max_val) {
|
||
return 0; // Out of range
|
||
}
|
||
return 1;
|
||
} else if (strcmp(expected_type, "string") == 0) {
|
||
// String values are generally valid, but check length
|
||
if (strlen(value) > 255) {
|
||
return 0; // Too long
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
// Generate a unique change ID based on admin pubkey and timestamp
|
||
void generate_change_id(const char* admin_pubkey, char* change_id) {
|
||
char input[128];
|
||
snprintf(input, sizeof(input), "%s_%ld", admin_pubkey, time(NULL));
|
||
|
||
// Simple hash - just use first 32 chars of the input
|
||
size_t input_len = strlen(input);
|
||
for (int i = 0; i < 32 && i < (int)input_len; i++) {
|
||
change_id[i] = input[i];
|
||
}
|
||
change_id[32] = '\0';
|
||
}
|
||
|
||
// Store a pending configuration change
|
||
char* store_pending_config_change(const char* admin_pubkey, const char* key,
|
||
const char* old_value, const char* new_value) {
|
||
if (!admin_pubkey || !key || !old_value || !new_value) {
|
||
return NULL;
|
||
}
|
||
|
||
// Clean up expired changes first
|
||
cleanup_expired_pending_changes();
|
||
|
||
// Create new pending change
|
||
pending_config_change_t* change = malloc(sizeof(pending_config_change_t));
|
||
if (!change) {
|
||
return NULL;
|
||
}
|
||
|
||
strncpy(change->admin_pubkey, admin_pubkey, sizeof(change->admin_pubkey) - 1);
|
||
change->admin_pubkey[sizeof(change->admin_pubkey) - 1] = '\0';
|
||
|
||
strncpy(change->config_key, key, sizeof(change->config_key) - 1);
|
||
change->config_key[sizeof(change->config_key) - 1] = '\0';
|
||
|
||
strncpy(change->old_value, old_value, sizeof(change->old_value) - 1);
|
||
change->old_value[sizeof(change->old_value) - 1] = '\0';
|
||
|
||
strncpy(change->new_value, new_value, sizeof(change->new_value) - 1);
|
||
change->new_value[sizeof(change->new_value) - 1] = '\0';
|
||
|
||
change->timestamp = time(NULL);
|
||
generate_change_id(admin_pubkey, change->change_id);
|
||
|
||
// Add to linked list
|
||
change->next = pending_changes_head;
|
||
pending_changes_head = change;
|
||
pending_changes_count++;
|
||
|
||
// Return a copy of the change ID
|
||
char* change_id_copy = malloc(33);
|
||
if (change_id_copy) {
|
||
strcpy(change_id_copy, change->change_id);
|
||
}
|
||
return change_id_copy;
|
||
}
|
||
|
||
// Find a pending change by admin pubkey and change ID
|
||
pending_config_change_t* find_pending_change(const char* admin_pubkey, const char* change_id) {
|
||
if (!admin_pubkey) {
|
||
return NULL;
|
||
}
|
||
|
||
pending_config_change_t* current = pending_changes_head;
|
||
while (current) {
|
||
if (strcmp(current->admin_pubkey, admin_pubkey) == 0) {
|
||
if (!change_id || strcmp(current->change_id, change_id) == 0) {
|
||
return current;
|
||
}
|
||
}
|
||
current = current->next;
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
// Find the most recent pending change for an admin
|
||
pending_config_change_t* find_latest_pending_change(const char* admin_pubkey) {
|
||
if (!admin_pubkey) {
|
||
return NULL;
|
||
}
|
||
|
||
pending_config_change_t* latest = NULL;
|
||
pending_config_change_t* current = pending_changes_head;
|
||
|
||
while (current) {
|
||
if (strcmp(current->admin_pubkey, admin_pubkey) == 0) {
|
||
if (!latest || current->timestamp > latest->timestamp) {
|
||
latest = current;
|
||
}
|
||
}
|
||
current = current->next;
|
||
}
|
||
return latest;
|
||
}
|
||
|
||
// Remove a pending change from the list
|
||
void remove_pending_change(pending_config_change_t* change_to_remove) {
|
||
if (!change_to_remove) {
|
||
return;
|
||
}
|
||
|
||
if (pending_changes_head == change_to_remove) {
|
||
pending_changes_head = change_to_remove->next;
|
||
} else {
|
||
pending_config_change_t* current = pending_changes_head;
|
||
while (current && current->next != change_to_remove) {
|
||
current = current->next;
|
||
}
|
||
if (current) {
|
||
current->next = change_to_remove->next;
|
||
}
|
||
}
|
||
|
||
free(change_to_remove);
|
||
pending_changes_count--;
|
||
}
|
||
|
||
// Clean up expired pending changes (older than 5 minutes)
|
||
void cleanup_expired_pending_changes(void) {
|
||
time_t now = time(NULL);
|
||
pending_config_change_t* current = pending_changes_head;
|
||
|
||
while (current) {
|
||
pending_config_change_t* next = current->next;
|
||
if (now - current->timestamp > CONFIG_CHANGE_TIMEOUT) {
|
||
remove_pending_change(current);
|
||
}
|
||
current = next;
|
||
}
|
||
}
|
||
|
||
// Apply a configuration change to the database
|
||
int apply_config_change(const char* key, const char* value) {
|
||
if (!key || !value) {
|
||
return -1;
|
||
}
|
||
|
||
if (!db_is_available()) {
|
||
DEBUG_ERROR("Database not available for config change");
|
||
return -1;
|
||
}
|
||
|
||
// Normalize boolean values
|
||
char normalized_value[256];
|
||
strncpy(normalized_value, value, sizeof(normalized_value) - 1);
|
||
normalized_value[sizeof(normalized_value) - 1] = '\0';
|
||
|
||
// Convert various boolean representations to "true"/"false"
|
||
if (strcmp(value, "1") == 0 || strcmp(value, "yes") == 0 || strcmp(value, "on") == 0) {
|
||
strcpy(normalized_value, "true");
|
||
} else if (strcmp(value, "0") == 0 || strcmp(value, "no") == 0 || strcmp(value, "off") == 0) {
|
||
strcpy(normalized_value, "false");
|
||
}
|
||
|
||
// Determine the data type based on the configuration key
|
||
const char* data_type = "string"; // Default to string
|
||
for (int i = 0; known_configs[i].key != NULL; i++) {
|
||
if (strcmp(key, known_configs[i].key) == 0) {
|
||
if (strcmp(known_configs[i].type, "bool") == 0) {
|
||
data_type = "boolean";
|
||
} else if (strcmp(known_configs[i].type, "int") == 0) {
|
||
data_type = "integer";
|
||
} else if (strcmp(known_configs[i].type, "string") == 0) {
|
||
data_type = "string";
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Update or insert the configuration value
|
||
if (db_upsert_config_value(key, normalized_value, data_type) != 0) {
|
||
DEBUG_ERROR("Failed to update configuration in database");
|
||
return -1;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
// Generate confirmation message for config change
|
||
char* generate_config_change_confirmation(const char* key, const char* old_value, const char* new_value) {
|
||
if (!key || !old_value || !new_value) {
|
||
return NULL;
|
||
}
|
||
|
||
char* confirmation = malloc(2048);
|
||
if (!confirmation) {
|
||
return NULL;
|
||
}
|
||
|
||
// Get description for the config key
|
||
const char* description = "";
|
||
if (strcmp(key, "auth_enabled") == 0) {
|
||
description = "This controls whether authentication is required for the relay.";
|
||
} else if (strcmp(key, "nip42_auth_required") == 0) {
|
||
description = "This controls whether NIP-42 authentication is required.";
|
||
} else if (strcmp(key, "nip40_expiration_enabled") == 0) {
|
||
description = "This controls whether NIP-40 event expiration is enabled.";
|
||
} else if (strcmp(key, "max_connections") == 0) {
|
||
description = "This sets the maximum number of concurrent connections.";
|
||
} else if (strcmp(key, "max_subscriptions_per_client") == 0) {
|
||
description = "This sets the maximum subscriptions per client.";
|
||
} else if (strcmp(key, "pow_min_difficulty") == 0) {
|
||
description = "This sets the minimum proof-of-work difficulty required.";
|
||
} else if (strcmp(key, "idle_connection_timeout_sec") == 0) {
|
||
description = "Seconds before an idle connection (no REQ/EVENT) is closed and IP flagged. 0 to disable.";
|
||
} else if (strcmp(key, "idle_ban_enabled") == 0) {
|
||
description = "Whether to ban IPs that repeatedly connect without sending REQ or EVENT.";
|
||
} else if (strcmp(key, "idle_ban_threshold") == 0) {
|
||
description = "Number of idle/early-disconnect failures before banning an IP.";
|
||
} else if (strcmp(key, "idle_ban_window_sec") == 0) {
|
||
description = "Time window in seconds for counting idle failures.";
|
||
} else if (strcmp(key, "idle_ban_duration_sec") == 0) {
|
||
description = "Initial ban duration in seconds for idle failures (doubles each time).";
|
||
} else if (strstr(key, "relay_") == key) {
|
||
description = "This changes relay metadata information.";
|
||
}
|
||
|
||
snprintf(confirmation, 2048,
|
||
"🔧 Configuration Change Request\n"
|
||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||
"\n"
|
||
"Setting: %s\n"
|
||
"Current Value: %s\n"
|
||
"New Value: %s\n"
|
||
"\n"
|
||
"%s%s"
|
||
"\n"
|
||
"⚠️ Reply with 'yes' to confirm or 'no' to cancel.\n"
|
||
"⏰ This request will expire in 5 minutes.",
|
||
key, old_value, new_value,
|
||
strlen(description) > 0 ? "ℹ️ " : "",
|
||
description
|
||
);
|
||
|
||
return confirmation;
|
||
}
|
||
|
||
// Handle confirmation responses (yes/no)
|
||
int handle_config_confirmation(const char* admin_pubkey, const char* response) {
|
||
if (!admin_pubkey || !response) {
|
||
return -1;
|
||
}
|
||
|
||
// Clean up expired changes first
|
||
cleanup_expired_pending_changes();
|
||
|
||
// Convert response to lowercase
|
||
char response_lower[64];
|
||
size_t response_len = strlen(response);
|
||
size_t copy_len = response_len < sizeof(response_lower) - 1 ? response_len : sizeof(response_lower) - 1;
|
||
memcpy(response_lower, response, copy_len);
|
||
response_lower[copy_len] = '\0';
|
||
|
||
for (size_t i = 0; i < copy_len; i++) {
|
||
if (response_lower[i] >= 'A' && response_lower[i] <= 'Z') {
|
||
response_lower[i] = response_lower[i] + 32;
|
||
}
|
||
}
|
||
|
||
// Trim whitespace
|
||
char* start = response_lower;
|
||
while (*start == ' ' || *start == '\t') start++;
|
||
char* end = start + strlen(start) - 1;
|
||
while (end > start && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) {
|
||
*end = '\0';
|
||
end--;
|
||
}
|
||
|
||
// Check if it's a confirmation response
|
||
int is_yes = (strcmp(start, "yes") == 0 || strcmp(start, "y") == 0 ||
|
||
strcmp(start, "confirm") == 0 || strcmp(start, "ok") == 0);
|
||
int is_no = (strcmp(start, "no") == 0 || strcmp(start, "n") == 0 ||
|
||
strcmp(start, "cancel") == 0 || strcmp(start, "abort") == 0);
|
||
|
||
if (!is_yes && !is_no) {
|
||
return 0; // Not a confirmation response
|
||
}
|
||
|
||
// Find the most recent pending change for this admin
|
||
pending_config_change_t* change = find_latest_pending_change(admin_pubkey);
|
||
if (!change) {
|
||
return -2; // No pending changes
|
||
}
|
||
|
||
if (is_yes) {
|
||
// Apply the configuration change
|
||
int result = apply_config_change(change->config_key, change->new_value);
|
||
if (result == 0) {
|
||
// Send success response
|
||
char success_msg[1024];
|
||
snprintf(success_msg, sizeof(success_msg),
|
||
"✅ Configuration Updated\n"
|
||
"━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||
"\n"
|
||
"%s: %s → %s\n"
|
||
"\n"
|
||
"Change applied successfully.",
|
||
change->config_key, change->old_value, change->new_value
|
||
);
|
||
|
||
char error_msg[256];
|
||
int send_result = send_nip17_response(admin_pubkey, success_msg, error_msg, sizeof(error_msg));
|
||
if (send_result != 0) {
|
||
DEBUG_ERROR(error_msg);
|
||
}
|
||
|
||
// Remove the pending change
|
||
remove_pending_change(change);
|
||
return 1; // Success
|
||
} else {
|
||
// Send error response
|
||
char error_msg[1024];
|
||
snprintf(error_msg, sizeof(error_msg),
|
||
"❌ Configuration Update Failed\n"
|
||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||
"\n"
|
||
"Failed to apply change: %s = %s\n"
|
||
"\n"
|
||
"Please check the relay logs for details.",
|
||
change->config_key, change->new_value
|
||
);
|
||
|
||
char send_error_msg[256];
|
||
int send_result = send_nip17_response(admin_pubkey, error_msg, send_error_msg, sizeof(send_error_msg));
|
||
if (send_result != 0) {
|
||
DEBUG_ERROR(send_error_msg);
|
||
}
|
||
|
||
// Remove the pending change
|
||
remove_pending_change(change);
|
||
return -3; // Application failed
|
||
}
|
||
} else if (is_no) {
|
||
// Cancel the change
|
||
char cancel_msg[512];
|
||
snprintf(cancel_msg, sizeof(cancel_msg),
|
||
"🚫 Configuration Change Cancelled\n"
|
||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||
"\n"
|
||
"Change cancelled: %s\n"
|
||
"\n"
|
||
"No changes were made to the relay configuration.",
|
||
change->config_key
|
||
);
|
||
|
||
send_nip17_response(admin_pubkey, cancel_msg, NULL, 0);
|
||
|
||
// Remove the pending change
|
||
remove_pending_change(change);
|
||
return 2; // Cancelled
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
// Process a configuration change request
|
||
int process_config_change_request(const char* admin_pubkey, const char* message) {
|
||
if (!admin_pubkey || !message) {
|
||
return -1;
|
||
}
|
||
|
||
char key[128], value[256];
|
||
|
||
// Parse the configuration command
|
||
if (!parse_config_command(message, key, value)) {
|
||
return 0; // Not a config command
|
||
}
|
||
|
||
// Validate the configuration change
|
||
if (!validate_config_change(key, value)) {
|
||
char error_msg[2048];
|
||
snprintf(error_msg, sizeof(error_msg),
|
||
"❌ Invalid Configuration\n"
|
||
"━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||
"\n"
|
||
"Invalid configuration: %s = %s\n"
|
||
"\n"
|
||
"The configuration key '%s' is either unknown or the value '%s' is invalid.\n"
|
||
"\n"
|
||
"Supported keys include: auth_enabled, max_connections, default_limit, relay_description, etc.\n"
|
||
"Use 'config' command to see all current settings.",
|
||
key, value, key, value
|
||
);
|
||
send_nip17_response(admin_pubkey, error_msg, NULL, 0);
|
||
return -2;
|
||
}
|
||
|
||
// Get current value
|
||
const char* current_value = get_config_value(key);
|
||
if (!current_value) {
|
||
current_value = "unset";
|
||
}
|
||
|
||
// Check if the value is already set to the requested value
|
||
if (strcmp(current_value, value) == 0) {
|
||
char already_set_msg[1024];
|
||
snprintf(already_set_msg, sizeof(already_set_msg),
|
||
"ℹ️ Configuration Already Set\n"
|
||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||
"\n"
|
||
"%s is already set to: %s\n"
|
||
"\n"
|
||
"No change needed.",
|
||
key, value
|
||
);
|
||
send_nip17_response(admin_pubkey, already_set_msg, NULL, 0);
|
||
return 3;
|
||
}
|
||
|
||
// Store the pending change
|
||
char* change_id = store_pending_config_change(admin_pubkey, key, current_value, value);
|
||
if (!change_id) {
|
||
char error_msg[256];
|
||
snprintf(error_msg, sizeof(error_msg),
|
||
"❌ Failed to store configuration change request.\n"
|
||
"Please try again."
|
||
);
|
||
send_nip17_response(admin_pubkey, error_msg, NULL, 0);
|
||
return -3;
|
||
}
|
||
|
||
// Generate and send confirmation message
|
||
char* confirmation = generate_config_change_confirmation(key, current_value, value);
|
||
if (confirmation) {
|
||
char error_msg[256];
|
||
int send_result = send_nip17_response(admin_pubkey, confirmation, error_msg, sizeof(error_msg));
|
||
if (send_result != 0) {
|
||
DEBUG_ERROR(error_msg);
|
||
}
|
||
free(confirmation);
|
||
}
|
||
|
||
free(change_id);
|
||
return 1; // Confirmation sent
|
||
}
|
||
|
||
// Forward declarations for relay event creation functions
|
||
cJSON* create_relay_metadata_event(cJSON* metadata);
|
||
cJSON* create_relay_dm_list_event(cJSON* dm_relays);
|
||
cJSON* create_relay_list_event(cJSON* relays);
|
||
|
||
// Handle create_relay_event admin commands
|
||
int handle_create_relay_event_command(cJSON* event, int kind, cJSON* event_data, char* error_message, size_t error_size, struct lws* wsi) {
|
||
if (!event || !event_data || !error_message) {
|
||
return -1;
|
||
}
|
||
|
||
// Get request event ID for response correlation
|
||
cJSON* request_id_obj = cJSON_GetObjectItemCaseSensitive(event, "id");
|
||
if (!request_id_obj || !cJSON_IsString(request_id_obj)) {
|
||
snprintf(error_message, error_size, "Missing request event ID");
|
||
return -1;
|
||
}
|
||
const char* request_id = cJSON_GetStringValue(request_id_obj);
|
||
|
||
// Get sender pubkey for response
|
||
cJSON* sender_pubkey_obj = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
|
||
if (!sender_pubkey_obj || !cJSON_IsString(sender_pubkey_obj)) {
|
||
snprintf(error_message, error_size, "Missing sender pubkey");
|
||
return -1;
|
||
}
|
||
const char* sender_pubkey = cJSON_GetStringValue(sender_pubkey_obj);
|
||
|
||
// Create the relay event based on kind
|
||
cJSON* relay_event = NULL;
|
||
switch (kind) {
|
||
case 0: // User metadata
|
||
relay_event = create_relay_metadata_event(event_data);
|
||
break;
|
||
case 10050: // DM relay list
|
||
relay_event = create_relay_dm_list_event(event_data);
|
||
break;
|
||
case 10002: // Relay list
|
||
relay_event = create_relay_list_event(event_data);
|
||
break;
|
||
default: {
|
||
char response_content[256];
|
||
snprintf(response_content, sizeof(response_content),
|
||
"❌ Unsupported event kind: %d\n\nSupported kinds: 0 (metadata), 10050 (DM relays), 10002 (relays)",
|
||
kind);
|
||
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
||
}
|
||
}
|
||
|
||
if (!relay_event) {
|
||
char response_content[128];
|
||
snprintf(response_content, sizeof(response_content),
|
||
"❌ Failed to create relay event (kind %d)\n\nCheck relay logs for details.", kind);
|
||
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
||
}
|
||
|
||
// Store the event in database
|
||
int store_result = store_event(relay_event);
|
||
if (store_result != 0) {
|
||
cJSON_Delete(relay_event);
|
||
char response_content[128];
|
||
snprintf(response_content, sizeof(response_content),
|
||
"❌ Failed to store relay event (kind %d) in database", kind);
|
||
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
||
}
|
||
|
||
// Broadcast the event to connected clients
|
||
broadcast_event_to_subscriptions(relay_event);
|
||
|
||
// Clean up
|
||
cJSON_Delete(relay_event);
|
||
|
||
// Send success response (plain text like other admin commands)
|
||
char response_content[256];
|
||
const char* kind_name = (kind == 0) ? "metadata" : (kind == 10050) ? "DM relay list" : "relay list";
|
||
snprintf(response_content, sizeof(response_content),
|
||
"✅ Relay event created successfully\n\nKind: %d (%s)\n\nEvent has been stored and broadcast to subscribers.",
|
||
kind, kind_name);
|
||
|
||
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
||
}
|
||
|
||
// Create a relay metadata event (kind 0)
|
||
cJSON* create_relay_metadata_event(cJSON* metadata) {
|
||
if (!metadata || !cJSON_IsObject(metadata)) {
|
||
DEBUG_ERROR("Invalid metadata object for kind 0 event");
|
||
return NULL;
|
||
}
|
||
|
||
// Get relay keys
|
||
const char* relay_pubkey = get_config_value("relay_pubkey");
|
||
char* relay_privkey_hex = get_relay_private_key();
|
||
if (!relay_pubkey || !relay_privkey_hex) {
|
||
DEBUG_ERROR("Could not get relay keys for metadata event");
|
||
if (relay_privkey_hex) free(relay_privkey_hex);
|
||
return NULL;
|
||
}
|
||
|
||
// Convert relay private key to bytes
|
||
unsigned char relay_privkey[32];
|
||
if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) {
|
||
free(relay_privkey_hex);
|
||
DEBUG_ERROR("Failed to convert relay private key for metadata event");
|
||
return NULL;
|
||
}
|
||
free(relay_privkey_hex);
|
||
|
||
// Create metadata content
|
||
char* content = cJSON_Print(metadata);
|
||
if (!content) {
|
||
DEBUG_ERROR("Failed to serialize metadata for kind 0 event");
|
||
return NULL;
|
||
}
|
||
|
||
// Create and sign the event
|
||
cJSON* signed_event = nostr_create_and_sign_event(
|
||
0, // kind (metadata)
|
||
content, // content
|
||
NULL, // tags (none for kind 0)
|
||
relay_privkey, // private key
|
||
(time_t)time(NULL) // timestamp
|
||
);
|
||
|
||
free(content);
|
||
|
||
if (!signed_event) {
|
||
DEBUG_ERROR("Failed to create and sign metadata event");
|
||
return NULL;
|
||
}
|
||
|
||
DEBUG_LOG("Created relay metadata event (kind 0)");
|
||
return signed_event;
|
||
}
|
||
|
||
// Create a relay DM list event (kind 10050)
|
||
cJSON* create_relay_dm_list_event(cJSON* dm_relays) {
|
||
if (!dm_relays || !cJSON_IsObject(dm_relays)) {
|
||
DEBUG_ERROR("Invalid DM relays object for kind 10050 event");
|
||
return NULL;
|
||
}
|
||
|
||
// Get relay keys
|
||
const char* relay_pubkey = get_config_value("relay_pubkey");
|
||
char* relay_privkey_hex = get_relay_private_key();
|
||
if (!relay_pubkey || !relay_privkey_hex) {
|
||
DEBUG_ERROR("Could not get relay keys for DM list event");
|
||
if (relay_privkey_hex) free(relay_privkey_hex);
|
||
return NULL;
|
||
}
|
||
|
||
// Convert relay private key to bytes
|
||
unsigned char relay_privkey[32];
|
||
if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) {
|
||
free(relay_privkey_hex);
|
||
DEBUG_ERROR("Failed to convert relay private key for DM list event");
|
||
return NULL;
|
||
}
|
||
free(relay_privkey_hex);
|
||
|
||
// Create empty content for kind 10050
|
||
const char* content = "";
|
||
|
||
// Create tags from relay list
|
||
cJSON* tags = cJSON_CreateArray();
|
||
if (!tags) {
|
||
DEBUG_ERROR("Failed to create tags array for DM list event");
|
||
return NULL;
|
||
}
|
||
|
||
// Extract relays array
|
||
cJSON* relays_array = cJSON_GetObjectItemCaseSensitive(dm_relays, "relays");
|
||
if (relays_array && cJSON_IsArray(relays_array)) {
|
||
cJSON* relay_item = NULL;
|
||
cJSON_ArrayForEach(relay_item, relays_array) {
|
||
if (cJSON_IsString(relay_item)) {
|
||
const char* relay_url = cJSON_GetStringValue(relay_item);
|
||
if (relay_url && strlen(relay_url) > 0) {
|
||
cJSON* tag = cJSON_CreateArray();
|
||
cJSON_AddItemToArray(tag, cJSON_CreateString("relay"));
|
||
cJSON_AddItemToArray(tag, cJSON_CreateString(relay_url));
|
||
cJSON_AddItemToArray(tags, tag);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Create and sign the event
|
||
cJSON* signed_event = nostr_create_and_sign_event(
|
||
10050, // kind (DM relay list)
|
||
content, // content (empty)
|
||
tags, // tags
|
||
relay_privkey, // private key
|
||
(time_t)time(NULL) // timestamp
|
||
);
|
||
|
||
cJSON_Delete(tags);
|
||
|
||
if (!signed_event) {
|
||
DEBUG_ERROR("Failed to create and sign DM list event");
|
||
return NULL;
|
||
}
|
||
|
||
DEBUG_LOG("Created relay DM list event (kind 10050)");
|
||
return signed_event;
|
||
}
|
||
|
||
// Create a relay list event (kind 10002)
|
||
cJSON* create_relay_list_event(cJSON* relays) {
|
||
if (!relays || !cJSON_IsObject(relays)) {
|
||
DEBUG_ERROR("Invalid relays object for kind 10002 event");
|
||
return NULL;
|
||
}
|
||
|
||
// Get relay keys
|
||
const char* relay_pubkey = get_config_value("relay_pubkey");
|
||
char* relay_privkey_hex = get_relay_private_key();
|
||
if (!relay_pubkey || !relay_privkey_hex) {
|
||
DEBUG_ERROR("Could not get relay keys for relay list event");
|
||
if (relay_privkey_hex) free(relay_privkey_hex);
|
||
return NULL;
|
||
}
|
||
|
||
// Convert relay private key to bytes
|
||
unsigned char relay_privkey[32];
|
||
if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) {
|
||
free(relay_privkey_hex);
|
||
DEBUG_ERROR("Failed to convert relay private key for relay list event");
|
||
return NULL;
|
||
}
|
||
free(relay_privkey_hex);
|
||
|
||
// Create empty content for kind 10002
|
||
const char* content = "";
|
||
|
||
// Create tags from relay list
|
||
cJSON* tags = cJSON_CreateArray();
|
||
if (!tags) {
|
||
DEBUG_ERROR("Failed to create tags array for relay list event");
|
||
return NULL;
|
||
}
|
||
|
||
// Extract relays array
|
||
cJSON* relays_array = cJSON_GetObjectItemCaseSensitive(relays, "relays");
|
||
if (relays_array && cJSON_IsArray(relays_array)) {
|
||
cJSON* relay_item = NULL;
|
||
cJSON_ArrayForEach(relay_item, relays_array) {
|
||
if (cJSON_IsObject(relay_item)) {
|
||
cJSON* url = cJSON_GetObjectItemCaseSensitive(relay_item, "url");
|
||
cJSON* read = cJSON_GetObjectItemCaseSensitive(relay_item, "read");
|
||
cJSON* write = cJSON_GetObjectItemCaseSensitive(relay_item, "write");
|
||
|
||
if (url && cJSON_IsString(url)) {
|
||
const char* relay_url = cJSON_GetStringValue(url);
|
||
int read_flag = read && cJSON_IsBool(read) ? cJSON_IsTrue(read) : true;
|
||
int write_flag = write && cJSON_IsBool(write) ? cJSON_IsTrue(write) : true;
|
||
|
||
// Create marker string
|
||
const char* marker = NULL;
|
||
if (read_flag && write_flag) {
|
||
marker = ""; // No marker means both read and write
|
||
} else if (read_flag) {
|
||
marker = "read";
|
||
} else if (write_flag) {
|
||
marker = "write";
|
||
} else {
|
||
// Skip invalid entries
|
||
continue;
|
||
}
|
||
|
||
cJSON* tag = cJSON_CreateArray();
|
||
cJSON_AddItemToArray(tag, cJSON_CreateString("r"));
|
||
cJSON_AddItemToArray(tag, cJSON_CreateString(relay_url));
|
||
if (marker[0] != '\0') {
|
||
cJSON_AddItemToArray(tag, cJSON_CreateString(marker));
|
||
}
|
||
cJSON_AddItemToArray(tags, tag);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Create and sign the event
|
||
cJSON* signed_event = nostr_create_and_sign_event(
|
||
10002, // kind (relay list)
|
||
content, // content (empty)
|
||
tags, // tags
|
||
relay_privkey, // private key
|
||
(time_t)time(NULL) // timestamp
|
||
);
|
||
|
||
cJSON_Delete(tags);
|
||
|
||
if (!signed_event) {
|
||
DEBUG_ERROR("Failed to create and sign relay list event");
|
||
return NULL;
|
||
}
|
||
|
||
DEBUG_LOG("Created relay list event (kind 10002)");
|
||
return signed_event;
|
||
}
|
||
|
||
// Handle monitoring system admin commands
|
||
int handle_monitoring_command(cJSON* event, const char* command, char* error_message, size_t error_size, struct lws* wsi) {
|
||
if (!event || !command || !error_message) {
|
||
return -1;
|
||
}
|
||
|
||
// Get request event ID for response correlation
|
||
cJSON* request_id_obj = cJSON_GetObjectItemCaseSensitive(event, "id");
|
||
if (!request_id_obj || !cJSON_IsString(request_id_obj)) {
|
||
snprintf(error_message, error_size, "Missing request event ID");
|
||
return -1;
|
||
}
|
||
const char* request_id = cJSON_GetStringValue(request_id_obj);
|
||
|
||
// Get sender pubkey for response
|
||
cJSON* sender_pubkey_obj = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
|
||
if (!sender_pubkey_obj || !cJSON_IsString(sender_pubkey_obj)) {
|
||
snprintf(error_message, error_size, "Missing sender pubkey");
|
||
return -1;
|
||
}
|
||
const char* sender_pubkey = cJSON_GetStringValue(sender_pubkey_obj);
|
||
|
||
// Parse command
|
||
char cmd[256];
|
||
char arg[256];
|
||
cmd[0] = '\0';
|
||
arg[0] = '\0';
|
||
|
||
// Simple command parsing - split on space
|
||
const char* space_pos = strchr(command, ' ');
|
||
if (space_pos) {
|
||
size_t cmd_len = space_pos - command;
|
||
if (cmd_len < sizeof(cmd)) {
|
||
memcpy(cmd, command, cmd_len);
|
||
cmd[cmd_len] = '\0';
|
||
strcpy(arg, space_pos + 1);
|
||
}
|
||
} else {
|
||
strcpy(cmd, command);
|
||
}
|
||
|
||
// Convert to lowercase for case-insensitive matching
|
||
for (char* p = cmd; *p; p++) {
|
||
if (*p >= 'A' && *p <= 'Z') *p = *p + 32;
|
||
}
|
||
|
||
// Handle set_monitoring_throttle command (only remaining monitoring command)
|
||
if (strcmp(cmd, "set_monitoring_throttle") == 0) {
|
||
if (arg[0] == '\0') {
|
||
char* response_content = "❌ Missing throttle value\n\nUsage: set_monitoring_throttle <seconds>";
|
||
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
||
}
|
||
|
||
char* endptr;
|
||
long throttle_seconds = strtol(arg, &endptr, 10);
|
||
if (*endptr != '\0' || throttle_seconds < 0 || throttle_seconds > 3600) {
|
||
char* response_content = "❌ Invalid throttle value\n\nThrottle must be between 0 and 3600 seconds (0 = disabled).";
|
||
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
||
}
|
||
|
||
char throttle_str[16];
|
||
snprintf(throttle_str, sizeof(throttle_str), "%ld", throttle_seconds);
|
||
|
||
if (update_config_in_table("kind_24567_reporting_throttle_sec", throttle_str) == 0) {
|
||
char response_content[256];
|
||
snprintf(response_content, sizeof(response_content),
|
||
"✅ Monitoring throttle updated\n\n"
|
||
"Minimum interval between monitoring events: %ld seconds\n\n"
|
||
"ℹ️ Monitoring activates automatically when you subscribe to kind 24567 events.",
|
||
throttle_seconds);
|
||
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
||
} else {
|
||
char* response_content = "❌ Failed to update monitoring throttle";
|
||
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
||
}
|
||
} else {
|
||
char response_content[1024];
|
||
snprintf(response_content, sizeof(response_content),
|
||
"❌ Unknown monitoring command: %s\n\n"
|
||
"Available command:\n"
|
||
"• set_monitoring_throttle <seconds>\n\n"
|
||
"ℹ️ Monitoring is now subscription-based:\n"
|
||
"Subscribe to kind 24567 events to receive real-time monitoring data.\n"
|
||
"Monitoring automatically activates when subscriptions exist and deactivates when they close.",
|
||
cmd);
|
||
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
|
||
}
|
||
} |