Files
ginxsom/docs/WEBSOCKET_IMPLEMENTATION.md

57 KiB

We have mostly implemented being able to handle websocket interactions in ginxsome, but I want to rethink why we are doing this, and then change the code.

Why do I want Ginxsom to handle websockets?

Ginxsome should have a npub, and you should be able to be able to interact with it as if it is a person on nostr, i.e. a regular nostr user. So it should subscribe to relays, and it should be able to read events that are sent to it - such as commands sent from the administrator, and it should be able to post events, such as it's current status, kind 0, etc.

So for this use, we don't need ginxsome to be a websocket server, but only a websocket client.

So possibly, implementing libwebsockets was too much. For this use, we probably can just use nostr_core_lib and the websocket client implementation that is already in nostr_core_lib.

So review what we currently have implemented, and give suggestions.

I want ginxsome to have within it's config db table, the values "enable_relay_connect", kind_0_content, kind_10002_tags, .

Upon startup, if enable_relay_connect is true, ginxsome should establish and maintain a connection to those relays listed in "kind_10002_tags".

Upon startup after connecting to the relays, it should publish a signed kind 0 to the relays is subscribes to, using the content from the database value kind_0_content, and it should publish a kind 10002 also using the data that is in the database.

It should also subscribe to any kind 23456 events published by the administrator and addressed to the blossom_pubkey.

--- AGENT IMPLEMENTATION ---

Implementation Plan

Phase 1: Update nostr_core_lib Submodule

Status: COMPLETED

Actions Taken:

  1. Removed outdated nostr_core_lib submodule that only had synchronous API
  2. Re-added submodule from ssh://git@git.laantungir.net:2222/laantungir/nostr_core_lib.git
  3. Rebuilt library with all NIPs using ./build.sh --nips=all
  4. Verified new async API is available: nostr_relay_pool_publish_async() at line 301

Key Changes:

  • Old API: nostr_relay_pool_publish() (synchronous, blocking)
  • New API: nostr_relay_pool_publish_async() (async with callbacks)
  • Subscription API now requires 12 parameters including EOSE result mode

Phase 2: Database Configuration Schema

Status: COMPLETED

Database Table: config

  • enable_relay_connect (boolean) - Enable/disable relay client functionality
  • kind_0_content (JSON string) - Profile metadata for Kind 0 event
  • kind_10002_tags (JSON array) - List of relay URLs for Kind 10002 event

Example Configuration:

INSERT INTO config (key, value) VALUES
  ('enable_relay_connect', 'true'),
  ('kind_0_content', '{"name":"Ginxsom Server","about":"Blossom media server"}'),
  ('kind_10002_tags', '["wss://relay.laantungir.net","wss://relay.damus.io"]');

Phase 3: Relay Client Module Implementation

Status: COMPLETED

File: src/relay_client.c

Core Functions:

  1. relay_client_init() - Initialize relay pool and load config from database
  2. relay_client_start() - Start management thread for relay operations
  3. relay_client_publish_kind0() - Publish profile event using async API
  4. relay_client_publish_kind10002() - Publish relay list using async API
  5. relay_client_send_admin_response() - Send Kind 23457 responses
  6. on_admin_command_event() - Callback for received Kind 23456 commands
  7. on_publish_response() - Callback for async publish results

Key Implementation Details:

  • Uses nostr_relay_pool_t from nostr_core_lib for connection management
  • Async publish with nostr_relay_pool_publish_async() and callbacks
  • Subscription with updated 12-parameter signature
  • Management thread calls nostr_relay_pool_poll() to drive event loop
  • Automatic reconnection handled by pool's reconnect config

Async API Usage:

// Create pool with reconnection config
nostr_pool_reconnect_config_t* config = nostr_pool_reconnect_config_default();
pool = nostr_relay_pool_create(config);

// Async publish with callback
nostr_relay_pool_publish_async(
    pool,
    relay_urls,
    relay_count,
    event,
    on_publish_response,  // Callback for results
    user_data
);

// Subscribe with full parameter set
nostr_relay_pool_subscribe(
    pool,
    relay_urls,
    relay_count,
    filter,
    on_event_callback,
    on_eose_callback,
    user_data,
    close_on_eose,
    enable_deduplication,
    NOSTR_POOL_EOSE_FULL_SET,  // result_mode
    relay_timeout_seconds,
    eose_timeout_seconds
);

Phase 4: Main Program Integration

Status: COMPLETED

File: src/main.c

Integration Points:

  1. Added #include "relay_client.h"
  2. Call relay_client_init(db_path) after validator initialization
  3. Call relay_client_start() to begin relay connections
  4. Proper error handling and logging throughout

Startup Sequence:

1. Initialize database
2. Initialize request validator
3. Initialize relay client (loads config)
4. Start relay client (spawns management thread)
5. Begin FastCGI request processing

