1641 lines
57 KiB
Markdown
1641 lines
57 KiB
Markdown
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**:
|
|
```sql
|
|
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**:
|
|
```c
|
|
// 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**:
|
|
```bash
|
|
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**:
|
|
```bash
|
|
# 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`](../src/relay_client.c:1) and [`src/relay_client.h`](../src/relay_client.h:1) 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`:
|
|
- Created [`nostr_core_lib/nostr_core/core_relay_pool.h`](../nostr_core_lib/nostr_core/core_relay_pool.h:1) - Public API header
|
|
- Created [`nostr_core_lib/nostr_core/request_validator.h`](../nostr_core_lib/nostr_core/request_validator.h:1) - Stub for compilation
|
|
- Updated [`Makefile`](../Makefile:1) to compile `core_relay_pool.c` directly
|
|
- Pool manages all relay connections, subscriptions, and message routing
|
|
|
|
### Phase 4: Event Publishing ✅
|
|
Implemented proper Nostr event creation and publishing:
|
|
- [`relay_client_publish_kind0()`](../src/relay_client.c:404) - Publishes profile metadata using `nostr_create_and_sign_event()`
|
|
- [`relay_client_publish_kind10002()`](../src/relay_client.c:482) - Publishes relay list with proper tag structure
|
|
- Uses `nostr_relay_pool_publish()` for multi-relay broadcasting
|
|
- Events are properly signed with Ginxsom's private key
|
|
|
|
### Phase 5: Admin Command Subscription ✅
|
|
Implemented subscription to Kind 23456 admin commands:
|
|
- [`on_admin_command_event()`](../src/relay_client.c:604) - Callback for received admin commands
|
|
- [`subscribe_to_admin_commands()`](../src/relay_client.c:649) - Sets up subscription with filter
|
|
- Filters events by admin pubkey and Ginxsom's pubkey in 'p' tags
|
|
- Processes commands and sends responses via Kind 23457 events
|
|
|
|
### Phase 6: Management Thread ✅
|
|
Simplified relay management using pool polling:
|
|
- [`relay_management_thread()`](../src/relay_client.c:294) - 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()`](../src/relay_client.c:619) - 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:**
|
|
```sql
|
|
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;
|
|
```
|
|
|
|
2. **Start Server:**
|
|
```bash
|
|
./restart-all.sh
|
|
```
|
|
|
|
3. **Monitor Logs:**
|
|
```bash
|
|
tail -f logs/app/app.log
|
|
```
|
|
|
|
4. **Check Status via Admin API:**
|
|
```bash
|
|
curl http://localhost:8080/admin/relay/status
|
|
```
|
|
|
|
5. **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:
|
|
```c
|
|
// 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**:
|
|
```c
|
|
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**:
|
|
```c
|
|
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()`:
|
|
|
|
```c
|
|
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:
|
|
|
|
```c
|
|
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()`:
|
|
|
|
```c
|
|
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()`:
|
|
|
|
```c
|
|
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:
|
|
|
|
```c
|
|
// 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()`:
|
|
|
|
```c
|
|
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**:
|
|
```c
|
|
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()`:
|
|
|
|
```c
|
|
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()`:
|
|
|
|
```c
|
|
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()`:
|
|
|
|
```c
|
|
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**:
|
|
```bash
|
|
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**:
|
|
```bash
|
|
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**
|
|
```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()**
|
|
```c
|
|
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()**
|
|
```c
|
|
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)
|
|
```c
|
|
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**
|
|
```c
|
|
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()**
|
|
```c
|
|
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()**
|
|
```c
|
|
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()**
|
|
```c
|
|
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**
|
|
```c
|
|
// 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()**
|
|
```c
|
|
relay_client_stop();
|
|
```
|
|
|
|
#### Phase 10: Testing (2-3 hours)
|
|
|
|
1. **Configure database**
|
|
```sql
|
|
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**
|
|
```bash
|
|
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!**
|