/* * Ginxsom Relay Client Implementation * * Manages connections to Nostr relays, publishes events, and subscribes to admin commands. */ #include "relay_client.h" #include "admin_commands.h" #include "../nostr_core_lib/nostr_core/nostr_core.h" #include #include #include #include #include #include #include // Forward declare app_log to avoid including ginxsom.h (which has typedef conflicts) typedef enum { LOG_DEBUG = 0, LOG_INFO = 1, LOG_WARN = 2, LOG_ERROR = 3 } log_level_t; void app_log(log_level_t level, const char* format, ...); // Maximum number of relays to connect to #define MAX_RELAYS 10 // Reconnection settings #define RECONNECT_DELAY_SECONDS 30 #define MAX_RECONNECT_ATTEMPTS 5 // Global state static struct { int enabled; int initialized; int running; char db_path[512]; nostr_relay_pool_t* pool; char** relay_urls; int relay_count; nostr_pool_subscription_t* admin_subscription; pthread_t management_thread; pthread_mutex_t state_mutex; } g_relay_state = {0}; // External globals from main.c extern char g_blossom_seckey[65]; extern char g_blossom_pubkey[65]; extern char g_admin_pubkey[65]; // Forward declarations static void *relay_management_thread(void *arg); static int load_config_from_db(void); static int parse_relay_urls(const char *json_array); static int subscribe_to_admin_commands(void); static void on_publish_response(const char* relay_url, const char* event_id, int success, const char* message, void* user_data); static void on_admin_command_event(cJSON* event, const char* relay_url, void* user_data); static void on_admin_subscription_eose(cJSON** events, int event_count, void* user_data); // Initialize relay client system int relay_client_init(const char *db_path) { if (g_relay_state.initialized) { app_log(LOG_WARN, "Relay client already initialized"); return 0; } app_log(LOG_INFO, "Initializing relay client system..."); // Store database path strncpy(g_relay_state.db_path, db_path, sizeof(g_relay_state.db_path) - 1); // Initialize mutex if (pthread_mutex_init(&g_relay_state.state_mutex, NULL) != 0) { app_log(LOG_ERROR, "Failed to initialize relay state mutex"); return -1; } // Load configuration from database if (load_config_from_db() != 0) { app_log(LOG_ERROR, "Failed to load relay configuration from database"); pthread_mutex_destroy(&g_relay_state.state_mutex); return -1; } // Create relay pool if enabled if (g_relay_state.enabled) { // Use default reconnection config (don't free - it's a static structure) nostr_pool_reconnect_config_t* config = nostr_pool_reconnect_config_default(); g_relay_state.pool = nostr_relay_pool_create(config); if (!g_relay_state.pool) { app_log(LOG_ERROR, "Failed to create relay pool"); pthread_mutex_destroy(&g_relay_state.state_mutex); return -1; } // Add all relays to pool for (int i = 0; i < g_relay_state.relay_count; i++) { if (nostr_relay_pool_add_relay(g_relay_state.pool, g_relay_state.relay_urls[i]) != NOSTR_SUCCESS) { app_log(LOG_WARN, "Failed to add relay to pool: %s", g_relay_state.relay_urls[i]); } } // Trigger initial connection attempts by creating a dummy subscription // This forces ensure_relay_connection() to be called for each relay app_log(LOG_INFO, "Initiating relay connections..."); cJSON* dummy_filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); cJSON_AddItemToArray(kinds, cJSON_CreateNumber(0)); // Kind 0 (will match nothing) cJSON_AddItemToObject(dummy_filter, "kinds", kinds); cJSON_AddNumberToObject(dummy_filter, "limit", 0); // Limit 0 = no results nostr_pool_subscription_t* dummy_sub = nostr_relay_pool_subscribe( g_relay_state.pool, (const char**)g_relay_state.relay_urls, g_relay_state.relay_count, dummy_filter, NULL, // No event callback NULL, // No EOSE callback NULL, // No user data 1, // close_on_eose 1, // enable_deduplication NOSTR_POOL_EOSE_FIRST, // result_mode 30, // relay_timeout_seconds 30 // eose_timeout_seconds ); cJSON_Delete(dummy_filter); // Immediately close the dummy subscription if (dummy_sub) { nostr_pool_subscription_close(dummy_sub); app_log(LOG_INFO, "Connection attempts initiated for %d relays", g_relay_state.relay_count); } else { app_log(LOG_WARN, "Failed to initiate connection attempts"); } } g_relay_state.initialized = 1; app_log(LOG_INFO, "Relay client initialized (enabled: %d, relays: %d)", g_relay_state.enabled, g_relay_state.relay_count); return 0; } // Load configuration from database static int load_config_from_db(void) { sqlite3 *db; sqlite3_stmt *stmt; int rc; rc = sqlite3_open_v2(g_relay_state.db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "Cannot open database: %s", sqlite3_errmsg(db)); return -1; } // Load enable_relay_connect const char *sql = "SELECT value FROM config WHERE key = ?"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "Failed to prepare statement: %s", sqlite3_errmsg(db)); sqlite3_close(db); return -1; } sqlite3_bind_text(stmt, 1, "enable_relay_connect", -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { const char *value = (const char *)sqlite3_column_text(stmt, 0); g_relay_state.enabled = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0); } else { g_relay_state.enabled = 0; } sqlite3_finalize(stmt); // If not enabled, skip loading relay URLs if (!g_relay_state.enabled) { sqlite3_close(db); return 0; } // Load kind_10002_tags (relay URLs) rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "Failed to prepare statement: %s", sqlite3_errmsg(db)); sqlite3_close(db); return -1; } sqlite3_bind_text(stmt, 1, "kind_10002_tags", -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { const char *json_array = (const char *)sqlite3_column_text(stmt, 0); if (parse_relay_urls(json_array) != 0) { app_log(LOG_ERROR, "Failed to parse relay URLs from config"); sqlite3_finalize(stmt); sqlite3_close(db); return -1; } } else { app_log(LOG_WARN, "No relay URLs configured in kind_10002_tags"); } sqlite3_finalize(stmt); sqlite3_close(db); return 0; } // Parse relay URLs from JSON array static int parse_relay_urls(const char *json_array) { cJSON *root = cJSON_Parse(json_array); if (!root || !cJSON_IsArray(root)) { app_log(LOG_ERROR, "Invalid JSON array for relay URLs"); if (root) cJSON_Delete(root); return -1; } int count = cJSON_GetArraySize(root); if (count > MAX_RELAYS) { app_log(LOG_WARN, "Too many relays configured (%d), limiting to %d", count, MAX_RELAYS); count = MAX_RELAYS; } // Allocate relay URLs array g_relay_state.relay_urls = malloc(count * sizeof(char*)); if (!g_relay_state.relay_urls) { cJSON_Delete(root); return -1; } g_relay_state.relay_count = 0; for (int i = 0; i < count; i++) { cJSON *item = cJSON_GetArrayItem(root, i); if (cJSON_IsString(item) && item->valuestring) { g_relay_state.relay_urls[g_relay_state.relay_count] = strdup(item->valuestring); if (!g_relay_state.relay_urls[g_relay_state.relay_count]) { // Cleanup on failure for (int j = 0; j < g_relay_state.relay_count; j++) { free(g_relay_state.relay_urls[j]); } free(g_relay_state.relay_urls); cJSON_Delete(root); return -1; } g_relay_state.relay_count++; } } cJSON_Delete(root); app_log(LOG_INFO, "Parsed %d relay URLs from configuration", g_relay_state.relay_count); return 0; } // Start relay connections int relay_client_start(void) { if (!g_relay_state.initialized) { app_log(LOG_ERROR, "Relay client not initialized"); return -1; } if (!g_relay_state.enabled) { app_log(LOG_INFO, "Relay client disabled in configuration"); return 0; } if (g_relay_state.running) { app_log(LOG_WARN, "Relay client already running"); return 0; } app_log(LOG_INFO, "Starting relay client..."); // Start management thread g_relay_state.running = 1; if (pthread_create(&g_relay_state.management_thread, NULL, relay_management_thread, NULL) != 0) { app_log(LOG_ERROR, "Failed to create relay management thread"); g_relay_state.running = 0; return -1; } app_log(LOG_INFO, "Relay client started successfully"); return 0; } // Relay management thread static void *relay_management_thread(void *arg) { (void)arg; app_log(LOG_INFO, "Relay management thread started"); // Wait for at least one relay to connect (max 30 seconds) int connected = 0; for (int i = 0; i < 30 && !connected; i++) { sleep(1); // Poll to process connection attempts nostr_relay_pool_poll(g_relay_state.pool, 100); // Check if any relay is connected for (int j = 0; j < g_relay_state.relay_count; j++) { nostr_pool_relay_status_t status = nostr_relay_pool_get_relay_status( g_relay_state.pool, g_relay_state.relay_urls[j] ); if (status == NOSTR_POOL_RELAY_CONNECTED) { connected = 1; app_log(LOG_INFO, "Relay connected: %s", g_relay_state.relay_urls[j]); break; } } } if (!connected) { app_log(LOG_WARN, "No relays connected after 30 seconds, continuing anyway"); } // Publish initial events relay_client_publish_kind0(); relay_client_publish_kind10002(); // Subscribe to admin commands subscribe_to_admin_commands(); // Main loop: poll the relay pool for incoming messages while (g_relay_state.running) { // Poll with 1000ms timeout int events_processed = nostr_relay_pool_poll(g_relay_state.pool, 1000); if (events_processed < 0) { app_log(LOG_ERROR, "Error polling relay pool"); sleep(1); } // Pool handles all connection management, reconnection, and message processing } app_log(LOG_INFO, "Relay management thread stopping"); return NULL; } // Stop relay connections void relay_client_stop(void) { if (!g_relay_state.running) { return; } app_log(LOG_INFO, "Stopping relay client..."); g_relay_state.running = 0; // Wait for management thread to finish pthread_join(g_relay_state.management_thread, NULL); // Close admin subscription if (g_relay_state.admin_subscription) { nostr_pool_subscription_close(g_relay_state.admin_subscription); g_relay_state.admin_subscription = NULL; } // Destroy relay pool (automatically disconnects all relays) if (g_relay_state.pool) { nostr_relay_pool_destroy(g_relay_state.pool); g_relay_state.pool = NULL; } // Free relay URLs if (g_relay_state.relay_urls) { for (int i = 0; i < g_relay_state.relay_count; i++) { free(g_relay_state.relay_urls[i]); } free(g_relay_state.relay_urls); g_relay_state.relay_urls = NULL; } pthread_mutex_destroy(&g_relay_state.state_mutex); app_log(LOG_INFO, "Relay client stopped"); } // Check if relay client is enabled int relay_client_is_enabled(void) { return g_relay_state.enabled; } // Publish Kind 0 profile event int relay_client_publish_kind0(void) { if (!g_relay_state.enabled || !g_relay_state.running || !g_relay_state.pool) { return -1; } app_log(LOG_INFO, "Publishing Kind 0 profile event..."); // Load kind_0_content from database sqlite3 *db; sqlite3_stmt *stmt; int rc; rc = sqlite3_open_v2(g_relay_state.db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "Cannot open database: %s", sqlite3_errmsg(db)); return -1; } const char *sql = "SELECT value FROM config WHERE key = 'kind_0_content'"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "Failed to prepare statement: %s", sqlite3_errmsg(db)); sqlite3_close(db); return -1; } rc = sqlite3_step(stmt); if (rc != SQLITE_ROW) { app_log(LOG_WARN, "No kind_0_content found in config"); sqlite3_finalize(stmt); sqlite3_close(db); return -1; } const char *content = (const char *)sqlite3_column_text(stmt, 0); // Convert private key from hex to bytes unsigned char privkey_bytes[32]; if (nostr_hex_to_bytes(g_blossom_seckey, privkey_bytes, 32) != 0) { app_log(LOG_ERROR, "Failed to convert private key from hex"); sqlite3_finalize(stmt); sqlite3_close(db); return -1; } // Create and sign Kind 0 event using nostr_core_lib cJSON* event = nostr_create_and_sign_event( 0, // kind content, // content NULL, // tags (empty for Kind 0) privkey_bytes, // private key time(NULL) // created_at ); sqlite3_finalize(stmt); sqlite3_close(db); if (!event) { app_log(LOG_ERROR, "Failed to create Kind 0 event"); return -1; } // Publish to all relays using async pool API int result = nostr_relay_pool_publish_async( g_relay_state.pool, (const char**)g_relay_state.relay_urls, g_relay_state.relay_count, event, on_publish_response, (void*)"Kind 0" // user_data to identify event type ); cJSON_Delete(event); if (result == 0) { app_log(LOG_INFO, "Kind 0 profile event publish initiated"); return 0; } else { app_log(LOG_ERROR, "Failed to initiate Kind 0 profile event publish"); return -1; } } // Publish Kind 10002 relay list event int relay_client_publish_kind10002(void) { if (!g_relay_state.enabled || !g_relay_state.running || !g_relay_state.pool) { return -1; } app_log(LOG_INFO, "Publishing Kind 10002 relay list event..."); // Build tags array from configured relays cJSON* tags = cJSON_CreateArray(); for (int i = 0; i < g_relay_state.relay_count; i++) { cJSON* tag = cJSON_CreateArray(); cJSON_AddItemToArray(tag, cJSON_CreateString("r")); cJSON_AddItemToArray(tag, cJSON_CreateString(g_relay_state.relay_urls[i])); cJSON_AddItemToArray(tags, tag); } // Convert private key from hex to bytes unsigned char privkey_bytes[32]; if (nostr_hex_to_bytes(g_blossom_seckey, privkey_bytes, 32) != 0) { app_log(LOG_ERROR, "Failed to convert private key from hex"); cJSON_Delete(tags); return -1; } // Create and sign Kind 10002 event cJSON* event = nostr_create_and_sign_event( 10002, // kind "", // content (empty for Kind 10002) tags, // tags privkey_bytes, // private key time(NULL) // created_at ); cJSON_Delete(tags); if (!event) { app_log(LOG_ERROR, "Failed to create Kind 10002 event"); return -1; } // Publish to all relays using async pool API int result = nostr_relay_pool_publish_async( g_relay_state.pool, (const char**)g_relay_state.relay_urls, g_relay_state.relay_count, event, on_publish_response, (void*)"Kind 10002" // user_data to identify event type ); cJSON_Delete(event); if (result == 0) { app_log(LOG_INFO, "Kind 10002 relay list event publish initiated"); return 0; } else { app_log(LOG_ERROR, "Failed to initiate Kind 10002 relay list event publish"); return -1; } } // Send Kind 23459 admin response event int relay_client_send_admin_response(const char *recipient_pubkey, const char *response_content) { if (!g_relay_state.enabled || !g_relay_state.running || !g_relay_state.pool) { return -1; } if (!recipient_pubkey || !response_content) { return -1; } app_log(LOG_INFO, "Sending Kind 23459 admin response to %s", recipient_pubkey); // TODO: Encrypt response_content using NIP-44 // For now, use plaintext (stub implementation) const char *encrypted_content = response_content; // Build tags array cJSON* tags = cJSON_CreateArray(); cJSON* p_tag = cJSON_CreateArray(); cJSON_AddItemToArray(p_tag, cJSON_CreateString("p")); cJSON_AddItemToArray(p_tag, cJSON_CreateString(recipient_pubkey)); cJSON_AddItemToArray(tags, p_tag); // Convert private key from hex to bytes unsigned char privkey_bytes[32]; if (nostr_hex_to_bytes(g_blossom_seckey, privkey_bytes, 32) != 0) { app_log(LOG_ERROR, "Failed to convert private key from hex"); cJSON_Delete(tags); return -1; } // Create and sign Kind 23459 event cJSON* event = nostr_create_and_sign_event( 23459, // kind encrypted_content, // content tags, // tags privkey_bytes, // private key time(NULL) // created_at ); cJSON_Delete(tags); if (!event) { app_log(LOG_ERROR, "Failed to create Kind 23459 event"); return -1; } // Publish to all relays using async pool API int result = nostr_relay_pool_publish_async( g_relay_state.pool, (const char**)g_relay_state.relay_urls, g_relay_state.relay_count, event, on_publish_response, (void*)"Kind 23459" // user_data to identify event type ); cJSON_Delete(event); if (result == 0) { app_log(LOG_INFO, "Kind 23459 admin response publish initiated"); return 0; } else { app_log(LOG_ERROR, "Failed to initiate Kind 23459 admin response publish"); return -1; } } // Callback for publish responses static void on_publish_response(const char* relay_url, const char* event_id, int success, const char* message, void* user_data) { const char* event_type = (const char*)user_data; if (success) { app_log(LOG_INFO, "%s event published successfully to %s (ID: %s)", event_type, relay_url, event_id); } else { app_log(LOG_WARN, "%s event rejected by %s: %s", event_type, relay_url, message ? message : "unknown error"); } } // Callback for received Kind 23458 admin command events static void on_admin_command_event(cJSON* event, const char* relay_url, void* user_data) { (void)user_data; app_log(LOG_INFO, "Received Kind 23458 admin command from relay: %s", relay_url); // Extract event fields cJSON* kind_json = cJSON_GetObjectItem(event, "kind"); cJSON* pubkey_json = cJSON_GetObjectItem(event, "pubkey"); cJSON* content_json = cJSON_GetObjectItem(event, "content"); cJSON* id_json = cJSON_GetObjectItem(event, "id"); if (!kind_json || !pubkey_json || !content_json || !id_json) { app_log(LOG_ERROR, "Invalid event structure"); return; } int kind = cJSON_GetNumberValue(kind_json); const char* sender_pubkey = cJSON_GetStringValue(pubkey_json); const char* encrypted_content = cJSON_GetStringValue(content_json); const char* event_id = cJSON_GetStringValue(id_json); if (kind != 23458) { app_log(LOG_WARN, "Unexpected event kind: %d", kind); return; } // Verify sender is admin if (strcmp(sender_pubkey, g_admin_pubkey) != 0) { app_log(LOG_WARN, "Ignoring command from non-admin pubkey: %s", sender_pubkey); return; } app_log(LOG_INFO, "Processing admin command (event ID: %s)", event_id); // Convert keys from hex to bytes unsigned char server_privkey[32]; unsigned char admin_pubkey_bytes[32]; if (nostr_hex_to_bytes(g_blossom_seckey, server_privkey, 32) != 0) { app_log(LOG_ERROR, "Failed to convert server private key from hex"); return; } if (nostr_hex_to_bytes(sender_pubkey, admin_pubkey_bytes, 32) != 0) { app_log(LOG_ERROR, "Failed to convert admin public key from hex"); return; } // Decrypt command content using NIP-44 char decrypted_command[4096]; if (admin_decrypt_command(server_privkey, admin_pubkey_bytes, encrypted_content, decrypted_command, sizeof(decrypted_command)) != 0) { app_log(LOG_ERROR, "Failed to decrypt admin command"); // Send error response cJSON* error_response = cJSON_CreateObject(); cJSON_AddStringToObject(error_response, "status", "error"); cJSON_AddStringToObject(error_response, "message", "Failed to decrypt command"); char* error_json = cJSON_PrintUnformatted(error_response); cJSON_Delete(error_response); char encrypted_response[4096]; if (admin_encrypt_response(server_privkey, admin_pubkey_bytes, error_json, encrypted_response, sizeof(encrypted_response)) == 0) { relay_client_send_admin_response(sender_pubkey, encrypted_response); } free(error_json); return; } app_log(LOG_DEBUG, "Decrypted command: %s", decrypted_command); // Parse command JSON cJSON* command_json = cJSON_Parse(decrypted_command); if (!command_json) { app_log(LOG_ERROR, "Failed to parse command JSON"); cJSON* error_response = cJSON_CreateObject(); cJSON_AddStringToObject(error_response, "status", "error"); cJSON_AddStringToObject(error_response, "message", "Invalid JSON format"); char* error_json = cJSON_PrintUnformatted(error_response); cJSON_Delete(error_response); char encrypted_response[4096]; if (admin_encrypt_response(server_privkey, admin_pubkey_bytes, error_json, encrypted_response, sizeof(encrypted_response)) == 0) { relay_client_send_admin_response(sender_pubkey, encrypted_response); } free(error_json); return; } // Process command and get response cJSON* response_json = admin_commands_process(command_json, event_id); cJSON_Delete(command_json); if (!response_json) { app_log(LOG_ERROR, "Failed to process admin command"); response_json = cJSON_CreateObject(); cJSON_AddStringToObject(response_json, "status", "error"); cJSON_AddStringToObject(response_json, "message", "Failed to process command"); } // Convert response to JSON string char* response_str = cJSON_PrintUnformatted(response_json); cJSON_Delete(response_json); if (!response_str) { app_log(LOG_ERROR, "Failed to serialize response JSON"); return; } // Encrypt and send response char encrypted_response[4096]; if (admin_encrypt_response(server_privkey, admin_pubkey_bytes, response_str, encrypted_response, sizeof(encrypted_response)) != 0) { app_log(LOG_ERROR, "Failed to encrypt admin response"); free(response_str); return; } free(response_str); if (relay_client_send_admin_response(sender_pubkey, encrypted_response) != 0) { app_log(LOG_ERROR, "Failed to send admin response"); } } // Callback for EOSE (End Of Stored Events) - new signature static void on_admin_subscription_eose(cJSON** events, int event_count, void* user_data) { (void)events; (void)event_count; (void)user_data; app_log(LOG_INFO, "Received EOSE for admin command subscription"); } // Subscribe to admin commands (Kind 23458) static int subscribe_to_admin_commands(void) { if (!g_relay_state.pool) { return -1; } app_log(LOG_INFO, "Subscribing to Kind 23458 admin commands..."); // Create subscription filter for Kind 23458 events addressed to us cJSON* filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); cJSON_AddItemToArray(kinds, cJSON_CreateNumber(23458)); cJSON_AddItemToObject(filter, "kinds", kinds); cJSON* p_tags = cJSON_CreateArray(); cJSON_AddItemToArray(p_tags, cJSON_CreateString(g_blossom_pubkey)); cJSON_AddItemToObject(filter, "#p", p_tags); cJSON_AddNumberToObject(filter, "since", (double)time(NULL)); // Subscribe using pool with new API signature g_relay_state.admin_subscription = nostr_relay_pool_subscribe( g_relay_state.pool, (const char**)g_relay_state.relay_urls, g_relay_state.relay_count, filter, on_admin_command_event, on_admin_subscription_eose, NULL, // user_data 0, // close_on_eose (keep subscription open) 1, // enable_deduplication NOSTR_POOL_EOSE_FULL_SET, // result_mode 30, // relay_timeout_seconds 30 // eose_timeout_seconds ); cJSON_Delete(filter); if (!g_relay_state.admin_subscription) { app_log(LOG_ERROR, "Failed to create admin command subscription"); return -1; } app_log(LOG_INFO, "Successfully subscribed to admin commands"); return 0; } // Get current relay connection status char *relay_client_get_status(void) { if (!g_relay_state.pool) { return strdup("[]"); } cJSON *root = cJSON_CreateArray(); pthread_mutex_lock(&g_relay_state.state_mutex); for (int i = 0; i < g_relay_state.relay_count; i++) { cJSON *relay_obj = cJSON_CreateObject(); cJSON_AddStringToObject(relay_obj, "url", g_relay_state.relay_urls[i]); // Get status from pool nostr_pool_relay_status_t status = nostr_relay_pool_get_relay_status( g_relay_state.pool, g_relay_state.relay_urls[i] ); const char *state_str; switch (status) { case NOSTR_POOL_RELAY_CONNECTED: state_str = "connected"; break; case NOSTR_POOL_RELAY_CONNECTING: state_str = "connecting"; break; case NOSTR_POOL_RELAY_ERROR: state_str = "error"; break; default: state_str = "disconnected"; break; } cJSON_AddStringToObject(relay_obj, "state", state_str); // Get statistics from pool const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats( g_relay_state.pool, g_relay_state.relay_urls[i] ); if (stats) { cJSON_AddNumberToObject(relay_obj, "events_received", stats->events_received); cJSON_AddNumberToObject(relay_obj, "events_published", stats->events_published); cJSON_AddNumberToObject(relay_obj, "connection_attempts", stats->connection_attempts); cJSON_AddNumberToObject(relay_obj, "connection_failures", stats->connection_failures); if (stats->query_latency_avg > 0) { cJSON_AddNumberToObject(relay_obj, "query_latency_ms", stats->query_latency_avg); } } cJSON_AddItemToArray(root, relay_obj); } pthread_mutex_unlock(&g_relay_state.state_mutex); char *json_str = cJSON_PrintUnformatted(root); cJSON_Delete(root); return json_str; } // Force reconnection to all relays int relay_client_reconnect(void) { if (!g_relay_state.enabled || !g_relay_state.running || !g_relay_state.pool) { return -1; } app_log(LOG_INFO, "Forcing reconnection to all relays..."); // Remove and re-add all relays to force reconnection pthread_mutex_lock(&g_relay_state.state_mutex); for (int i = 0; i < g_relay_state.relay_count; i++) { nostr_relay_pool_remove_relay(g_relay_state.pool, g_relay_state.relay_urls[i]); nostr_relay_pool_add_relay(g_relay_state.pool, g_relay_state.relay_urls[i]); } pthread_mutex_unlock(&g_relay_state.state_mutex); app_log(LOG_INFO, "Reconnection initiated for all relays"); return 0; }