Phase 5: Build System Updates

Status: COMPLETED

Makefile Changes:

  • Added src/relay_client.c to source files
  • Added nostr_core_lib/nostr_core/core_relay_pool.c compilation
  • Updated include paths for nostr_core headers
  • Linked with updated libnostr_core_x64.a (352KB with all NIPs)

Compilation Command:

make clean && make

Phase 6: Testing Plan 🔄

Status: PENDING

Test Cases:

  1. Verify compilation with new async API
  2. Test relay connection to wss://relay.laantungir.net
  3. Verify Kind 0 profile event publishing
  4. Verify Kind 10002 relay list publishing
  5. Test Kind 23456 admin command subscription
  6. Test Kind 23457 admin response sending
  7. Verify automatic reconnection on disconnect
  8. Test with multiple relays simultaneously

Testing Commands:

# Start server
./restart-all.sh

# Check logs for relay activity
tail -f logs/app/app.log | grep -i relay

# Monitor relay connections
# (Check for "Relay connected" messages)

Technical Notes

Callback Pattern: The new async API uses callbacks for all operations:

  • on_publish_response() - Called when relay accepts/rejects event
  • on_admin_command_event() - Called when Kind 23456 received
  • on_admin_subscription_eose() - Called when EOSE received

Event Loop: The management thread continuously calls nostr_relay_pool_poll(pool, 1000) which:

  • Processes incoming WebSocket messages
  • Triggers callbacks for events and responses
  • Handles connection state changes
  • Manages automatic reconnection

Memory Management:

  • Pool handles all WebSocket connection memory
  • Events created with nostr_create_and_sign_event() must be freed with cJSON_Delete()
  • Subscription filters must be freed after subscription creation

Future Enhancements

  1. NIP-44 Encryption: Encrypt Kind 23456/23457 messages
  2. Command Processing: Implement actual command execution logic
  3. Status Monitoring: Add /admin/relay-status endpoint
  4. Dynamic Configuration: Allow runtime relay list updates
  5. Metrics Collection: Track relay performance and uptime

References

  • Nostr Core Lib: nostr_core_lib/nostr_core/nostr_core.h
  • Relay Pool API: Lines 189-335 in nostr_core.h
  • NIP-01: Basic protocol and event structure
  • NIP-65: Relay list metadata (Kind 10002)
  • Custom Kinds: 23456 (admin commands), 23457 (admin responses)

Implementation Summary

Successfully implemented Nostr relay client functionality in Ginxsom using nostr_relay_pool_t from nostr_core_lib. The implementation allows Ginxsom to act as a Nostr client, connecting to relays, publishing events, and subscribing to admin commands.

Phase 1: Database Schema

Added three new configuration fields to the config table:

  • enable_relay_connect (INTEGER) - Enable/disable relay connections
  • kind_0_content (TEXT) - JSON content for Kind 0 (profile metadata) events
  • kind_10002_tags (TEXT) - JSON array of relay URLs for Kind 10002 (relay list) events

Phase 2: Core Module Structure

Created src/relay_client.c and src/relay_client.h implementing:

  • Initialization and cleanup functions
  • Configuration loading from database
  • Thread-safe state management
  • Integration with main.c

Phase 3: Relay Pool Integration

Replaced custom WebSocket management with nostr_relay_pool_t:

Phase 4: Event Publishing

Implemented proper Nostr event creation and publishing:

Phase 5: Admin Command Subscription

Implemented subscription to Kind 23456 admin commands:

Phase 6: Management Thread

Simplified relay management using pool polling:

  • relay_management_thread() - Main event loop
  • Calls nostr_relay_pool_poll() to process incoming messages
  • Pool handles all WebSocket I/O, reconnection, and message parsing
  • Thread-safe state management with mutex

Phase 7: Status and Monitoring

Implemented comprehensive status reporting:

  • relay_client_get_status() - Returns JSON status for all relays
  • Includes connection state, statistics, and latency measurements
  • Exposes pool statistics: events received/published, query/publish latency
  • Used by admin API for monitoring

Key Implementation Details

Startup Sequence:

  1. relay_client_init() - Initialize system, load config from database
  2. If enable_relay_connect is true:
    • Create relay pool with nostr_relay_pool_create()
    • Add relays from kind_10002_tags using nostr_relay_pool_add_relay()
    • Start management thread
  3. Management thread connects to relays automatically
  4. Publish Kind 0 and Kind 10002 events on successful connection
  5. Subscribe to Kind 23456 admin commands

Event Flow:

Relay → WebSocket → Pool → Subscription Callback → Command Handler → Response Event → Pool → Relay

Thread Safety:

  • Global state protected by pthread_mutex_t
  • Pool operations are thread-safe
  • Callbacks execute in management thread context

Files Modified/Created

