// 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 #include #include #include #include #include int get_active_connection_count(void); #include #include #include #include #include #include #include #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 "; 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 \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); } }