Files
c-relay/src/api.c

2349 lines
86 KiB
C
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Define _GNU_SOURCE to ensure all POSIX features are available
#define _GNU_SOURCE
// API module for serving embedded web content and admin API functions
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <libwebsockets.h>
#include <cjson/cJSON.h>
#include <sqlite3.h>
#include <time.h>
#include <sys/stat.h>
#include <unistd.h>
#include <strings.h>
#include "api.h"
#include "embedded_web_content.h"
#include "config.h"
#include "debug.h"
#include "../nostr_core_lib/nostr_core/nostr_core.h"
#include "../nostr_core_lib/nostr_core/nip017.h"
#include "../nostr_core_lib/nostr_core/nip044.h"
#include "subscriptions.h"
// External subscription manager (from main.c via subscriptions.c)
extern subscription_manager_t g_subscription_manager;
// Global variables for config change system
static pending_config_change_t* pending_changes_head = NULL;
static int pending_changes_count = 0;
#define CONFIG_CHANGE_TIMEOUT 300 // 5 minutes
// Forward declarations for database functions
int store_event(cJSON* event);
int broadcast_event_to_subscriptions(cJSON* event);
// Forward declarations for config functions
char* get_relay_private_key(void);
const char* get_config_value(const char* key);
int get_config_bool(const char* key, int default_value);
int update_config_in_table(const char* key, const char* value);
// Monitoring system state (throttling now handled per-function)
// Forward declaration for monitoring helper function
int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_func)(void));
// Forward declaration for CPU metrics query function
cJSON* query_cpu_metrics(void);
// Monitoring system helper functions
int get_monitoring_throttle_seconds(void) {
return get_config_int("kind_24567_reporting_throttle_sec", 5);
}
// Query event kind distribution from database
cJSON* query_event_kind_distribution(void) {
extern sqlite3* g_db;
if (!g_db) {
DEBUG_ERROR("Database not available for monitoring query");
return NULL;
}
// Query event kinds distribution with total count
sqlite3_stmt* stmt;
const char* sql = "SELECT kind, COUNT(*) as count FROM events GROUP BY kind ORDER BY count DESC";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
DEBUG_ERROR("Failed to prepare event kind distribution query");
return NULL;
}
cJSON* distribution = cJSON_CreateObject();
cJSON_AddStringToObject(distribution, "data_type", "event_kinds");
cJSON_AddNumberToObject(distribution, "timestamp", (double)time(NULL));
cJSON* kinds_array = cJSON_CreateArray();
long long total_events = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
int kind = sqlite3_column_int(stmt, 0);
long long count = sqlite3_column_int64(stmt, 1);
total_events += count;
cJSON* kind_obj = cJSON_CreateObject();
cJSON_AddNumberToObject(kind_obj, "kind", kind);
cJSON_AddNumberToObject(kind_obj, "count", count);
cJSON_AddItemToArray(kinds_array, kind_obj);
}
sqlite3_finalize(stmt);
cJSON_AddNumberToObject(distribution, "total_events", total_events);
cJSON_AddItemToObject(distribution, "kinds", kinds_array);
return distribution;
}
// Query time-based statistics from database
cJSON* query_time_based_statistics(void) {
extern sqlite3* g_db;
if (!g_db) {
DEBUG_ERROR("Database not available for time stats query");
return NULL;
}
time_t now = time(NULL);
cJSON* time_stats = cJSON_CreateObject();
cJSON_AddStringToObject(time_stats, "data_type", "time_stats");
cJSON_AddNumberToObject(time_stats, "timestamp", (double)now);
cJSON* periods_array = cJSON_CreateArray();
// Define time periods: 24h, 7d, 30d
struct {
const char* period;
time_t seconds;
const char* description;
} periods[] = {
{"last_24h", 86400, "Events in the last 24 hours"},
{"last_7d", 604800, "Events in the last 7 days"},
{"last_30d", 2592000, "Events in the last 30 days"},
{NULL, 0, NULL}
};
// Get total events count
sqlite3_stmt* total_stmt;
const char* total_sql = "SELECT COUNT(*) FROM events";
long long total_events = 0;
if (sqlite3_prepare_v2(g_db, total_sql, -1, &total_stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(total_stmt) == SQLITE_ROW) {
total_events = sqlite3_column_int64(total_stmt, 0);
}
sqlite3_finalize(total_stmt);
}
// Query each time period
for (int i = 0; periods[i].period != NULL; i++) {
sqlite3_stmt* stmt;
const char* sql = "SELECT COUNT(*) FROM events WHERE created_at >= ?";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
DEBUG_ERROR("Failed to prepare time stats query");
continue;
}
time_t cutoff = now - periods[i].seconds;
sqlite3_bind_int64(stmt, 1, cutoff);
long long count = 0;
if (sqlite3_step(stmt) == SQLITE_ROW) {
count = sqlite3_column_int64(stmt, 0);
}
sqlite3_finalize(stmt);
cJSON* period_obj = cJSON_CreateObject();
cJSON_AddStringToObject(period_obj, "period", periods[i].period);
cJSON_AddNumberToObject(period_obj, "count", count);
cJSON_AddStringToObject(period_obj, "description", periods[i].description);
cJSON_AddItemToArray(periods_array, period_obj);
}
cJSON_AddItemToObject(time_stats, "periods", periods_array);
cJSON_AddNumberToObject(time_stats, "total_events", total_events);
return time_stats;
}
// Query top pubkeys by event count from database
cJSON* query_top_pubkeys(void) {
extern sqlite3* g_db;
if (!g_db) {
DEBUG_ERROR("Database not available for top pubkeys query");
return NULL;
}
// Query top 10 pubkeys by event count
sqlite3_stmt* stmt;
const char* sql = "SELECT pubkey, COUNT(*) as count FROM events GROUP BY pubkey ORDER BY count DESC LIMIT 10";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
DEBUG_ERROR("Failed to prepare top pubkeys query");
return NULL;
}
cJSON* top_pubkeys = cJSON_CreateObject();
cJSON_AddStringToObject(top_pubkeys, "data_type", "top_pubkeys");
cJSON_AddNumberToObject(top_pubkeys, "timestamp", (double)time(NULL));
cJSON* pubkeys_array = cJSON_CreateArray();
// Get total events count for percentage calculation
sqlite3_stmt* total_stmt;
const char* total_sql = "SELECT COUNT(*) FROM events";
long long total_events = 0;
if (sqlite3_prepare_v2(g_db, total_sql, -1, &total_stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(total_stmt) == SQLITE_ROW) {
total_events = sqlite3_column_int64(total_stmt, 0);
}
sqlite3_finalize(total_stmt);
}
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* pubkey = (const char*)sqlite3_column_text(stmt, 0);
long long count = sqlite3_column_int64(stmt, 1);
cJSON* pubkey_obj = cJSON_CreateObject();
cJSON_AddStringToObject(pubkey_obj, "pubkey", pubkey ? pubkey : "");
cJSON_AddNumberToObject(pubkey_obj, "event_count", count);
// Percentage will be calculated by frontend using total_events
cJSON_AddItemToArray(pubkeys_array, pubkey_obj);
}
sqlite3_finalize(stmt);
cJSON_AddItemToObject(top_pubkeys, "pubkeys", pubkeys_array);
cJSON_AddNumberToObject(top_pubkeys, "total_events", total_events);
return top_pubkeys;
}
// Query active subscriptions summary from database
cJSON* query_active_subscriptions(void) {
extern sqlite3* g_db;
if (!g_db) {
DEBUG_ERROR("Database not available for active subscriptions query");
return NULL;
}
// Get configuration limits
int max_subs = g_subscription_manager.max_total_subscriptions;
int max_per_client = g_subscription_manager.max_subscriptions_per_client;
// Query total active subscriptions from database
sqlite3_stmt* stmt;
const char* sql =
"SELECT COUNT(*) as total_subs, "
"COUNT(DISTINCT client_ip) as client_count "
"FROM subscriptions "
"WHERE event_type = 'created' AND ended_at IS NULL";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
DEBUG_ERROR("Failed to prepare active subscriptions query");
return NULL;
}
int total_subs = 0;
int client_count = 0;
if (sqlite3_step(stmt) == SQLITE_ROW) {
total_subs = sqlite3_column_int(stmt, 0);
client_count = sqlite3_column_int(stmt, 1);
}
sqlite3_finalize(stmt);
// Query max subscriptions per client
int most_subs_per_client = 0;
const char* max_sql =
"SELECT MAX(sub_count) FROM ("
" SELECT COUNT(*) as sub_count "
" FROM subscriptions "
" WHERE event_type = 'created' AND ended_at IS NULL "
" GROUP BY client_ip"
")";
if (sqlite3_prepare_v2(g_db, max_sql, -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
most_subs_per_client = sqlite3_column_int(stmt, 0);
}
sqlite3_finalize(stmt);
}
// Calculate statistics
double utilization_percentage = max_subs > 0 ? (total_subs * 100.0 / max_subs) : 0.0;
double avg_subs_per_client = client_count > 0 ? (total_subs * 1.0 / client_count) : 0.0;
// Build JSON response matching the design spec
cJSON* subscriptions = cJSON_CreateObject();
cJSON_AddStringToObject(subscriptions, "data_type", "active_subscriptions");
cJSON_AddNumberToObject(subscriptions, "timestamp", (double)time(NULL));
cJSON* data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "total_subscriptions", total_subs);
cJSON_AddNumberToObject(data, "max_subscriptions", max_subs);
cJSON_AddNumberToObject(data, "utilization_percentage", utilization_percentage);
cJSON_AddNumberToObject(data, "subscriptions_per_client_avg", avg_subs_per_client);
cJSON_AddNumberToObject(data, "most_subscriptions_per_client", most_subs_per_client);
cJSON_AddNumberToObject(data, "max_subscriptions_per_client", max_per_client);
cJSON_AddNumberToObject(data, "active_clients", client_count);
cJSON_AddItemToObject(subscriptions, "data", data);
return subscriptions;
}
// Query detailed subscription information from database log (ADMIN ONLY)
// Uses subscriptions table instead of in-memory iteration to avoid mutex contention
cJSON* query_subscription_details(void) {
extern sqlite3* g_db;
if (!g_db) {
DEBUG_ERROR("Database not available for subscription details query");
return NULL;
}
// Query active subscriptions directly from subscriptions table
// Get subscriptions that were created but not yet closed/expired/disconnected
sqlite3_stmt* stmt;
const char* sql =
"SELECT subscription_id, client_ip, wsi_pointer, filter_json, events_sent, "
"created_at, (strftime('%s', 'now') - created_at) as duration_seconds "
"FROM subscriptions "
"WHERE event_type = 'created' AND ended_at IS NULL "
"ORDER BY created_at DESC LIMIT 100";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
DEBUG_ERROR("Failed to prepare subscription details query");
return NULL;
}
time_t current_time = time(NULL);
cJSON* subscriptions_data = cJSON_CreateObject();
cJSON_AddStringToObject(subscriptions_data, "data_type", "subscription_details");
cJSON_AddNumberToObject(subscriptions_data, "timestamp", (double)current_time);
cJSON* data = cJSON_CreateObject();
cJSON* subscriptions_array = cJSON_CreateArray();
// Iterate through query results
while (sqlite3_step(stmt) == SQLITE_ROW) {
cJSON* sub_obj = cJSON_CreateObject();
// Extract subscription data from database
const char* sub_id = (const char*)sqlite3_column_text(stmt, 0);
const char* client_ip = (const char*)sqlite3_column_text(stmt, 1);
const char* wsi_pointer = (const char*)sqlite3_column_text(stmt, 2);
const char* filter_json = (const char*)sqlite3_column_text(stmt, 3);
long long events_sent = sqlite3_column_int64(stmt, 4);
long long created_at = sqlite3_column_int64(stmt, 5);
long long duration_seconds = sqlite3_column_int64(stmt, 6);
// Add basic subscription info
cJSON_AddStringToObject(sub_obj, "id", sub_id ? sub_id : "");
cJSON_AddStringToObject(sub_obj, "client_ip", client_ip ? client_ip : "");
cJSON_AddStringToObject(sub_obj, "wsi_pointer", wsi_pointer ? wsi_pointer : "");
cJSON_AddNumberToObject(sub_obj, "created_at", (double)created_at);
cJSON_AddNumberToObject(sub_obj, "duration_seconds", (double)duration_seconds);
cJSON_AddNumberToObject(sub_obj, "events_sent", events_sent);
cJSON_AddBoolToObject(sub_obj, "active", 1); // All from this view are active
// Parse and add filter JSON if available
if (filter_json) {
cJSON* filters = cJSON_Parse(filter_json);
if (filters) {
cJSON_AddItemToObject(sub_obj, "filters", filters);
} else {
// If parsing fails, add empty array
cJSON_AddItemToObject(sub_obj, "filters", cJSON_CreateArray());
}
} else {
cJSON_AddItemToObject(sub_obj, "filters", cJSON_CreateArray());
}
cJSON_AddItemToArray(subscriptions_array, sub_obj);
}
sqlite3_finalize(stmt);
// Add subscriptions array and count to data
cJSON_AddItemToObject(data, "subscriptions", subscriptions_array);
cJSON_AddNumberToObject(data, "total_count", cJSON_GetArraySize(subscriptions_array));
cJSON_AddItemToObject(subscriptions_data, "data", data);
return subscriptions_data;
}
// Generate and broadcast monitoring event
int generate_monitoring_event(void) {
// Generate event_kinds monitoring event
if (generate_monitoring_event_for_type("event_kinds", query_event_kind_distribution) != 0) {
DEBUG_ERROR("Failed to generate event_kinds monitoring event");
return -1;
}
// Generate time_stats monitoring event
if (generate_monitoring_event_for_type("time_stats", query_time_based_statistics) != 0) {
DEBUG_ERROR("Failed to generate time_stats monitoring event");
return -1;
}
// Generate top_pubkeys monitoring event
if (generate_monitoring_event_for_type("top_pubkeys", query_top_pubkeys) != 0) {
DEBUG_ERROR("Failed to generate top_pubkeys monitoring event");
return -1;
}
// Generate active_subscriptions monitoring event
if (generate_monitoring_event_for_type("active_subscriptions", query_active_subscriptions) != 0) {
DEBUG_ERROR("Failed to generate active_subscriptions monitoring event");
return -1;
}
// Generate subscription_details monitoring event (admin-only)
if (generate_monitoring_event_for_type("subscription_details", query_subscription_details) != 0) {
DEBUG_ERROR("Failed to generate subscription_details monitoring event");
return -1;
}
// Generate CPU metrics monitoring event
if (generate_monitoring_event_for_type("cpu_metrics", query_cpu_metrics) != 0) {
DEBUG_ERROR("Failed to generate cpu_metrics monitoring event");
return -1;
}
DEBUG_INFO("Generated and broadcast all monitoring events");
return 0;
}
// Helper function to generate monitoring event for a specific type
int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_func)(void)) {
// Query the monitoring data
cJSON* monitoring_data = query_func();
if (!monitoring_data) {
DEBUG_ERROR("Failed to query monitoring data for %s", d_tag_value);
return -1;
}
// Convert to JSON string for content
char* content_json = cJSON_Print(monitoring_data);
cJSON_Delete(monitoring_data);
if (!content_json) {
DEBUG_ERROR("Failed to serialize monitoring data for %s", d_tag_value);
return -1;
}
// Get relay keys for signing
const char* relay_pubkey = get_config_value("relay_pubkey");
char* relay_privkey_hex = get_relay_private_key();
if (!relay_pubkey || !relay_privkey_hex) {
free(content_json);
DEBUG_ERROR("Could not get relay keys for monitoring event (%s)", d_tag_value);
return -1;
}
// Convert relay private key to bytes
unsigned char relay_privkey[32];
if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) {
free(relay_privkey_hex);
free(content_json);
DEBUG_ERROR("Failed to convert relay private key for monitoring event (%s)", d_tag_value);
return -1;
}
free(relay_privkey_hex);
// Create monitoring event (kind 24567 - ephemeral)
cJSON* monitoring_event = cJSON_CreateObject();
cJSON_AddStringToObject(monitoring_event, "id", ""); // Will be set by signing
cJSON_AddStringToObject(monitoring_event, "pubkey", relay_pubkey);
cJSON_AddNumberToObject(monitoring_event, "created_at", (double)time(NULL));
cJSON_AddNumberToObject(monitoring_event, "kind", 24567);
cJSON_AddStringToObject(monitoring_event, "content", content_json);
// Create tags array with d tag for identification
cJSON* tags = cJSON_CreateArray();
// d tag for event identification
cJSON* d_tag = cJSON_CreateArray();
cJSON_AddItemToArray(d_tag, cJSON_CreateString("d"));
cJSON_AddItemToArray(d_tag, cJSON_CreateString(d_tag_value));
cJSON_AddItemToArray(tags, d_tag);
cJSON_AddItemToObject(monitoring_event, "tags", tags);
// Use the library function to create and sign the event
cJSON* signed_event = nostr_create_and_sign_event(
24567, // kind (ephemeral)
cJSON_GetStringValue(cJSON_GetObjectItem(monitoring_event, "content")), // content
tags, // tags
relay_privkey, // private key
(time_t)cJSON_GetNumberValue(cJSON_GetObjectItem(monitoring_event, "created_at")) // timestamp
);
if (!signed_event) {
cJSON_Delete(monitoring_event);
free(content_json);
DEBUG_ERROR("Failed to create and sign monitoring event (%s)", d_tag_value);
return -1;
}
// Replace the unsigned event with the signed one
cJSON_Delete(monitoring_event);
monitoring_event = signed_event;
// Broadcast the ephemeral event to active subscriptions (no database storage)
broadcast_event_to_subscriptions(monitoring_event);
cJSON_Delete(monitoring_event);
free(content_json);
DEBUG_LOG("Monitoring event broadcast (ephemeral kind 24567, type: %s)", d_tag_value);
return 0;
}
// Monitoring hook called when an event is stored
void monitoring_on_event_stored(void) {
// Check throttling first (cheapest check)
static time_t last_monitoring_time = 0;
time_t current_time = time(NULL);
int throttle_seconds = get_monitoring_throttle_seconds();
if (current_time - last_monitoring_time < throttle_seconds) {
return;
}
// Check if anyone is subscribed to monitoring events (kind 24567)
// This is the ONLY activation check needed - if someone subscribes, they want monitoring
if (!has_subscriptions_for_kind(24567)) {
return; // No subscribers = no expensive operations
}
// Generate monitoring events only when someone is listening
last_monitoring_time = current_time;
generate_monitoring_event();
}
// Forward declaration for known_configs (defined in config.c)
typedef struct {
const char* key;
const char* type;
int min_val;
int max_val;
} config_definition_t;
// Define known_configs array locally in api.c
static const config_definition_t known_configs[] = {
// Authentication
{"auth_enabled", "bool", 0, 1},
{"nip42_auth_required", "bool", 0, 1},
{"nip42_auth_required_events", "bool", 0, 1},
{"nip42_auth_required_subscriptions", "bool", 0, 1},
{"nip42_auth_required_kinds", "string", 0, 255},
{"nip42_challenge_expiration", "int", 60, 3600},
{"nip42_challenge_timeout", "int", 60, 3600},
{"nip42_time_tolerance", "int", 60, 3600},
{"nip70_protected_events_enabled", "bool", 0, 1},
// Server Core Settings
{"relay_port", "int", 1, 65535},
{"max_connections", "int", 1, 10000},
// NIP-11 Relay Information
{"relay_name", "string", 0, 256},
{"relay_description", "string", 0, 512},
{"relay_contact", "string", 0, 256},
{"relay_software", "string", 0, 256},
{"relay_version", "string", 0, 256},
{"supported_nips", "string", 0, 1024},
{"language_tags", "string", 0, 1024},
{"relay_countries", "string", 0, 1024},
{"posting_policy", "string", 0, 1024},
{"payments_url", "string", 0, 1024},
// NIP-13 Proof of Work
{"pow_min_difficulty", "int", 0, 32},
{"pow_mode", "string", 0, 32},
// NIP-40 Expiration Timestamp
{"nip40_expiration_enabled", "bool", 0, 1},
{"nip40_expiration_strict", "bool", 0, 1},
{"nip40_expiration_filter", "bool", 0, 1},
{"nip40_expiration_grace_period", "int", 0, 86400},
// Subscription Limits
{"max_subscriptions_per_client", "int", 1, 1000},
{"max_total_subscriptions", "int", 1, 100000},
{"max_filters_per_subscription", "int", 1, 100},
// Event Processing Limits
{"max_event_tags", "int", 1, 1000},
{"max_content_length", "int", 100, 1048576},
{"max_message_length", "int", 1024, 1048576},
// Performance Settings
{"default_limit", "int", 1, 50000},
{"max_limit", "int", 1, 50000},
// Relay keys
{"relay_pubkey", "string", 0, 65},
{"relay_privkey", "string", 0, 65},
{"admin_pubkey", "string", 0, 65},
// Sentinel
{NULL, NULL, 0, 0}
};
// External database connection (from main.c)
extern sqlite3* g_db;
extern char g_database_path[512];
// Forward declarations for database functions
int store_event(cJSON* event);
int broadcast_event_to_subscriptions(cJSON* event);
// Forward declarations for config functions
char* get_relay_private_key(void);
const char* get_config_value(const char* key);
int get_config_bool(const char* key, int default_value);
int update_config_in_table(const char* key, const char* value);
// Forward declaration for monitoring helper function
int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_func)(void));
// Handle HTTP request for embedded files (assumes GET)
int handle_embedded_file_request(struct lws* wsi, const char* requested_uri) {
const char* file_path;
// Handle /api requests
char temp_path[256];
if (strcmp(requested_uri, "/api") == 0) {
// /api -> serve index.html
file_path = "/";
} else if (strncmp(requested_uri, "/api/", 5) == 0) {
// Extract file path from /api/ prefix and add leading slash for lookup
snprintf(temp_path, sizeof(temp_path), "/%s", requested_uri + 5); // Add leading slash
file_path = temp_path;
} else {
DEBUG_WARN("Embedded file request without /api prefix");
lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
return -1;
}
// Get embedded file
embedded_file_t* file = get_embedded_file(file_path);
if (!file) {
DEBUG_WARN("Embedded file not found");
lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
return -1;
}
// Allocate session data
struct embedded_file_session_data* session_data = malloc(sizeof(struct embedded_file_session_data));
if (!session_data) {
DEBUG_ERROR("Failed to allocate embedded file session data");
return -1;
}
session_data->type = 1; // Embedded file
session_data->data = file->data;
session_data->size = file->size;
session_data->content_type = file->content_type;
session_data->headers_sent = 0;
session_data->body_sent = 0;
// Store session data
lws_set_wsi_user(wsi, session_data);
// Prepare HTTP response headers
unsigned char buf[LWS_PRE + 1024];
unsigned char *p = &buf[LWS_PRE];
unsigned char *start = p;
unsigned char *end = &buf[sizeof(buf) - 1];
if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end)) {
free(session_data);
return -1;
}
if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)file->content_type, strlen(file->content_type), &p, end)) {
free(session_data);
return -1;
}
if (lws_add_http_header_content_length(wsi, file->size, &p, end)) {
free(session_data);
return -1;
}
// Add CORS headers (same as NIP-11 for consistency)
if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-origin:", (unsigned char*)"*", 1, &p, end)) {
free(session_data);
return -1;
}
if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-headers:", (unsigned char*)"content-type, accept", 20, &p, end)) {
free(session_data);
return -1;
}
if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-methods:", (unsigned char*)"GET, OPTIONS", 12, &p, end)) {
free(session_data);
return -1;
}
// Add Connection: close to ensure connection closes after response
if (lws_add_http_header_by_name(wsi, (unsigned char*)"connection:", (unsigned char*)"close", 5, &p, end)) {
free(session_data);
return -1;
}
if (lws_finalize_http_header(wsi, &p, end)) {
free(session_data);
return -1;
}
// Write headers
if (lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS) < 0) {
free(session_data);
return -1;
}
session_data->headers_sent = 1;
// Request callback for body transmission
lws_callback_on_writable(wsi);
return 0;
}
// Send admin response with request ID correlation
int send_admin_response(const char* sender_pubkey, const char* response_content, const char* request_id,
char* error_message, size_t error_size, struct lws* wsi) {
(void)wsi; // Suppress unused parameter warning
if (!sender_pubkey || !response_content || !request_id || !error_message) {
if (error_message) {
strncpy(error_message, "Admin response: Invalid parameters", error_size - 1);
}
return -1;
}
// Get relay keys for signing
const char* relay_pubkey = get_config_value("relay_pubkey");
char* relay_privkey_hex = get_relay_private_key();
if (!relay_pubkey || !relay_privkey_hex) {
if (relay_privkey_hex) free(relay_privkey_hex);
strncpy(error_message, "Admin response: Could not get relay keys", error_size - 1);
return -1;
}
// Convert relay private key to bytes
unsigned char relay_privkey[32];
if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) {
free(relay_privkey_hex);
strncpy(error_message, "Admin response: Failed to convert relay private key", error_size - 1);
return -1;
}
free(relay_privkey_hex);
// Convert sender pubkey to bytes for NIP-44 encryption
unsigned char sender_pubkey_bytes[32];
if (nostr_hex_to_bytes(sender_pubkey, sender_pubkey_bytes, sizeof(sender_pubkey_bytes)) != 0) {
strncpy(error_message, "Admin response: Failed to convert sender pubkey", error_size - 1);
return -1;
}
// Encrypt response content using NIP-44
char encrypted_content[16384]; // Buffer for encrypted content (increased size)
int encrypt_result = nostr_nip44_encrypt(
relay_privkey, // sender private key (bytes)
sender_pubkey_bytes, // recipient public key (bytes)
response_content, // plaintext
encrypted_content, // output buffer
sizeof(encrypted_content) // output buffer size
);
if (encrypt_result != 0) {
strncpy(error_message, "Admin response: Failed to encrypt response content", error_size - 1);
return -1;
}
// Create response event (kind 23457)
cJSON* response_event = cJSON_CreateObject();
cJSON_AddStringToObject(response_event, "id", ""); // Will be set by signing
cJSON_AddStringToObject(response_event, "pubkey", relay_pubkey);
cJSON_AddNumberToObject(response_event, "created_at", (double)time(NULL));
cJSON_AddNumberToObject(response_event, "kind", 23457);
cJSON_AddStringToObject(response_event, "content", encrypted_content);
// Create tags array with p tag and e tag for request correlation
cJSON* tags = cJSON_CreateArray();
// p tag for recipient
cJSON* p_tag = cJSON_CreateArray();
cJSON_AddItemToArray(p_tag, cJSON_CreateString("p"));
cJSON_AddItemToArray(p_tag, cJSON_CreateString(sender_pubkey));
cJSON_AddItemToArray(tags, p_tag);
// e tag for request event correlation
cJSON* e_tag = cJSON_CreateArray();
cJSON_AddItemToArray(e_tag, cJSON_CreateString("e"));
cJSON_AddItemToArray(e_tag, cJSON_CreateString(request_id));
cJSON_AddItemToArray(tags, e_tag);
cJSON_AddItemToObject(response_event, "tags", tags);
// Use the library function to create and sign the event
cJSON* signed_event = nostr_create_and_sign_event(
23457, // kind
cJSON_GetStringValue(cJSON_GetObjectItem(response_event, "content")), // content
tags, // tags
relay_privkey, // private key
(time_t)cJSON_GetNumberValue(cJSON_GetObjectItem(response_event, "created_at")) // timestamp
);
if (!signed_event) {
cJSON_Delete(response_event);
strncpy(error_message, "Admin response: Failed to create and sign event", error_size - 1);
return -1;
}
// Replace the unsigned event with the signed one
cJSON_Delete(response_event);
response_event = signed_event;
// Broadcast the event
broadcast_event_to_subscriptions(response_event);
// Store in database
int store_result = store_event(response_event);
cJSON_Delete(response_event);
if (store_result != 0) {
strncpy(error_message, "Admin response: Failed to store response event", error_size - 1);
return -1;
}
return 0;
}
// =============================================================================
// SQL QUERY ADMIN FUNCTIONS
// =============================================================================
// Validate SQL query for security and safety
int validate_sql_query(const char* query, char* error_message, size_t error_size) {
if (!query || !error_message) {
return 0;
}
// Convert query to uppercase for case-insensitive keyword checking
char query_upper[4096];
size_t query_len = strlen(query);
if (query_len >= sizeof(query_upper)) {
snprintf(error_message, error_size, "Query too long (max %zu characters)", sizeof(query_upper) - 1);
return 0;
}
// Copy and convert to uppercase
for (size_t i = 0; i < query_len; i++) {
query_upper[i] = (query[i] >= 'a' && query[i] <= 'z') ? query[i] - 32 : query[i];
}
query_upper[query_len] = '\0';
// List of blocked keywords (case-insensitive)
const char* blocked_keywords[] = {
"INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER", "TRUNCATE",
"EXEC", "EXECUTE", "MERGE", "BULK", "BACKUP", "RESTORE",
"GRANT", "REVOKE", "DENY", "COMMIT", "ROLLBACK", "SAVEPOINT",
"SHUTDOWN", "PRAGMA", "VACUUM", "REINDEX", "ANALYZE",
NULL // Sentinel
};
// Check for blocked keywords
for (int i = 0; blocked_keywords[i] != NULL; i++) {
char keyword_pattern[64];
snprintf(keyword_pattern, sizeof(keyword_pattern), " %s ", blocked_keywords[i]);
if (strstr(query_upper, keyword_pattern) != NULL) {
snprintf(error_message, error_size, "Query blocked: %s statements not allowed", blocked_keywords[i]);
return 0;
}
// Also check at start of query
if (strncmp(query_upper, blocked_keywords[i], strlen(blocked_keywords[i])) == 0) {
snprintf(error_message, error_size, "Query blocked: %s statements not allowed", blocked_keywords[i]);
return 0;
}
}
// Check for SELECT keyword (must be present)
if (strstr(query_upper, " SELECT ") == NULL && strncmp(query_upper, "SELECT ", 7) != 0) {
// Allow WITH clauses (CTEs) which can start queries
if (strstr(query_upper, " WITH ") == NULL && strncmp(query_upper, "WITH ", 5) != 0) {
snprintf(error_message, error_size, "Query blocked: Only SELECT statements and WITH clauses are allowed");
return 0;
}
}
// Basic length check
if (query_len < 6) { // Minimum "SELECT" length
snprintf(error_message, error_size, "Query too short");
return 0;
}
return 1; // Query passed validation
}
// Execute SQL query with safety limits
char* execute_sql_query(const char* query, const char* request_id, char* error_message, size_t error_size) {
if (!query || !request_id || !error_message) {
return NULL;
}
if (!g_db) {
snprintf(error_message, error_size, "Database not available");
return NULL;
}
// Set busy timeout to prevent long-running queries (5 seconds)
sqlite3_busy_timeout(g_db, 5000);
// Prepare statement
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, query, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
const char* err_msg = sqlite3_errmsg(g_db);
snprintf(error_message, error_size, "SQL prepare failed: %s", err_msg);
return NULL;
}
// Execute query and collect results
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", "sql_query");
cJSON_AddStringToObject(response, "request_id", request_id);
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
cJSON_AddStringToObject(response, "query", query);
// Get column information
int col_count = sqlite3_column_count(stmt);
cJSON* columns = cJSON_CreateArray();
for (int i = 0; i < col_count; i++) {
const char* col_name = sqlite3_column_name(stmt, i);
cJSON_AddItemToArray(columns, cJSON_CreateString(col_name ? col_name : ""));
}
cJSON_AddItemToObject(response, "columns", columns);
// Execute and collect rows (with limit)
cJSON* rows = cJSON_CreateArray();
int row_count = 0;
const int MAX_ROWS = 1000; // Configurable limit
struct timespec start_time;
clock_gettime(CLOCK_MONOTONIC, &start_time);
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW && row_count < MAX_ROWS) {
cJSON* row = cJSON_CreateArray();
for (int i = 0; i < col_count; i++) {
int col_type = sqlite3_column_type(stmt, i);
switch (col_type) {
case SQLITE_INTEGER:
cJSON_AddItemToArray(row, cJSON_CreateNumber((double)sqlite3_column_int64(stmt, i)));
break;
case SQLITE_FLOAT:
cJSON_AddItemToArray(row, cJSON_CreateNumber(sqlite3_column_double(stmt, i)));
break;
case SQLITE_TEXT: {
const char* text = (const char*)sqlite3_column_text(stmt, i);
cJSON_AddItemToArray(row, cJSON_CreateString(text ? text : ""));
break;
}
case SQLITE_BLOB: {
// Convert blob to hex string for JSON compatibility
const void* blob = sqlite3_column_blob(stmt, i);
int blob_size = sqlite3_column_bytes(stmt, i);
if (blob && blob_size > 0) {
char* hex_str = malloc(blob_size * 2 + 1);
if (hex_str) {
for (int j = 0; j < blob_size; j++) {
sprintf(hex_str + j * 2, "%02x", ((unsigned char*)blob)[j]);
}
hex_str[blob_size * 2] = '\0';
cJSON_AddItemToArray(row, cJSON_CreateString(hex_str));
free(hex_str);
} else {
cJSON_AddItemToArray(row, cJSON_CreateString("[BLOB]"));
}
} else {
cJSON_AddItemToArray(row, cJSON_CreateString(""));
}
break;
}
case SQLITE_NULL:
cJSON_AddItemToArray(row, cJSON_CreateNull());
break;
default:
cJSON_AddItemToArray(row, cJSON_CreateString("[UNKNOWN]"));
break;
}
}
cJSON_AddItemToArray(rows, row);
row_count++;
// Check timeout (additional safety check)
struct timespec current_time;
clock_gettime(CLOCK_MONOTONIC, &current_time);
double elapsed = (current_time.tv_sec - start_time.tv_sec) +
(current_time.tv_nsec - start_time.tv_nsec) / 1e9;
if (elapsed > 4.5) { // 4.5 seconds to allow for cleanup
break;
}
}
sqlite3_finalize(stmt);
// Check for execution errors
if (rc != SQLITE_DONE && rc != SQLITE_ROW) {
const char* err_msg = sqlite3_errmsg(g_db);
snprintf(error_message, error_size, "SQL execution failed: %s", err_msg);
cJSON_Delete(response);
return NULL;
}
// Check row limit
if (row_count >= MAX_ROWS) {
cJSON_AddStringToObject(response, "warning", "Result truncated to maximum row limit");
}
// Add metadata
cJSON_AddNumberToObject(response, "row_count", row_count);
cJSON_AddNumberToObject(response, "execution_time_ms", 0); // Will be set by caller
cJSON_AddItemToObject(response, "rows", rows);
// Convert to JSON string
char* json_result = cJSON_Print(response);
cJSON_Delete(response);
if (!json_result) {
snprintf(error_message, error_size, "Failed to generate JSON response");
return NULL;
}
return json_result;
}
// Unified handler for SQL query commands
int handle_sql_query_unified(cJSON* event, const char* query, char* error_message, size_t error_size, struct lws* wsi) {
if (!event || !query || !error_message) {
return -1;
}
// Get request event ID for response correlation
cJSON* request_id_obj = cJSON_GetObjectItem(event, "id");
if (!request_id_obj || !cJSON_IsString(request_id_obj)) {
snprintf(error_message, error_size, "Missing request event ID");
return -1;
}
const char* request_id = cJSON_GetStringValue(request_id_obj);
// Validate query
if (!validate_sql_query(query, error_message, error_size)) {
return -1;
}
// Execute query
char* result_json = execute_sql_query(query, request_id, error_message, error_size);
if (!result_json) {
return -1;
}
// Get sender pubkey for response
cJSON* sender_pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
if (!sender_pubkey_obj || !cJSON_IsString(sender_pubkey_obj)) {
free(result_json);
snprintf(error_message, error_size, "Missing sender pubkey");
return -1;
}
const char* sender_pubkey = cJSON_GetStringValue(sender_pubkey_obj);
// Send response as kind 23457 event with request ID in tags
int send_result = send_admin_response(sender_pubkey, result_json, request_id, error_message, error_size, wsi);
free(result_json);
return send_result;
}
// Handle HTTP_WRITEABLE for embedded files
int handle_embedded_file_writeable(struct lws* wsi) {
struct embedded_file_session_data* session_data = (struct embedded_file_session_data*)lws_wsi_user(wsi);
if (!session_data || session_data->headers_sent == 0 || session_data->body_sent == 1) {
return 0;
}
// Allocate buffer for data transmission
unsigned char *buf = malloc(LWS_PRE + session_data->size);
if (!buf) {
DEBUG_ERROR("Failed to allocate buffer for embedded file transmission");
free(session_data);
lws_set_wsi_user(wsi, NULL);
return -1;
}
// Copy data to buffer
memcpy(buf + LWS_PRE, session_data->data, session_data->size);
// Write data
int write_result = lws_write(wsi, buf + LWS_PRE, session_data->size, LWS_WRITE_HTTP);
// Free the transmission buffer
free(buf);
if (write_result < 0) {
DEBUG_ERROR("Failed to write embedded file data");
free(session_data);
lws_set_wsi_user(wsi, NULL);
return -1;
}
// Mark as sent and clean up
session_data->body_sent = 1;
free(session_data);
lws_set_wsi_user(wsi, NULL);
return 0;
}
// Query CPU usage metrics
cJSON* query_cpu_metrics(void) {
cJSON* cpu_stats = cJSON_CreateObject();
cJSON_AddStringToObject(cpu_stats, "data_type", "cpu_metrics");
cJSON_AddNumberToObject(cpu_stats, "timestamp", (double)time(NULL));
// Read process CPU times from /proc/self/stat
FILE* proc_stat = fopen("/proc/self/stat", "r");
if (proc_stat) {
unsigned long utime, stime; // user and system CPU time in clock ticks
if (fscanf(proc_stat, "%*d %*s %*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %lu %lu", &utime, &stime) == 2) {
unsigned long total_proc_time = utime + stime;
// Get system CPU times from /proc/stat
FILE* sys_stat = fopen("/proc/stat", "r");
if (sys_stat) {
unsigned long user, nice, system, idle, iowait, irq, softirq;
if (fscanf(sys_stat, "cpu %lu %lu %lu %lu %lu %lu %lu", &user, &nice, &system, &idle, &iowait, &irq, &softirq) == 7) {
unsigned long total_sys_time = user + nice + system + idle + iowait + irq + softirq;
// Calculate CPU percentages (simplified - would need deltas for accuracy)
// For now, just store the raw values - frontend can calculate deltas
cJSON_AddNumberToObject(cpu_stats, "process_cpu_time", (double)total_proc_time);
cJSON_AddNumberToObject(cpu_stats, "system_cpu_time", (double)total_sys_time);
cJSON_AddNumberToObject(cpu_stats, "system_idle_time", (double)idle);
}
fclose(sys_stat);
}
// Get current CPU core the process is running on
int current_core = sched_getcpu();
if (current_core >= 0) {
cJSON_AddNumberToObject(cpu_stats, "current_cpu_core", current_core);
}
}
fclose(proc_stat);
}
// Get process ID
pid_t pid = getpid();
cJSON_AddNumberToObject(cpu_stats, "process_id", (double)pid);
// Get memory usage from /proc/self/status
FILE* mem_stat = fopen("/proc/self/status", "r");
if (mem_stat) {
char line[256];
while (fgets(line, sizeof(line), mem_stat)) {
if (strncmp(line, "VmRSS:", 6) == 0) {
unsigned long rss_kb;
if (sscanf(line, "VmRSS: %lu kB", &rss_kb) == 1) {
double rss_mb = rss_kb / 1024.0;
cJSON_AddNumberToObject(cpu_stats, "memory_usage_mb", rss_mb);
}
break;
}
}
fclose(mem_stat);
}
return cpu_stats;
}
// Generate stats JSON from database queries
char* generate_stats_json(void) {
extern sqlite3* g_db;
if (!g_db) {
DEBUG_ERROR("Database not available for stats generation");
return NULL;
}
// Build response with database statistics
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", "stats_query");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
// Get database file size
extern char g_database_path[512];
struct stat db_stat;
long long db_size = 0;
if (stat(g_database_path, &db_stat) == 0) {
db_size = db_stat.st_size;
}
cJSON_AddNumberToObject(response, "database_size_bytes", db_size);
// Get active subscriptions count from in-memory manager
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
int active_subs = g_subscription_manager.total_subscriptions;
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
cJSON_AddNumberToObject(response, "active_subscriptions", active_subs);
// Query total events count
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM events", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
cJSON_AddNumberToObject(response, "total_events", sqlite3_column_int64(stmt, 0));
}
sqlite3_finalize(stmt);
}
// Query event kinds distribution
cJSON* event_kinds = cJSON_CreateArray();
if (sqlite3_prepare_v2(g_db, "SELECT kind, count, percentage FROM event_kinds_view ORDER BY count DESC", -1, &stmt, NULL) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
cJSON* kind_obj = cJSON_CreateObject();
cJSON_AddNumberToObject(kind_obj, "kind", sqlite3_column_int(stmt, 0));
cJSON_AddNumberToObject(kind_obj, "count", sqlite3_column_int64(stmt, 1));
cJSON_AddNumberToObject(kind_obj, "percentage", sqlite3_column_double(stmt, 2));
cJSON_AddItemToArray(event_kinds, kind_obj);
}
sqlite3_finalize(stmt);
}
cJSON_AddItemToObject(response, "event_kinds", event_kinds);
// Query time-based statistics
cJSON* time_stats = cJSON_CreateObject();
if (sqlite3_prepare_v2(g_db, "SELECT period, total_events FROM time_stats_view", -1, &stmt, NULL) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* period = (const char*)sqlite3_column_text(stmt, 0);
sqlite3_int64 count = sqlite3_column_int64(stmt, 1);
if (strcmp(period, "total") == 0) {
cJSON_AddNumberToObject(time_stats, "total", count);
} else if (strcmp(period, "24h") == 0) {
cJSON_AddNumberToObject(time_stats, "last_24h", count);
} else if (strcmp(period, "7d") == 0) {
cJSON_AddNumberToObject(time_stats, "last_7d", count);
} else if (strcmp(period, "30d") == 0) {
cJSON_AddNumberToObject(time_stats, "last_30d", count);
}
}
sqlite3_finalize(stmt);
}
cJSON_AddItemToObject(response, "time_stats", time_stats);
// Query top pubkeys
cJSON* top_pubkeys = cJSON_CreateArray();
if (sqlite3_prepare_v2(g_db, "SELECT pubkey, event_count, percentage FROM top_pubkeys_view ORDER BY event_count DESC LIMIT 10", -1, &stmt, NULL) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
cJSON* pubkey_obj = cJSON_CreateObject();
const char* pubkey = (const char*)sqlite3_column_text(stmt, 0);
cJSON_AddStringToObject(pubkey_obj, "pubkey", pubkey ? pubkey : "");
cJSON_AddNumberToObject(pubkey_obj, "event_count", sqlite3_column_int64(stmt, 1));
cJSON_AddNumberToObject(pubkey_obj, "percentage", sqlite3_column_double(stmt, 2));
cJSON_AddItemToArray(top_pubkeys, pubkey_obj);
}
sqlite3_finalize(stmt);
}
cJSON_AddItemToObject(response, "top_pubkeys", top_pubkeys);
// Get database creation timestamp (oldest event)
if (sqlite3_prepare_v2(g_db, "SELECT MIN(created_at) FROM events", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
sqlite3_int64 oldest_timestamp = sqlite3_column_int64(stmt, 0);
if (oldest_timestamp > 0) {
cJSON_AddNumberToObject(response, "database_created_at", (double)oldest_timestamp);
}
}
sqlite3_finalize(stmt);
}
// Get latest event timestamp
if (sqlite3_prepare_v2(g_db, "SELECT MAX(created_at) FROM events", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
sqlite3_int64 latest_timestamp = sqlite3_column_int64(stmt, 0);
if (latest_timestamp > 0) {
cJSON_AddNumberToObject(response, "latest_event_at", (double)latest_timestamp);
}
}
sqlite3_finalize(stmt);
}
// Convert to JSON string
char* json_string = cJSON_Print(response);
cJSON_Delete(response);
if (!json_string) {
DEBUG_ERROR("Failed to generate stats JSON");
}
return json_string;
}
// Unified NIP-17 response sender - handles all common response logic
int send_nip17_response(const char* sender_pubkey, const char* response_content,
char* error_message, size_t error_size) {
if (!sender_pubkey || !response_content || !error_message) {
if (error_message) {
strncpy(error_message, "NIP-17: Invalid parameters for response", error_size - 1);
}
return -1;
}
// Get relay keys for signing
const char* relay_pubkey = get_config_value("relay_pubkey");
char* relay_privkey_hex = get_relay_private_key();
if (!relay_pubkey || !relay_privkey_hex) {
if (relay_privkey_hex) free(relay_privkey_hex);
strncpy(error_message, "NIP-17: Could not get relay keys for response", error_size - 1);
return -1;
}
// Convert relay private key to bytes
unsigned char relay_privkey[32];
if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) {
free(relay_privkey_hex);
strncpy(error_message, "NIP-17: Failed to convert relay private key for response", error_size - 1);
return -1;
}
free(relay_privkey_hex);
// Create DM response event using library function
cJSON* dm_response = nostr_nip17_create_chat_event(
response_content, // message content
(const char**)&sender_pubkey, // recipient pubkeys
1, // num recipients
NULL, // subject (optional)
NULL, // reply_to_event_id (optional)
NULL, // reply_relay_url (optional)
relay_pubkey // sender pubkey
);
if (!dm_response) {
strncpy(error_message, "NIP-17: Failed to create DM response event", error_size - 1);
return -1;
}
// Create and sign gift wrap using library function
cJSON* gift_wraps[1];
int send_result = nostr_nip17_send_dm(
dm_response, // dm_event
(const char**)&sender_pubkey, // recipient_pubkeys
1, // num_recipients
relay_privkey, // sender_private_key
gift_wraps, // gift_wraps_out
1 // max_gift_wraps
);
cJSON_Delete(dm_response);
if (send_result != 1 || !gift_wraps[0]) {
strncpy(error_message, "NIP-17: Failed to create and sign response gift wrap", error_size - 1);
return -1;
}
// Fix the p tag in the gift wrap - library function may use wrong pubkey
cJSON* gift_wrap_tags = cJSON_GetObjectItem(gift_wraps[0], "tags");
if (gift_wrap_tags && cJSON_IsArray(gift_wrap_tags)) {
// Find and replace the p tag with the correct user pubkey
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, gift_wrap_tags) {
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
if (tag_name && cJSON_IsString(tag_name) &&
strcmp(cJSON_GetStringValue(tag_name), "p") == 0) {
// Replace the p tag value with the correct user pubkey
cJSON_ReplaceItemInArray(tag, 1, cJSON_CreateString(sender_pubkey));
break;
}
}
}
}
// Broadcast FIRST before storing (broadcasting needs the event intact)
// Make a copy for broadcasting to avoid use-after-free issues
cJSON* gift_wrap_copy = cJSON_Duplicate(gift_wraps[0], 1);
if (!gift_wrap_copy) {
cJSON_Delete(gift_wraps[0]);
strncpy(error_message, "NIP-17: Failed to duplicate gift wrap for broadcast", error_size - 1);
return -1;
}
// Broadcast the copy to active subscriptions
broadcast_event_to_subscriptions(gift_wrap_copy);
// Store the original in database
int store_result = store_event(gift_wraps[0]);
// Clean up both copies
cJSON_Delete(gift_wrap_copy);
cJSON_Delete(gift_wraps[0]);
if (store_result != 0) {
strncpy(error_message, "NIP-17: Failed to store response gift wrap", error_size - 1);
return -1;
}
return 0;
}
// Generate config text from database
char* generate_config_text(void) {
extern sqlite3* g_db;
if (!g_db) {
DEBUG_ERROR("NIP-17: Database not available for config query");
return NULL;
}
// Build comprehensive config text from database
char* config_text = malloc(8192);
if (!config_text) {
DEBUG_ERROR("NIP-17: Failed to allocate memory for config text");
return NULL;
}
int offset = 0;
// Header
offset += snprintf(config_text + offset, 8192 - offset,
"🔧 Relay Configuration\n"
"━━━━━━━━━━━━━━━━━━━━━━━━\n");
// Query all config values from database
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(g_db, "SELECT key, value FROM config ORDER BY key", -1, &stmt, NULL) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW && offset < 8192 - 200) {
const char* key = (const char*)sqlite3_column_text(stmt, 0);
const char* value = (const char*)sqlite3_column_text(stmt, 1);
if (key && value) {
offset += snprintf(config_text + offset, 8192 - offset,
"%s: %s\n", key, value);
}
}
sqlite3_finalize(stmt);
} else {
free(config_text);
DEBUG_ERROR("NIP-17: Failed to query config from database");
return NULL;
}
// Footer
offset += snprintf(config_text + offset, 8192 - offset,
"\n✅ Configuration retrieved successfully");
return config_text;
}
// Generate human-readable stats text
char* generate_stats_text(void) {
char* stats_json = generate_stats_json();
if (!stats_json) {
DEBUG_ERROR("NIP-17: Failed to generate stats for plain text command");
return NULL;
}
// Parse the JSON to extract values for human-readable format
cJSON* stats_obj = cJSON_Parse(stats_json);
char* stats_text = malloc(16384); // Increased buffer size for comprehensive stats
if (!stats_text) {
free(stats_json);
if (stats_obj) cJSON_Delete(stats_obj);
return NULL;
}
if (stats_obj) {
// Extract basic metrics
cJSON* total_events = cJSON_GetObjectItem(stats_obj, "total_events");
cJSON* db_size = cJSON_GetObjectItem(stats_obj, "database_size_bytes");
cJSON* oldest_event = cJSON_GetObjectItem(stats_obj, "database_created_at");
cJSON* newest_event = cJSON_GetObjectItem(stats_obj, "latest_event_at");
cJSON* time_stats = cJSON_GetObjectItem(stats_obj, "time_stats");
cJSON* event_kinds = cJSON_GetObjectItem(stats_obj, "event_kinds");
cJSON* top_pubkeys = cJSON_GetObjectItem(stats_obj, "top_pubkeys");
long long total = total_events ? (long long)cJSON_GetNumberValue(total_events) : 0;
long long db_bytes = db_size ? (long long)cJSON_GetNumberValue(db_size) : 0;
double db_mb = db_bytes / (1024.0 * 1024.0);
// Get active subscriptions count from in-memory manager
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
int active_subs = g_subscription_manager.total_subscriptions;
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
// Format timestamps
char oldest_str[64] = "-";
char newest_str[64] = "-";
if (oldest_event && cJSON_GetNumberValue(oldest_event) > 0) {
time_t oldest_ts = (time_t)cJSON_GetNumberValue(oldest_event);
struct tm* tm_info = localtime(&oldest_ts);
strftime(oldest_str, sizeof(oldest_str), "%m/%d/%Y, %I:%M:%S %p", tm_info);
}
if (newest_event && cJSON_GetNumberValue(newest_event) > 0) {
time_t newest_ts = (time_t)cJSON_GetNumberValue(newest_event);
struct tm* tm_info = localtime(&newest_ts);
strftime(newest_str, sizeof(newest_str), "%m/%d/%Y, %I:%M:%S %p", tm_info);
}
// Extract time-based stats
long long last_24h = 0, last_7d = 0, last_30d = 0;
if (time_stats) {
cJSON* h24 = cJSON_GetObjectItem(time_stats, "last_24h");
cJSON* d7 = cJSON_GetObjectItem(time_stats, "last_7d");
cJSON* d30 = cJSON_GetObjectItem(time_stats, "last_30d");
last_24h = h24 ? (long long)cJSON_GetNumberValue(h24) : 0;
last_7d = d7 ? (long long)cJSON_GetNumberValue(d7) : 0;
last_30d = d30 ? (long long)cJSON_GetNumberValue(d30) : 0;
}
// Start building the comprehensive stats text
int offset = 0;
// Header
offset += snprintf(stats_text + offset, 16384 - offset,
"📊 Relay Statistics\n"
"━━━━━━━━━━━━━━━━━━━━\n");
// Database Overview section
offset += snprintf(stats_text + offset, 16384 - offset,
"Database Overview:\n"
"Metric\tValue\tDescription\n"
"Database Size\t%.2f MB (%lld bytes)\tCurrent database file size\n"
"Total Events\t%lld\tTotal number of events stored\n"
"Active Subscriptions\t%d\tCurrent active WebSocket subscriptions\n"
"Oldest Event\t%s\tTimestamp of oldest event\n"
"Newest Event\t%s\tTimestamp of newest event\n"
"\n",
db_mb, db_bytes, total, active_subs, oldest_str, newest_str);
// Event Kind Distribution section
offset += snprintf(stats_text + offset, 16384 - offset,
"Event Kind Distribution:\n"
"Event Kind\tCount\tPercentage\n");
if (event_kinds && cJSON_IsArray(event_kinds)) {
cJSON* kind_item = NULL;
cJSON_ArrayForEach(kind_item, event_kinds) {
cJSON* kind = cJSON_GetObjectItem(kind_item, "kind");
cJSON* count = cJSON_GetObjectItem(kind_item, "count");
cJSON* percentage = cJSON_GetObjectItem(kind_item, "percentage");
if (kind && count && percentage) {
offset += snprintf(stats_text + offset, 16384 - offset,
"%lld\t%lld\t%.1f%%\n",
(long long)cJSON_GetNumberValue(kind),
(long long)cJSON_GetNumberValue(count),
cJSON_GetNumberValue(percentage));
}
}
} else {
offset += snprintf(stats_text + offset, 16384 - offset,
"No event data available\n");
}
offset += snprintf(stats_text + offset, 16384 - offset, "\n");
// Time-based Statistics section
offset += snprintf(stats_text + offset, 16384 - offset,
"Time-based Statistics:\n"
"Period\tEvents\tDescription\n"
"Last 24 Hours\t%lld\tEvents in the last day\n"
"Last 7 Days\t%lld\tEvents in the last week\n"
"Last 30 Days\t%lld\tEvents in the last month\n"
"\n",
last_24h, last_7d, last_30d);
// Top Pubkeys section
offset += snprintf(stats_text + offset, 16384 - offset,
"Top Pubkeys by Event Count:\n"
"Rank\tPubkey\tEvent Count\tPercentage\n");
if (top_pubkeys && cJSON_IsArray(top_pubkeys)) {
int rank = 1;
cJSON* pubkey_item = NULL;
cJSON_ArrayForEach(pubkey_item, top_pubkeys) {
cJSON* pubkey = cJSON_GetObjectItem(pubkey_item, "pubkey");
cJSON* event_count = cJSON_GetObjectItem(pubkey_item, "event_count");
cJSON* percentage = cJSON_GetObjectItem(pubkey_item, "percentage");
if (pubkey && event_count && percentage) {
const char* pubkey_str = cJSON_GetStringValue(pubkey);
char short_pubkey[20] = "...";
if (pubkey_str && strlen(pubkey_str) >= 16) {
snprintf(short_pubkey, sizeof(short_pubkey), "%.16s...", pubkey_str);
}
offset += snprintf(stats_text + offset, 16384 - offset,
"%d\t%s\t%lld\t%.1f%%\n",
rank++,
short_pubkey,
(long long)cJSON_GetNumberValue(event_count),
cJSON_GetNumberValue(percentage));
}
}
} else {
offset += snprintf(stats_text + offset, 16384 - offset,
"No pubkey data available\n");
}
// Footer
offset += snprintf(stats_text + offset, 16384 - offset,
"\n✅ Statistics retrieved successfully");
cJSON_Delete(stats_obj);
} else {
// Fallback if JSON parsing fails
snprintf(stats_text, 16384,
"📊 Relay Statistics\n"
"━━━━━━━━━━━━━━━━━━━━\n"
"Raw data: %s\n"
"\n"
"⚠️ Could not parse statistics data",
stats_json
);
}
free(stats_json);
return stats_text;
}
// =============================================================================
// CONFIGURATION CHANGE IMPLEMENTATION
// =============================================================================
// Parse configuration command from natural language
// Supports patterns like:
// - "auth_enabled true"
// - "set auth_enabled to true"
// - "change auth_enabled true"
// - "auth_enabled = true"
// - "auth_enabled: true"
// - "enable auth" / "disable auth"
int parse_config_command(const char* message, char* key, char* value) {
if (!message || !key || !value) {
return 0;
}
// Clean up the message - convert to lowercase and trim
char clean_msg[512];
size_t msg_len = strlen(message);
size_t copy_len = msg_len < sizeof(clean_msg) - 1 ? msg_len : sizeof(clean_msg) - 1;
memcpy(clean_msg, message, copy_len);
clean_msg[copy_len] = '\0';
// Convert to lowercase
for (size_t i = 0; i < copy_len; i++) {
if (clean_msg[i] >= 'A' && clean_msg[i] <= 'Z') {
clean_msg[i] = clean_msg[i] + 32;
}
}
// Remove leading/trailing whitespace
char* start = clean_msg;
while (*start == ' ' || *start == '\t') start++;
char* end = start + strlen(start) - 1;
while (end > start && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) {
*end = '\0';
end--;
}
// Pattern 1: "enable auth" -> "auth_enabled true"
if (strstr(start, "enable auth") == start) {
strcpy(key, "auth_enabled");
strcpy(value, "true");
return 1;
}
// Pattern 2: "disable auth" -> "auth_enabled false"
if (strstr(start, "disable auth") == start) {
strcpy(key, "auth_enabled");
strcpy(value, "false");
return 1;
}
// Pattern 3: "enable nip42" -> "nip42_auth_required true"
if (strstr(start, "enable nip42") == start) {
strcpy(key, "nip42_auth_required");
strcpy(value, "true");
return 1;
}
// Pattern 4: "disable nip42" -> "nip42_auth_required false"
if (strstr(start, "disable nip42") == start) {
strcpy(key, "nip42_auth_required");
strcpy(value, "false");
return 1;
}
// Pattern 5: "set KEY to VALUE" or "change KEY to VALUE"
char* set_pos = strstr(start, "set ");
char* change_pos = strstr(start, "change ");
char* to_pos = strstr(start, " to ");
if ((set_pos == start || change_pos == start) && to_pos) {
char* key_start = (set_pos == start) ? start + 4 : start + 7;
size_t key_len = to_pos - key_start;
if (key_len > 0 && key_len < 127) {
memcpy(key, key_start, key_len);
key[key_len] = '\0';
// Trim key
char* key_end = key + strlen(key) - 1;
while (key_end > key && (*key_end == ' ' || *key_end == '\t')) {
*key_end = '\0';
key_end--;
}
char* value_start = to_pos + 4;
strcpy(value, value_start);
return 1;
}
}
// Pattern 6: "KEY = VALUE"
char* equals_pos = strstr(start, " = ");
if (equals_pos) {
size_t key_len = equals_pos - start;
if (key_len > 0 && key_len < 127) {
memcpy(key, start, key_len);
key[key_len] = '\0';
strcpy(value, equals_pos + 3);
return 1;
}
}
// Pattern 7: "KEY: VALUE" (colon-separated)
char* colon_pos = strstr(start, ": ");
if (colon_pos) {
size_t key_len = colon_pos - start;
if (key_len > 0 && key_len < 127) {
memcpy(key, start, key_len);
key[key_len] = '\0';
strcpy(value, colon_pos + 2);
return 1;
}
}
// Pattern 8: "KEY VALUE" (simple space-separated)
char* space_pos = strchr(start, ' ');
if (space_pos) {
size_t key_len = space_pos - start;
if (key_len > 0 && key_len < 127) {
memcpy(key, start, key_len);
key[key_len] = '\0';
strcpy(value, space_pos + 1);
return 1;
}
}
return 0; // No pattern matched
}
// Validate configuration key and value
int validate_config_change(const char* key, const char* value) {
if (!key || !value) {
return 0;
}
// Find the configuration key
int found = 0;
const char* expected_type = NULL;
int min_val = 0, max_val = 0;
for (int i = 0; known_configs[i].key != NULL; i++) {
if (strcmp(key, known_configs[i].key) == 0) {
found = 1;
expected_type = known_configs[i].type;
min_val = known_configs[i].min_val;
max_val = known_configs[i].max_val;
break;
}
}
if (!found) {
return 0; // Unknown configuration key
}
// Validate value based on type
if (strcmp(expected_type, "bool") == 0) {
if (strcmp(value, "true") == 0 || strcmp(value, "false") == 0 ||
strcmp(value, "1") == 0 || strcmp(value, "0") == 0 ||
strcmp(value, "yes") == 0 || strcmp(value, "no") == 0 ||
strcmp(value, "on") == 0 || strcmp(value, "off") == 0) {
return 1;
}
return 0;
} else if (strcmp(expected_type, "int") == 0) {
char* endptr;
long val = strtol(value, &endptr, 10);
if (*endptr != '\0') {
return 0; // Not a valid integer
}
if (val < min_val || val > max_val) {
return 0; // Out of range
}
return 1;
} else if (strcmp(expected_type, "string") == 0) {
// String values are generally valid, but check length
if (strlen(value) > 255) {
return 0; // Too long
}
return 1;
}
return 0;
}
// Generate a unique change ID based on admin pubkey and timestamp
void generate_change_id(const char* admin_pubkey, char* change_id) {
char input[128];
snprintf(input, sizeof(input), "%s_%ld", admin_pubkey, time(NULL));
// Simple hash - just use first 32 chars of the input
size_t input_len = strlen(input);
for (int i = 0; i < 32 && i < (int)input_len; i++) {
change_id[i] = input[i];
}
change_id[32] = '\0';
}
// Store a pending configuration change
char* store_pending_config_change(const char* admin_pubkey, const char* key,
const char* old_value, const char* new_value) {
if (!admin_pubkey || !key || !old_value || !new_value) {
return NULL;
}
// Clean up expired changes first
cleanup_expired_pending_changes();
// Create new pending change
pending_config_change_t* change = malloc(sizeof(pending_config_change_t));
if (!change) {
return NULL;
}
strncpy(change->admin_pubkey, admin_pubkey, sizeof(change->admin_pubkey) - 1);
change->admin_pubkey[sizeof(change->admin_pubkey) - 1] = '\0';
strncpy(change->config_key, key, sizeof(change->config_key) - 1);
change->config_key[sizeof(change->config_key) - 1] = '\0';
strncpy(change->old_value, old_value, sizeof(change->old_value) - 1);
change->old_value[sizeof(change->old_value) - 1] = '\0';
strncpy(change->new_value, new_value, sizeof(change->new_value) - 1);
change->new_value[sizeof(change->new_value) - 1] = '\0';
change->timestamp = time(NULL);
generate_change_id(admin_pubkey, change->change_id);
// Add to linked list
change->next = pending_changes_head;
pending_changes_head = change;
pending_changes_count++;
// Return a copy of the change ID
char* change_id_copy = malloc(33);
if (change_id_copy) {
strcpy(change_id_copy, change->change_id);
}
return change_id_copy;
}
// Find a pending change by admin pubkey and change ID
pending_config_change_t* find_pending_change(const char* admin_pubkey, const char* change_id) {
if (!admin_pubkey) {
return NULL;
}
pending_config_change_t* current = pending_changes_head;
while (current) {
if (strcmp(current->admin_pubkey, admin_pubkey) == 0) {
if (!change_id || strcmp(current->change_id, change_id) == 0) {
return current;
}
}
current = current->next;
}
return NULL;
}
// Find the most recent pending change for an admin
pending_config_change_t* find_latest_pending_change(const char* admin_pubkey) {
if (!admin_pubkey) {
return NULL;
}
pending_config_change_t* latest = NULL;
pending_config_change_t* current = pending_changes_head;
while (current) {
if (strcmp(current->admin_pubkey, admin_pubkey) == 0) {
if (!latest || current->timestamp > latest->timestamp) {
latest = current;
}
}
current = current->next;
}
return latest;
}
// Remove a pending change from the list
void remove_pending_change(pending_config_change_t* change_to_remove) {
if (!change_to_remove) {
return;
}
if (pending_changes_head == change_to_remove) {
pending_changes_head = change_to_remove->next;
} else {
pending_config_change_t* current = pending_changes_head;
while (current && current->next != change_to_remove) {
current = current->next;
}
if (current) {
current->next = change_to_remove->next;
}
}
free(change_to_remove);
pending_changes_count--;
}
// Clean up expired pending changes (older than 5 minutes)
void cleanup_expired_pending_changes(void) {
time_t now = time(NULL);
pending_config_change_t* current = pending_changes_head;
while (current) {
pending_config_change_t* next = current->next;
if (now - current->timestamp > CONFIG_CHANGE_TIMEOUT) {
remove_pending_change(current);
}
current = next;
}
}
// Apply a configuration change to the database
int apply_config_change(const char* key, const char* value) {
if (!key || !value) {
return -1;
}
extern sqlite3* g_db;
if (!g_db) {
DEBUG_ERROR("Database not available for config change");
return -1;
}
// Normalize boolean values
char normalized_value[256];
strncpy(normalized_value, value, sizeof(normalized_value) - 1);
normalized_value[sizeof(normalized_value) - 1] = '\0';
// Convert various boolean representations to "true"/"false"
if (strcmp(value, "1") == 0 || strcmp(value, "yes") == 0 || strcmp(value, "on") == 0) {
strcpy(normalized_value, "true");
} else if (strcmp(value, "0") == 0 || strcmp(value, "no") == 0 || strcmp(value, "off") == 0) {
strcpy(normalized_value, "false");
}
// Determine the data type based on the configuration key
const char* data_type = "string"; // Default to string
for (int i = 0; known_configs[i].key != NULL; i++) {
if (strcmp(key, known_configs[i].key) == 0) {
if (strcmp(known_configs[i].type, "bool") == 0) {
data_type = "boolean";
} else if (strcmp(known_configs[i].type, "int") == 0) {
data_type = "integer";
} else if (strcmp(known_configs[i].type, "string") == 0) {
data_type = "string";
}
break;
}
}
// Update or insert the configuration value
sqlite3_stmt* stmt;
const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type) VALUES (?, ?, ?)";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
DEBUG_ERROR("Failed to prepare config update statement");
const char* err_msg = sqlite3_errmsg(g_db);
DEBUG_ERROR(err_msg);
return -1;
}
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, normalized_value, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, data_type, -1, SQLITE_STATIC);
int result = sqlite3_step(stmt);
if (result != SQLITE_DONE) {
DEBUG_ERROR("Failed to update configuration in database");
const char* err_msg = sqlite3_errmsg(g_db);
DEBUG_ERROR(err_msg);
sqlite3_finalize(stmt);
return -1;
}
sqlite3_finalize(stmt);
return 0;
}
// Generate confirmation message for config change
char* generate_config_change_confirmation(const char* key, const char* old_value, const char* new_value) {
if (!key || !old_value || !new_value) {
return NULL;
}
char* confirmation = malloc(2048);
if (!confirmation) {
return NULL;
}
// Get description for the config key
const char* description = "";
if (strcmp(key, "auth_enabled") == 0) {
description = "This controls whether authentication is required for the relay.";
} else if (strcmp(key, "nip42_auth_required") == 0) {
description = "This controls whether NIP-42 authentication is required.";
} else if (strcmp(key, "nip40_expiration_enabled") == 0) {
description = "This controls whether NIP-40 event expiration is enabled.";
} else if (strcmp(key, "max_connections") == 0) {
description = "This sets the maximum number of concurrent connections.";
} else if (strcmp(key, "max_subscriptions_per_client") == 0) {
description = "This sets the maximum subscriptions per client.";
} else if (strcmp(key, "pow_min_difficulty") == 0) {
description = "This sets the minimum proof-of-work difficulty required.";
} else if (strstr(key, "relay_") == key) {
description = "This changes relay metadata information.";
}
snprintf(confirmation, 2048,
"🔧 Configuration Change Request\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
"\n"
"Setting: %s\n"
"Current Value: %s\n"
"New Value: %s\n"
"\n"
"%s%s"
"\n"
"⚠️ Reply with 'yes' to confirm or 'no' to cancel.\n"
"⏰ This request will expire in 5 minutes.",
key, old_value, new_value,
strlen(description) > 0 ? " " : "",
description
);
return confirmation;
}
// Handle confirmation responses (yes/no)
int handle_config_confirmation(const char* admin_pubkey, const char* response) {
if (!admin_pubkey || !response) {
return -1;
}
// Clean up expired changes first
cleanup_expired_pending_changes();
// Convert response to lowercase
char response_lower[64];
size_t response_len = strlen(response);
size_t copy_len = response_len < sizeof(response_lower) - 1 ? response_len : sizeof(response_lower) - 1;
memcpy(response_lower, response, copy_len);
response_lower[copy_len] = '\0';
for (size_t i = 0; i < copy_len; i++) {
if (response_lower[i] >= 'A' && response_lower[i] <= 'Z') {
response_lower[i] = response_lower[i] + 32;
}
}
// Trim whitespace
char* start = response_lower;
while (*start == ' ' || *start == '\t') start++;
char* end = start + strlen(start) - 1;
while (end > start && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) {
*end = '\0';
end--;
}
// Check if it's a confirmation response
int is_yes = (strcmp(start, "yes") == 0 || strcmp(start, "y") == 0 ||
strcmp(start, "confirm") == 0 || strcmp(start, "ok") == 0);
int is_no = (strcmp(start, "no") == 0 || strcmp(start, "n") == 0 ||
strcmp(start, "cancel") == 0 || strcmp(start, "abort") == 0);
if (!is_yes && !is_no) {
return 0; // Not a confirmation response
}
// Find the most recent pending change for this admin
pending_config_change_t* change = find_latest_pending_change(admin_pubkey);
if (!change) {
return -2; // No pending changes
}
if (is_yes) {
// Apply the configuration change
int result = apply_config_change(change->config_key, change->new_value);
if (result == 0) {
// Send success response
char success_msg[1024];
snprintf(success_msg, sizeof(success_msg),
"✅ Configuration Updated\n"
"━━━━━━━━━━━━━━━━━━━━━━━━\n"
"\n"
"%s: %s → %s\n"
"\n"
"Change applied successfully.",
change->config_key, change->old_value, change->new_value
);
char error_msg[256];
int send_result = send_nip17_response(admin_pubkey, success_msg, error_msg, sizeof(error_msg));
if (send_result != 0) {
DEBUG_ERROR(error_msg);
}
// Remove the pending change
remove_pending_change(change);
return 1; // Success
} else {
// Send error response
char error_msg[1024];
snprintf(error_msg, sizeof(error_msg),
"❌ Configuration Update Failed\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
"\n"
"Failed to apply change: %s = %s\n"
"\n"
"Please check the relay logs for details.",
change->config_key, change->new_value
);
char send_error_msg[256];
int send_result = send_nip17_response(admin_pubkey, error_msg, send_error_msg, sizeof(send_error_msg));
if (send_result != 0) {
DEBUG_ERROR(send_error_msg);
}
// Remove the pending change
remove_pending_change(change);
return -3; // Application failed
}
} else if (is_no) {
// Cancel the change
char cancel_msg[512];
snprintf(cancel_msg, sizeof(cancel_msg),
"🚫 Configuration Change Cancelled\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
"\n"
"Change cancelled: %s\n"
"\n"
"No changes were made to the relay configuration.",
change->config_key
);
send_nip17_response(admin_pubkey, cancel_msg, NULL, 0);
// Remove the pending change
remove_pending_change(change);
return 2; // Cancelled
}
return 0;
}
// Process a configuration change request
int process_config_change_request(const char* admin_pubkey, const char* message) {
if (!admin_pubkey || !message) {
return -1;
}
char key[128], value[256];
// Parse the configuration command
if (!parse_config_command(message, key, value)) {
return 0; // Not a config command
}
// Validate the configuration change
if (!validate_config_change(key, value)) {
char error_msg[2048];
snprintf(error_msg, sizeof(error_msg),
"❌ Invalid Configuration\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
"\n"
"Invalid configuration: %s = %s\n"
"\n"
"The configuration key '%s' is either unknown or the value '%s' is invalid.\n"
"\n"
"Supported keys include: auth_enabled, max_connections, default_limit, relay_description, etc.\n"
"Use 'config' command to see all current settings.",
key, value, key, value
);
send_nip17_response(admin_pubkey, error_msg, NULL, 0);
return -2;
}
// Get current value
const char* current_value = get_config_value(key);
if (!current_value) {
current_value = "unset";
}
// Check if the value is already set to the requested value
if (strcmp(current_value, value) == 0) {
char already_set_msg[1024];
snprintf(already_set_msg, sizeof(already_set_msg),
" Configuration Already Set\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
"\n"
"%s is already set to: %s\n"
"\n"
"No change needed.",
key, value
);
send_nip17_response(admin_pubkey, already_set_msg, NULL, 0);
return 3;
}
// Store the pending change
char* change_id = store_pending_config_change(admin_pubkey, key, current_value, value);
if (!change_id) {
char error_msg[256];
snprintf(error_msg, sizeof(error_msg),
"❌ Failed to store configuration change request.\n"
"Please try again."
);
send_nip17_response(admin_pubkey, error_msg, NULL, 0);
return -3;
}
// Generate and send confirmation message
char* confirmation = generate_config_change_confirmation(key, current_value, value);
if (confirmation) {
char error_msg[256];
int send_result = send_nip17_response(admin_pubkey, confirmation, error_msg, sizeof(error_msg));
if (send_result != 0) {
DEBUG_ERROR(error_msg);
}
free(confirmation);
}
free(change_id);
return 1; // Confirmation sent
}
// Handle monitoring system admin commands
int handle_monitoring_command(cJSON* event, const char* command, char* error_message, size_t error_size, struct lws* wsi) {
if (!event || !command || !error_message) {
return -1;
}
// Get request event ID for response correlation
cJSON* request_id_obj = cJSON_GetObjectItem(event, "id");
if (!request_id_obj || !cJSON_IsString(request_id_obj)) {
snprintf(error_message, error_size, "Missing request event ID");
return -1;
}
const char* request_id = cJSON_GetStringValue(request_id_obj);
// Get sender pubkey for response
cJSON* sender_pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
if (!sender_pubkey_obj || !cJSON_IsString(sender_pubkey_obj)) {
snprintf(error_message, error_size, "Missing sender pubkey");
return -1;
}
const char* sender_pubkey = cJSON_GetStringValue(sender_pubkey_obj);
// Parse command
char cmd[256];
char arg[256];
cmd[0] = '\0';
arg[0] = '\0';
// Simple command parsing - split on space
const char* space_pos = strchr(command, ' ');
if (space_pos) {
size_t cmd_len = space_pos - command;
if (cmd_len < sizeof(cmd)) {
memcpy(cmd, command, cmd_len);
cmd[cmd_len] = '\0';
strcpy(arg, space_pos + 1);
}
} else {
strcpy(cmd, command);
}
// Convert to lowercase for case-insensitive matching
for (char* p = cmd; *p; p++) {
if (*p >= 'A' && *p <= 'Z') *p = *p + 32;
}
// Handle set_monitoring_throttle command (only remaining monitoring command)
if (strcmp(cmd, "set_monitoring_throttle") == 0) {
if (arg[0] == '\0') {
char* response_content = "❌ Missing throttle value\n\nUsage: set_monitoring_throttle <seconds>";
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
}
char* endptr;
long throttle_seconds = strtol(arg, &endptr, 10);
if (*endptr != '\0' || throttle_seconds < 1 || throttle_seconds > 3600) {
char* response_content = "❌ Invalid throttle value\n\nThrottle must be between 1 and 3600 seconds.";
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
}
char throttle_str[16];
snprintf(throttle_str, sizeof(throttle_str), "%ld", throttle_seconds);
if (update_config_in_table("kind_24567_reporting_throttle_sec", throttle_str) == 0) {
char response_content[256];
snprintf(response_content, sizeof(response_content),
"✅ Monitoring throttle updated\n\n"
"Minimum interval between monitoring events: %ld seconds\n\n"
" Monitoring activates automatically when you subscribe to kind 24567 events.",
throttle_seconds);
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
} else {
char* response_content = "❌ Failed to update monitoring throttle";
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
}
} else {
char response_content[1024];
snprintf(response_content, sizeof(response_content),
"❌ Unknown monitoring command: %s\n\n"
"Available command:\n"
"• set_monitoring_throttle <seconds>\n\n"
" Monitoring is now subscription-based:\n"
"Subscribe to kind 24567 events to receive real-time monitoring data.\n"
"Monitoring automatically activates when subscriptions exist and deactivates when they close.",
cmd);
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
}
}