New Files:

  • src/relay_client.c - Main implementation (700+ lines)
  • src/relay_client.h - Public API header
  • nostr_core_lib/nostr_core/core_relay_pool.h - Pool API header
  • nostr_core_lib/nostr_core/request_validator.h - Compilation stub

Modified Files:

  • Makefile - Added core_relay_pool.c compilation
  • nostr_core_lib/nostr_core/core_relay_pool.c - Added header include
  • src/main.c - Integrated relay client initialization

Build Status

Successfully compiles with zero errors and zero warnings.

Testing Requirements

To test the implementation:

  1. Configure Database:
UPDATE config SET
  enable_relay_connect = 1,
  kind_0_content = '{"name":"Ginxsom","about":"Blossom server","picture":""}',
  kind_10002_tags = '["wss://relay.damus.io","wss://nos.lol"]'
WHERE id = 1;
  1. Start Server:
./restart-all.sh
  1. Monitor Logs:
tail -f logs/app/app.log
  1. Check Status via Admin API:
curl http://localhost:8080/admin/relay/status
  1. Test Admin Commands: Send a Kind 23456 event to Ginxsom's pubkey with a command in the content field.

Next Steps

  • Add relay connection testing
  • Verify Kind 0/10002 event publishing to real relays
  • Test admin command subscription and response
  • Add relay health monitoring
  • Implement automatic reconnection on failure
  • Add metrics for relay performance

Architecture Benefits

  1. Simplified Code: Pool handles all WebSocket complexity
  2. Robust: Built-in reconnection, deduplication, and error handling
  3. Scalable: Supports multiple relays and subscriptions efficiently
  4. Maintainable: Clean separation between relay management and business logic
  5. Observable: Comprehensive statistics and status reporting

Implementation Plan

Overview

Ginxsom will use nostr_relay_pool_t from nostr_core_lib/nostr_core/core_relay_pool.c as the foundation for relay connectivity. This pool manager already handles connection state, reconnection logic, event deduplication, subscriptions, and message processing. Our implementation will be a thin wrapper that:

  1. Loads configuration from database
  2. Creates and configures the pool
  3. Publishes events using pool functions
  4. Subscribes with callbacks for admin commands
  5. Polls the pool in a background thread

Architecture

┌─────────────────────────────────────────────────────────────┐
│                      relay_client.c                          │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  Configuration Layer                                    │ │
│  │  - Load enable_relay_connect, kind_0_content,          │ │
│  │    kind_10002_tags from database                       │ │
│  └────────────────────────────────────────────────────────┘ │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  Pool Management Layer                                  │ │
│  │  - Create nostr_relay_pool_t                           │ │
│  │  - Add relays from config                              │ │
│  │  - Destroy pool on shutdown                            │ │
│  └────────────────────────────────────────────────────────┘ │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  Event Publishing Layer                                 │ │
│  │  - Create Kind 0 with nostr_create_and_sign_event()   │ │
│  │  - Create Kind 10002 with nostr_create_and_sign_event()│ │
│  │  - Publish via nostr_relay_pool_publish()             │ │
│  └────────────────────────────────────────────────────────┘ │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  Subscription Layer                                     │ │
│  │  - Subscribe to Kind 23456 via                         │ │
│  │    nostr_relay_pool_subscribe()                        │ │
│  │  - Handle events in callback function                  │ │
│  │  - Decrypt NIP-44 encrypted commands                   │ │
│  └────────────────────────────────────────────────────────┘ │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  Background Thread                                      │ │
│  │  - Call nostr_relay_pool_poll() in loop               │ │
│  │  - Process incoming messages                           │ │
│  │  - Trigger callbacks                                   │ │
│  └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│              nostr_relay_pool_t (core_relay_pool.c)         │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  Connection Management                                  │ │
│  │  - Automatic connection/reconnection                   │ │
│  │  - Connection state tracking                           │ │
│  │  - Multiple relay support (up to 32)                   │ │
│  └────────────────────────────────────────────────────────┘ │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  Event Deduplication                                    │ │
│  │  - Track seen event IDs (1000 events)                 │ │
│  │  - Prevent duplicate processing                        │ │
│  └────────────────────────────────────────────────────────┘ │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  Subscription Management                                │ │
│  │  - REQ/CLOSE message handling                          │ │
│  │  - EOSE tracking per relay                             │ │
│  │  - Event callbacks                                     │ │
│  └────────────────────────────────────────────────────────┘ │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  Message Processing                                     │ │
│  │  - Parse EVENT, EOSE, OK, NOTICE messages             │ │
│  │  - Latency tracking                                    │ │
│  │  - Statistics collection                               │ │
│  └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│         nostr_websocket_tls (WebSocket Client)              │
│  - TLS/SSL connections                                      │
│  - WebSocket protocol handling                              │
│  - Send/receive messages                                    │
└─────────────────────────────────────────────────────────────┘

Phase 1: Add Pool API Declarations ✓ COMPLETED

Status: Database schema already includes required config keys in src/main.c:

  • enable_relay_connect (boolean)
  • kind_0_content (JSON string)
  • kind_10002_tags (JSON array of relay URLs)

Phase 2: Add Pool API Declarations (NEW)

Since nostr_relay_pool_t functions are not exposed in public headers, we need to declare them in relay_client.c:

File: src/relay_client.c

Add after includes:

// Forward declarations for nostr_relay_pool_t API
// These functions are defined in nostr_core_lib/nostr_core/core_relay_pool.c
// but not exposed in public headers

typedef struct nostr_relay_pool nostr_relay_pool_t;
typedef struct nostr_pool_subscription nostr_pool_subscription_t;

typedef enum {
    NOSTR_POOL_RELAY_DISCONNECTED = 0,
    NOSTR_POOL_RELAY_CONNECTING = 1,
    NOSTR_POOL_RELAY_CONNECTED = 2,
    NOSTR_POOL_RELAY_ERROR = 3
} nostr_pool_relay_status_t;

typedef struct {
    // Connection statistics
    int connection_attempts;
    int connection_failures;
    time_t connection_uptime_start;
    time_t last_event_time;
    
    // Event statistics
    int events_received;
    int events_published;
    int events_published_ok;
    int events_published_failed;
    
    // Latency statistics
    double ping_latency_current;
    double ping_latency_avg;
    double ping_latency_min;
    double ping_latency_max;
    int ping_samples;
    
    double query_latency_avg;
    double query_latency_min;
    double query_latency_max;
    int query_samples;
    
    double publish_latency_avg;
    int publish_samples;
} nostr_relay_stats_t;

// Pool management functions
nostr_relay_pool_t* nostr_relay_pool_create(void);
int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url);
int nostr_relay_pool_remove_relay(nostr_relay_pool_t* pool, const char* relay_url);
void nostr_relay_pool_destroy(nostr_relay_pool_t* pool);

// Subscription functions
nostr_pool_subscription_t* nostr_relay_pool_subscribe(
    nostr_relay_pool_t* pool,
    const char** relay_urls,
    int relay_count,
    cJSON* filter,
    void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
    void (*on_eose)(void* user_data),
    void* user_data);

int nostr_pool_subscription_close(nostr_pool_subscription_t* subscription);

// Publishing functions
int nostr_relay_pool_publish(
    nostr_relay_pool_t* pool,
    const char** relay_urls,
    int relay_count,
    cJSON* event);

// Polling functions
int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms);

// Status functions
nostr_pool_relay_status_t nostr_relay_pool_get_relay_status(
    nostr_relay_pool_t* pool,
    const char* relay_url);

const nostr_relay_stats_t* nostr_relay_pool_get_relay_stats(
    nostr_relay_pool_t* pool,
    const char* relay_url);

Estimated Time: 30 minutes

Phase 3: Replace Custom State with Pool

File: src/relay_client.c

Replace the global state structure:

REMOVE:

static struct {
    int enabled;
    int initialized;
    int running;
    char db_path[512];
    relay_info_t relays[MAX_RELAYS];  // ← REMOVE THIS
    int relay_count;                   // ← REMOVE THIS
    pthread_t management_thread;
    pthread_mutex_t state_mutex;
} g_relay_state = {0};

ADD:

static struct {
    int enabled;
    int initialized;
    int running;
    char db_path[512];
    nostr_relay_pool_t* pool;          // ← ADD THIS
    char** relay_urls;                  // ← ADD THIS (for tracking)
    int relay_count;                    // ← KEEP THIS
    nostr_pool_subscription_t* admin_subscription;  // ← ADD THIS
    pthread_t management_thread;
    pthread_mutex_t state_mutex;
} g_relay_state = {0};

Estimated Time: 1 hour

Phase 4: Update Initialization

File: src/relay_client.c

Update relay_client_init():

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) {
        g_relay_state.pool = nostr_relay_pool_create();
        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]);
            }
        }
    }
    
    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;
}

Update parse_relay_urls() to allocate relay_urls 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;
}

Estimated Time: 1-2 hours

Phase 5: Implement Event Publishing with Pool

File: src/relay_client.c

Update relay_client_publish_kind0():

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 pool
    int success_count = nostr_relay_pool_publish(
        g_relay_state.pool,
        (const char**)g_relay_state.relay_urls,
        g_relay_state.relay_count,
        event
    );
    
    cJSON_Delete(event);
    
    if (success_count > 0) {
        app_log(LOG_INFO, "Kind 0 profile event published to %d relays", success_count);
        return 0;
    } else {
        app_log(LOG_ERROR, "Failed to publish Kind 0 profile event");
        return -1;
    }
}

Update relay_client_publish_kind10002():

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 pool
    int success_count = nostr_relay_pool_publish(
        g_relay_state.pool,
        (const char**)g_relay_state.relay_urls,
        g_relay_state.relay_count,
        event
    );
    
    cJSON_Delete(event);
    
    if (success_count > 0) {
        app_log(LOG_INFO, "Kind 10002 relay list event published to %d relays", success_count);
        return 0;
    } else {
        app_log(LOG_ERROR, "Failed to publish Kind 10002 relay list event");
        return -1;
    }
}

Estimated Time: 2-3 hours

Phase 6: Implement Subscription with Callbacks

File: src/relay_client.c

Add callback function for admin commands:

// Callback for received Kind 23456 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 23456 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 != 23456) {
        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);
    
    // TODO: Decrypt content using NIP-44
    // For now, log the encrypted content
    app_log(LOG_DEBUG, "Encrypted command content: %s", encrypted_content);
    
    // TODO: Parse and execute command
    // TODO: Send response using relay_client_send_admin_response()
}

// Callback for EOSE (End Of Stored Events)
static void on_admin_subscription_eose(void* user_data) {
    (void)user_data;
    app_log(LOG_INFO, "Received EOSE for admin command subscription");
}

Update subscribe_to_admin_commands():

static int subscribe_to_admin_commands(void) {
    if (!g_relay_state.pool) {
        return -1;
    }
    
    app_log(LOG_INFO, "Subscribing to Kind 23456 admin commands...");
    
    // Create subscription filter for Kind 23456 events addressed to us
    cJSON* filter = cJSON_CreateObject();
    cJSON* kinds = cJSON_CreateArray();
    cJSON_AddItemToArray(kinds, cJSON_CreateNumber(23456));
    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
    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
    );
    
    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;
}

Estimated Time: 2-3 hours

Phase 7: Update Management Thread to Use Pool Polling

File: src/relay_client.c

Replace relay_management_thread():

REMOVE: All custom connection management code

ADD:

static void *relay_management_thread(void *arg) {
    (void)arg;
    
    app_log(LOG_INFO, "Relay management thread started");
    
    // Wait a bit for initial connections to establish
    sleep(2);
    
    // 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;
}

REMOVE: These functions are no longer needed:

  • connect_to_relay()
  • disconnect_from_relay()
  • publish_event_to_relays() (replaced by pool publish)

Estimated Time: 1 hour

Phase 8: Update Cleanup

File: src/relay_client.c

Update relay_client_stop():

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");
}

Estimated Time: 30 minutes

Phase 9: Update Status Functions

File: src/relay_client.c

Update relay_client_get_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;
}

Update relay_client_reconnect():

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;
}

Estimated Time: 1 hour

Phase 10: Testing

Test Plan:

  1. Configuration Test:

    sqlite3 db/config.db "UPDATE config SET value='true' WHERE key='enable_relay_connect';"
    sqlite3 db/config.db "UPDATE config SET value='[\"wss://relay.damus.io\",\"wss://nos.lol\"]' WHERE key='kind_10002_tags';"
    sqlite3 db/config.db "UPDATE config SET value='{\"name\":\"Ginxsom Test\",\"about\":\"Blossom server\"}' WHERE key='kind_0_content';"
    
  2. Build and Run:

    make clean && make
    ./restart-all.sh
    
  3. Verify Logs:

    • Check logs/app/app.log for:
      • "Relay client initialized"
      • "Connected to relay: wss://..."
      • "Kind 0 profile event published"
      • "Kind 10002 relay list event published"
      • "Subscribed to admin commands"
  4. Check Status:

    • Use admin API to query relay status
    • Verify connection states and statistics
  5. Test Admin Commands (Future):

    • Send Kind 23456 event from admin pubkey
    • Verify event is received and processed
    • Verify Kind 23457 response is sent

Estimated Time: 2-3 hours

Total Estimated Implementation Time

  • Phase 1: ✓ Already completed
  • Phase 2: 30 minutes (API declarations)
  • Phase 3: 1 hour (replace state)
  • Phase 4: 1-2 hours (initialization)
  • Phase 5: 2-3 hours (publishing)
  • Phase 6: 2-3 hours (subscriptions)
  • Phase 7: 1 hour (management thread)
  • Phase 8: 30 minutes (cleanup)
  • Phase 9: 1 hour (status functions)
  • Phase 10: 2-3 hours (testing)

Total: 11-16 hours

Key Benefits of This Approach

  1. Minimal Code: We write ~500 lines instead of ~2000 lines
  2. Robust: Pool handles all edge cases (reconnection, deduplication, etc.)
  3. Maintainable: Pool is tested and maintained in nostr_core_lib
  4. Efficient: Pool uses optimized WebSocket handling
  5. Scalable: Pool supports up to 32 relays with proper connection management

Future Enhancements

  1. NIP-44 Encryption: Decrypt Kind 23456 commands and encrypt Kind 23457 responses
  2. Command Processing: Parse and execute admin commands
  3. Response Handling: Send structured responses back to admin
  4. Metrics: Expose relay statistics via admin API
  5. Dynamic Configuration: Allow runtime relay list updates

Implementation Plan - REVISED

Current Status (Completed)

Phase 1-3: Database schema, relay client framework, and stub functions are complete

  • Config keys added: enable_relay_connect, kind_0_content, kind_10002_tags
  • Module structure created: src/relay_client.h and src/relay_client.c
  • Stub implementations ready for replacement

Critical Realization: Use nostr_relay_pool_t

The nostr_core_lib already has EVERYTHING we need in core_relay_pool.c:

From reviewing the code:

  • nostr_relay_pool_t - Manages multiple relay connections
  • nostr_relay_pool_create() - Creates pool
  • nostr_relay_pool_add_relay() - Adds relays
  • nostr_relay_pool_publish() - Publishes events to all relays
  • nostr_relay_pool_subscribe() - Subscribes with callbacks
  • nostr_relay_pool_poll() - Processes messages
  • Automatic connection management and reconnection
  • Event deduplication
  • Statistics tracking
  • Ping/pong handling (currently disabled but available)

What we should do:

  • Don't maintain our own relay connection state
  • Don't implement our own reconnection logic
  • Don't implement our own message receiving loop
  • Use nostr_relay_pool_t for everything
  • Our code becomes a thin configuration wrapper

Simplified Architecture

relay_client.c (thin wrapper)
    ↓
nostr_relay_pool_t (handles everything)
    ↓
nostr_websocket_tls.h (WebSocket client)

Our relay_client.c should only:

  1. Load config from database
  2. Create and configure relay pool
  3. Publish Kind 0 and Kind 10002 on startup
  4. Subscribe to Kind 23456 with callback
  5. Call nostr_relay_pool_poll() in background thread

Implementation Phases

Phase 4: Replace Custom State with Relay Pool (2-3 hours)

Goal: Use nostr_relay_pool_t instead of custom relay management

  1. Update global state in relay_client.c

    // REMOVE custom relay array:
    // relay_info_t relays[MAX_RELAYS];
    // int relay_count;
    
    // REPLACE with:
    static struct {
        int enabled;
        int initialized;
        int running;
        char db_path[512];
        nostr_relay_pool_t* pool;  // Use the pool!
        pthread_t management_thread;
        pthread_mutex_t state_mutex;
    } g_relay_state = {0};
    
  2. Update relay_client_init()

    int relay_client_init(const char *db_path) {
        // ... existing initialization ...
    
        // Create relay pool
        g_relay_state.pool = nostr_relay_pool_create();
        if (!g_relay_state.pool) {
            app_log(LOG_ERROR, "Failed to create relay pool");
            return -1;
        }
    
        // Load relay URLs from database and add to pool
        // Parse kind_10002_tags JSON
        cJSON *relay_array = cJSON_Parse(json_from_db);
        int count = cJSON_GetArraySize(relay_array);
    
        for (int i = 0; i < count; i++) {
            cJSON *item = cJSON_GetArrayItem(relay_array, i);
            if (cJSON_IsString(item)) {
                const char *url = item->valuestring;
                nostr_relay_pool_add_relay(g_relay_state.pool, url);
                app_log(LOG_INFO, "Added relay to pool: %s", url);
            }
        }
    
        cJSON_Delete(relay_array);
        return 0;
    }
    
  3. Remove custom connection functions

    • DELETE connect_to_relay() - pool handles this
    • DELETE disconnect_from_relay() - pool handles this
    • DELETE ensure_relay_connection() - pool handles this

Phase 5: Use Pool for Publishing (1-2 hours)

Goal: Use nostr_relay_pool_publish() for events

  1. Update relay_client_publish_kind0()

    int relay_client_publish_kind0(void) {
        // Load kind_0_content from database
        const char *content = ...; // from database
    
        // Create tags (empty for Kind 0)
        cJSON *tags = cJSON_CreateArray();
    
        // Convert hex private key to bytes
        unsigned char privkey_bytes[32];
        nostr_hex_to_bytes(g_blossom_seckey, privkey_bytes, 32);
    
        // Create and sign event using nostr_core_lib
        cJSON *event = nostr_create_and_sign_event(
            0,              // kind
            content,        // content
            tags,           // tags
            privkey_bytes,  // private key
            time(NULL)      // timestamp
        );
    
        if (!event) {
            app_log(LOG_ERROR, "Failed to create Kind 0 event");
            cJSON_Delete(tags);
            return -1;
        }
    
        // Get relay URLs from pool
        char **relay_urls = NULL;
        nostr_pool_relay_status_t *statuses = NULL;
        int relay_count = nostr_relay_pool_list_relays(g_relay_state.pool, 
                                                        &relay_urls, &statuses);
    
        // Publish to all relays in pool
        int success = nostr_relay_pool_publish(g_relay_state.pool, 
                                               (const char**)relay_urls,
                                               relay_count, event);
    
        // Cleanup
        for (int i = 0; i < relay_count; i++) {
            free(relay_urls[i]);
        }
        free(relay_urls);
        free(statuses);
        cJSON_Delete(event);
    
        return (success > 0) ? 0 : -1;
    }
    
  2. Update relay_client_publish_kind10002() (similar pattern)

    int relay_client_publish_kind10002(void) {
        // Build tags from relay URLs
        char **relay_urls = NULL;
        nostr_pool_relay_status_t *statuses = NULL;
        int relay_count = nostr_relay_pool_list_relays(g_relay_state.pool,
                                                        &relay_urls, &statuses);
    
        cJSON *tags = cJSON_CreateArray();
        for (int i = 0; i < relay_count; i++) {
            cJSON *tag = cJSON_CreateArray();
            cJSON_AddItemToArray(tag, cJSON_CreateString("r"));
            cJSON_AddItemToArray(tag, cJSON_CreateString(relay_urls[i]));
            cJSON_AddItemToArray(tags, tag);
        }
    
        // Create and sign event
        unsigned char privkey_bytes[32];
        nostr_hex_to_bytes(g_blossom_seckey, privkey_bytes, 32);
    
        cJSON *event = nostr_create_and_sign_event(10002, "", tags, 
                                                    privkey_bytes, time(NULL));
    
        // Publish
        int success = nostr_relay_pool_publish(g_relay_state.pool,
                                               (const char**)relay_urls,
                                               relay_count, event);
    
        // Cleanup
        for (int i = 0; i < relay_count; i++) {
            free(relay_urls[i]);
        }
        free(relay_urls);
        free(statuses);
        cJSON_Delete(event);
    
        return (success > 0) ? 0 : -1;
    }
    
  3. Remove publish_event_to_relays() - not needed, use pool directly

Phase 6: Use Pool for Subscriptions (2-3 hours)

Goal: Use nostr_relay_pool_subscribe() with callbacks

  1. Create event callback function

    static void on_admin_command_event(cJSON* event, const char* relay_url, 
                                       void* user_data) {
        app_log(LOG_INFO, "Received admin command from %s", relay_url);
    
        // Extract event details
        cJSON *kind = cJSON_GetObjectItem(event, "kind");
        cJSON *content = cJSON_GetObjectItem(event, "content");
        cJSON *pubkey = cJSON_GetObjectItem(event, "pubkey");
    
        // Verify it's from admin
        if (pubkey && cJSON_IsString(pubkey)) {
            const char *sender = cJSON_GetStringValue(pubkey);
            if (strcmp(sender, g_admin_pubkey) == 0) {
                // TODO: Process admin command
                // TODO: Decrypt NIP-44 content
                // TODO: Execute command
                // TODO: Send response via relay_client_send_admin_response()
                app_log(LOG_INFO, "Processing admin command");
            }
        }
    }
    
    static void on_eose(void* user_data) {
        app_log(LOG_DEBUG, "End of stored events for admin commands");
    }
    
  2. Update subscribe_to_admin_commands()

    static int subscribe_to_admin_commands(void) {
        // Create filter for Kind 23456 addressed to us
        cJSON *filter = cJSON_CreateObject();
    
        cJSON *kinds = cJSON_CreateArray();
        cJSON_AddItemToArray(kinds, cJSON_CreateNumber(23456));
        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", time(NULL));
    
        // Get relay URLs
        char **relay_urls = NULL;
        nostr_pool_relay_status_t *statuses = NULL;
        int relay_count = nostr_relay_pool_list_relays(g_relay_state.pool,
                                                        &relay_urls, &statuses);
    
        // Subscribe using pool
        nostr_pool_subscription_t *sub = nostr_relay_pool_subscribe(
            g_relay_state.pool,
            (const char**)relay_urls,
            relay_count,
            filter,
            on_admin_command_event,  // callback for events
            on_eose,                 // callback for EOSE
            NULL                     // user_data
        );
    
        // Cleanup
        for (int i = 0; i < relay_count; i++) {
            free(relay_urls[i]);
        }
        free(relay_urls);
        free(statuses);
        cJSON_Delete(filter);
    
        return (sub != NULL) ? 0 : -1;
    }
    

Phase 7: Simplify Management Thread (1 hour)

Goal: Let pool handle everything via polling

  1. Simplify relay_management_thread()

    static void *relay_management_thread(void *arg) {
        app_log(LOG_INFO, "Relay management thread started");
    
        // Wait for connections to establish
        sleep(2);
    
        // Publish initial events
        relay_client_publish_kind0();
        relay_client_publish_kind10002();
    
        // Subscribe to admin commands
        subscribe_to_admin_commands();
    
        // Main loop: just poll the pool
        while (g_relay_state.running) {
            // Let the pool handle everything
            nostr_relay_pool_poll(g_relay_state.pool, 100);
    
            // Small delay
            usleep(10000); // 10ms
        }
    
        app_log(LOG_INFO, "Relay management thread stopping");
        return NULL;
    }
    
  2. Remove all custom message handling - pool does it via callbacks

Phase 8: Update Cleanup (30 minutes)

Goal: Properly destroy pool

  1. Update relay_client_stop()
    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
        pthread_join(g_relay_state.management_thread, NULL);
    
        // Destroy pool (handles all cleanup)
        if (g_relay_state.pool) {
            nostr_relay_pool_destroy(g_relay_state.pool);
            g_relay_state.pool = NULL;
        }
    
        pthread_mutex_destroy(&g_relay_state.state_mutex);
    
        app_log(LOG_INFO, "Relay client stopped");
    }
    

Phase 9: Main Integration (1 hour)

Goal: Wire into ginxsom startup

  1. Add to main.c after database initialization

    // Initialize relay client
    if (relay_client_init(g_db_path) != 0) {
        app_log(LOG_ERROR, "Failed to initialize relay client");
    }
    
    // Start if enabled
    if (relay_client_is_enabled()) {
        if (relay_client_start() != 0) {
            app_log(LOG_ERROR, "Failed to start relay client");
        }
    }
    
  2. Add to cleanup_and_exit()

    relay_client_stop();
    

Phase 10: Testing (2-3 hours)

  1. Configure database

    UPDATE config SET value='true' WHERE key='enable_relay_connect';
    UPDATE config SET value='["wss://relay.damus.io","wss://nos.lol"]' 
        WHERE key='kind_10002_tags';
    UPDATE config SET value='{"name":"Ginxsom","about":"Blossom server"}' 
        WHERE key='kind_0_content';
    
  2. Build and test

    make clean && make
    ./build/ginxsom-fcgi
    
  3. Verify in logs

    • Relay pool created
    • Relays added to pool
    • Kind 0 published
    • Kind 10002 published
    • Subscribed to admin commands
  4. External verification

    • Use nostr client to search for events by ginxsom's pubkey

Key nostr_relay_pool Functions

Pool Management:

  • nostr_relay_pool_create() - Create pool
  • nostr_relay_pool_add_relay(pool, url) - Add relay
  • nostr_relay_pool_remove_relay(pool, url) - Remove relay
  • nostr_relay_pool_destroy(pool) - Cleanup

Publishing:

  • nostr_relay_pool_publish(pool, urls, count, event) - Publish to relays
  • Returns number of successful publishes

Subscribing:

  • nostr_relay_pool_subscribe(pool, urls, count, filter, on_event, on_eose, user_data) - Subscribe with callbacks
  • nostr_pool_subscription_close(subscription) - Close subscription

Polling:

  • nostr_relay_pool_poll(pool, timeout_ms) - Process messages
  • nostr_relay_pool_run(pool, timeout_ms) - Run until timeout

Status:

  • nostr_relay_pool_get_relay_status(pool, url) - Get relay status
  • nostr_relay_pool_list_relays(pool, &urls, &statuses) - List all relays
  • nostr_relay_pool_get_relay_stats(pool, url) - Get statistics

Event Creation:

  • nostr_create_and_sign_event(kind, content, tags, privkey, timestamp) - Create signed event

Estimated Timeline

  • Phase 4: Replace with Pool - 2-3 hours
  • Phase 5: Use Pool for Publishing - 1-2 hours
  • Phase 6: Use Pool for Subscriptions - 2-3 hours
  • Phase 7: Simplify Thread - 1 hour
  • Phase 8: Update Cleanup - 30 minutes
  • Phase 9: Main Integration - 1 hour
  • Phase 10: Testing - 2-3 hours

Total: 9-13 hours (much simpler by using the pool!)

What Gets Removed

By using nostr_relay_pool_t, we can DELETE:

  • Custom relay_info_t struct and array
  • connect_to_relay() function
  • disconnect_from_relay() function
  • ensure_relay_connection() function
  • Custom reconnection logic
  • Custom message receiving loop
  • publish_event_to_relays() function
  • Manual WebSocket state tracking

What Remains

Our code becomes much simpler:

  • Load config from database
  • Create and configure pool
  • Publish Kind 0/10002 using pool
  • Subscribe with callbacks
  • Call nostr_relay_pool_poll() in thread
  • Cleanup pool on shutdown

The relay pool does all the heavy lifting!