Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a916cc221 | ||
|
|
dcf421ff93 |
Binary file not shown.
Binary file not shown.
@@ -58,7 +58,7 @@
|
||||
<div class="inline-buttons">
|
||||
<button type="button" id="connect-relay-btn">CONNECT TO RELAY</button>
|
||||
<button type="button" id="disconnect-relay-btn" disabled>DISCONNECT</button>
|
||||
<button type="button" id="test-websocket-btn" disabled>TEST WEBSOCKET</button>
|
||||
<button type="button" id="restart-relay-btn" disabled>RESTART RELAY</button>
|
||||
</div>
|
||||
|
||||
<div class="status disconnected" id="relay-connection-status">NOT CONNECTED</div>
|
||||
|
||||
114
api/index.js
114
api/index.js
@@ -41,7 +41,7 @@
|
||||
const relayConnectionStatus = document.getElementById('relay-connection-status');
|
||||
const connectRelayBtn = document.getElementById('connect-relay-btn');
|
||||
const disconnectRelayBtn = document.getElementById('disconnect-relay-btn');
|
||||
const testWebSocketBtn = document.getElementById('test-websocket-btn');
|
||||
const restartRelayBtn = document.getElementById('restart-relay-btn');
|
||||
const configDisplay = document.getElementById('config-display');
|
||||
const configTableBody = document.getElementById('config-table-body');
|
||||
|
||||
@@ -369,28 +369,28 @@
|
||||
relayConnectionStatus.className = 'status connected';
|
||||
connectRelayBtn.disabled = true;
|
||||
disconnectRelayBtn.disabled = true;
|
||||
testWebSocketBtn.disabled = true;
|
||||
restartRelayBtn.disabled = true;
|
||||
break;
|
||||
case 'connected':
|
||||
relayConnectionStatus.textContent = 'CONNECTED';
|
||||
relayConnectionStatus.className = 'status connected';
|
||||
connectRelayBtn.disabled = true;
|
||||
disconnectRelayBtn.disabled = false;
|
||||
testWebSocketBtn.disabled = false;
|
||||
restartRelayBtn.disabled = false;
|
||||
break;
|
||||
case 'disconnected':
|
||||
relayConnectionStatus.textContent = 'NOT CONNECTED';
|
||||
relayConnectionStatus.className = 'status disconnected';
|
||||
connectRelayBtn.disabled = false;
|
||||
disconnectRelayBtn.disabled = true;
|
||||
testWebSocketBtn.disabled = true;
|
||||
restartRelayBtn.disabled = true;
|
||||
break;
|
||||
case 'error':
|
||||
relayConnectionStatus.textContent = 'CONNECTION FAILED';
|
||||
relayConnectionStatus.className = 'status error';
|
||||
connectRelayBtn.disabled = false;
|
||||
disconnectRelayBtn.disabled = true;
|
||||
testWebSocketBtn.disabled = true;
|
||||
restartRelayBtn.disabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1670,22 +1670,12 @@
|
||||
disconnectFromRelay();
|
||||
});
|
||||
|
||||
testWebSocketBtn.addEventListener('click', function (e) {
|
||||
restartRelayBtn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
if (!url) {
|
||||
log('Please enter a relay URL first', 'ERROR');
|
||||
return;
|
||||
}
|
||||
|
||||
testWebSocketConnection(url)
|
||||
.then(() => {
|
||||
log('WebSocket test successful', 'INFO');
|
||||
})
|
||||
.catch(error => {
|
||||
log(`WebSocket test failed: ${error.message}`, 'ERROR');
|
||||
});
|
||||
sendRestartCommand().catch(error => {
|
||||
log(`Restart command failed: ${error.message}`, 'ERROR');
|
||||
});
|
||||
});
|
||||
|
||||
// ================================
|
||||
@@ -3148,6 +3138,83 @@
|
||||
// DATABASE STATISTICS FUNCTIONS
|
||||
// ================================
|
||||
|
||||
// Send restart command to restart the relay using Administrator API
|
||||
async function sendRestartCommand() {
|
||||
if (!isLoggedIn || !userPubkey) {
|
||||
log('Must be logged in to restart relay', 'ERROR');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!relayPool) {
|
||||
log('SimplePool connection not available', 'ERROR');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log('Sending restart command to relay...', 'INFO');
|
||||
|
||||
// Create command array for restart
|
||||
const command_array = ["system_command", "restart"];
|
||||
|
||||
// Encrypt the command array directly using NIP-44
|
||||
const encrypted_content = await encryptForRelay(JSON.stringify(command_array));
|
||||
if (!encrypted_content) {
|
||||
throw new Error('Failed to encrypt command array');
|
||||
}
|
||||
|
||||
// Create single kind 23456 admin event
|
||||
const restartEvent = {
|
||||
kind: 23456,
|
||||
pubkey: userPubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [["p", getRelayPubkey()]],
|
||||
content: encrypted_content
|
||||
};
|
||||
|
||||
// Sign the event
|
||||
const signedEvent = await window.nostr.signEvent(restartEvent);
|
||||
if (!signedEvent || !signedEvent.sig) {
|
||||
throw new Error('Event signing failed');
|
||||
}
|
||||
|
||||
// Publish via SimplePool
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const publishPromises = relayPool.publish([url], signedEvent);
|
||||
|
||||
// Use Promise.allSettled to capture per-relay outcomes
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
|
||||
// Check if any relay accepted the event
|
||||
let successCount = 0;
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
successCount++;
|
||||
log(`Restart command published successfully to relay ${index}`, 'INFO');
|
||||
} else {
|
||||
log(`Restart command failed on relay ${index}: ${result.reason?.message || result.reason}`, 'ERROR');
|
||||
}
|
||||
});
|
||||
|
||||
if (successCount === 0) {
|
||||
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
|
||||
throw new Error(`All relays rejected restart command. Details: ${errorDetails}`);
|
||||
}
|
||||
|
||||
log('Restart command sent successfully - relay should restart shortly...', 'INFO');
|
||||
|
||||
// Update connection status to indicate restart is in progress
|
||||
updateRelayConnectionStatus('connecting');
|
||||
relayConnectionStatus.textContent = 'RESTARTING...';
|
||||
|
||||
// The relay will disconnect and need to be reconnected after restart
|
||||
// This will be handled by the WebSocket disconnection event
|
||||
|
||||
} catch (error) {
|
||||
log(`Failed to send restart command: ${error.message}`, 'ERROR');
|
||||
updateRelayConnectionStatus('error');
|
||||
}
|
||||
}
|
||||
|
||||
// Send stats_query command to get database statistics using Administrator API (inner events)
|
||||
async function sendStatsQuery() {
|
||||
if (!isLoggedIn || !userPubkey) {
|
||||
@@ -3304,9 +3371,12 @@
|
||||
const events7d = document.getElementById('events-7d');
|
||||
const events30d = document.getElementById('events-30d');
|
||||
|
||||
if (events24h) events24h.textContent = data.events_24h || '-';
|
||||
if (events7d) events7d.textContent = data.events_7d || '-';
|
||||
if (events30d) events30d.textContent = data.events_30d || '-';
|
||||
// Access the nested time_stats object from backend response
|
||||
const timeStats = data.time_stats || {};
|
||||
|
||||
if (events24h) events24h.textContent = timeStats.last_24h || '0';
|
||||
if (events7d) events7d.textContent = timeStats.last_7d || '0';
|
||||
if (events30d) events30d.textContent = timeStats.last_30d || '0';
|
||||
}
|
||||
|
||||
// Populate top pubkeys table
|
||||
|
||||
63
src/config.c
63
src/config.c
@@ -16,6 +16,9 @@
|
||||
// External database connection (from main.c)
|
||||
extern sqlite3* g_db;
|
||||
|
||||
// External shutdown flag (from main.c)
|
||||
extern volatile sig_atomic_t g_shutdown_flag;
|
||||
|
||||
// Global unified configuration cache instance
|
||||
unified_config_cache_t g_unified_cache = {
|
||||
.cache_lock = PTHREAD_MUTEX_INITIALIZER,
|
||||
@@ -3673,19 +3676,19 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(response, "command", "system_status");
|
||||
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
|
||||
|
||||
|
||||
cJSON* status_data = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(status_data, "database", g_db ? "connected" : "not_available");
|
||||
cJSON_AddStringToObject(status_data, "cache_status", g_unified_cache.cache_valid ? "valid" : "invalid");
|
||||
|
||||
|
||||
if (strlen(g_database_path) > 0) {
|
||||
cJSON_AddStringToObject(status_data, "database_path", g_database_path);
|
||||
}
|
||||
|
||||
|
||||
// Count configuration items and auth rules
|
||||
if (g_db) {
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
|
||||
// Config count
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
@@ -3693,7 +3696,7 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
|
||||
// Auth rules count
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM auth_rules", -1, &stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
@@ -3702,34 +3705,72 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
cJSON_AddItemToObject(response, "data", status_data);
|
||||
|
||||
|
||||
printf("=== System Status ===\n");
|
||||
printf("Database: %s\n", g_db ? "Connected" : "Not available");
|
||||
printf("Cache status: %s\n", g_unified_cache.cache_valid ? "Valid" : "Invalid");
|
||||
|
||||
|
||||
// Get admin pubkey from event for response
|
||||
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
|
||||
const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL;
|
||||
|
||||
|
||||
if (!admin_pubkey) {
|
||||
cJSON_Delete(response);
|
||||
snprintf(error_message, error_size, "missing admin pubkey for response");
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
// Send response as signed kind 23457 event
|
||||
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
|
||||
log_success("System status query completed successfully with signed response");
|
||||
cJSON_Delete(response);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
cJSON_Delete(response);
|
||||
snprintf(error_message, error_size, "failed to send system status response");
|
||||
return -1;
|
||||
}
|
||||
else if (strcmp(command, "restart") == 0) {
|
||||
// Build restart acknowledgment response
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(response, "command", "restart");
|
||||
cJSON_AddStringToObject(response, "status", "initiating_restart");
|
||||
cJSON_AddStringToObject(response, "message", "Relay restart initiated - shutting down gracefully");
|
||||
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
|
||||
|
||||
printf("=== Relay Restart Initiated ===\n");
|
||||
printf("Admin requested system restart\n");
|
||||
printf("Sending acknowledgment and initiating shutdown...\n");
|
||||
|
||||
// Get admin pubkey from event for response
|
||||
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
|
||||
const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL;
|
||||
|
||||
if (!admin_pubkey) {
|
||||
cJSON_Delete(response);
|
||||
snprintf(error_message, error_size, "missing admin pubkey for response");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Send acknowledgment response as signed kind 23457 event
|
||||
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
|
||||
log_success("Restart acknowledgment sent successfully - initiating shutdown");
|
||||
|
||||
// Trigger graceful shutdown by setting the global shutdown flag
|
||||
g_shutdown_flag = 1;
|
||||
log_info("Shutdown flag set - relay will restart gracefully");
|
||||
|
||||
cJSON_Delete(response);
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON_Delete(response);
|
||||
snprintf(error_message, error_size, "failed to send restart acknowledgment");
|
||||
return -1;
|
||||
}
|
||||
else {
|
||||
snprintf(error_message, error_size, "invalid: unknown system command '%s'", command);
|
||||
return -1;
|
||||
|
||||
924
src/dm_admin.c
924
src/dm_admin.c
@@ -47,6 +47,79 @@ extern int broadcast_event_to_subscriptions(cJSON* event);
|
||||
// Forward declarations for stats generation
|
||||
extern char* generate_stats_json(void);
|
||||
|
||||
// ================================
|
||||
// CONFIGURATION CHANGE SYSTEM
|
||||
// ================================
|
||||
|
||||
// Data structure for pending configuration changes
|
||||
typedef struct pending_config_change {
|
||||
char admin_pubkey[65]; // Who requested the change
|
||||
char config_key[128]; // What config to change
|
||||
char old_value[256]; // Current value
|
||||
char new_value[256]; // Requested new value
|
||||
time_t timestamp; // When requested
|
||||
char change_id[33]; // Unique ID for this change (first 32 chars of hash)
|
||||
struct pending_config_change* next; // Linked list for concurrent changes
|
||||
} pending_config_change_t;
|
||||
|
||||
// Global state for pending changes
|
||||
static pending_config_change_t* pending_changes_head = NULL;
|
||||
static int pending_changes_count = 0;
|
||||
|
||||
// Configuration change timeout (5 minutes)
|
||||
#define CONFIG_CHANGE_TIMEOUT 300
|
||||
|
||||
// Known configuration keys and their types for validation
|
||||
static struct {
|
||||
const char* key;
|
||||
const char* type; // "bool", "int", "string"
|
||||
int min_val;
|
||||
int max_val;
|
||||
} known_configs[] = {
|
||||
{"auth_enabled", "bool", 0, 1},
|
||||
{"nip42_auth_required", "bool", 0, 1},
|
||||
{"nip40_expiration_enabled", "bool", 0, 1},
|
||||
{"max_connections", "int", 1, 10000},
|
||||
{"max_subscriptions_per_client", "int", 1, 1000},
|
||||
{"max_event_tags", "int", 1, 1000},
|
||||
{"max_content_length", "int", 1, 1000000},
|
||||
{"max_limit", "int", 1, 10000},
|
||||
{"default_limit", "int", 1, 5000},
|
||||
{"max_filters_per_subscription", "int", 1, 100},
|
||||
{"max_total_subscriptions", "int", 1, 10000},
|
||||
{"pow_min_difficulty", "int", 0, 64},
|
||||
{"nip42_challenge_timeout", "int", 1, 3600},
|
||||
{"nip42_challenge_expiration", "int", 1, 3600},
|
||||
{"nip40_expiration_grace_period", "int", 1, 86400},
|
||||
{"relay_name", "string", 0, 0},
|
||||
{"relay_description", "string", 0, 0},
|
||||
{"relay_contact", "string", 0, 0},
|
||||
{"relay_icon", "string", 0, 0},
|
||||
{"relay_countries", "string", 0, 0},
|
||||
{"language_tags", "string", 0, 0},
|
||||
{"posting_policy", "string", 0, 0},
|
||||
{"payments_url", "string", 0, 0},
|
||||
{"supported_nips", "string", 0, 0},
|
||||
{"relay_software", "string", 0, 0},
|
||||
{"relay_version", "string", 0, 0},
|
||||
{"pow_mode", "string", 0, 0},
|
||||
{NULL, NULL, 0, 0}
|
||||
};
|
||||
|
||||
// Forward declarations for config change functions
|
||||
int parse_config_command(const char* message, char* key, char* value);
|
||||
int validate_config_change(const char* key, const char* value);
|
||||
char* store_pending_config_change(const char* admin_pubkey, const char* key,
|
||||
const char* old_value, const char* new_value);
|
||||
pending_config_change_t* find_pending_change(const char* admin_pubkey, const char* change_id);
|
||||
int apply_config_change(const char* key, const char* value);
|
||||
void cleanup_expired_pending_changes(void);
|
||||
int handle_config_confirmation(const char* admin_pubkey, const char* response);
|
||||
char* generate_config_change_confirmation(const char* key, const char* old_value, const char* new_value);
|
||||
int process_config_change_request(const char* admin_pubkey, const char* message);
|
||||
int send_nip17_response(const char* sender_pubkey, const char* response_content,
|
||||
char* error_message, size_t error_size);
|
||||
|
||||
// Forward declarations for admin event processing
|
||||
extern int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
|
||||
@@ -201,6 +274,682 @@ int process_dm_admin_command(cJSON* command_array, cJSON* event, char* error_mes
|
||||
return result;
|
||||
}
|
||||
|
||||
// ================================
|
||||
// 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;
|
||||
}
|
||||
|
||||
log_info("DEBUG: Parsing config command");
|
||||
char debug_msg[256];
|
||||
snprintf(debug_msg, sizeof(debug_msg), "DEBUG: Input message: '%.100s'", message);
|
||||
log_info(debug_msg);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
log_info("DEBUG: No config command pattern matched");
|
||||
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) {
|
||||
log_info("Cleaning up expired config change request");
|
||||
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) {
|
||||
log_error("DEBUG: apply_config_change called with NULL key or value");
|
||||
return -1;
|
||||
}
|
||||
|
||||
extern sqlite3* g_db;
|
||||
if (!g_db) {
|
||||
log_error("Database not available for config change");
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("DEBUG: Applying config change");
|
||||
char debug_msg[256];
|
||||
snprintf(debug_msg, sizeof(debug_msg), "DEBUG: Key='%s', Value='%s'", key, value);
|
||||
log_info(debug_msg);
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
log_info("DEBUG: Normalized value");
|
||||
char norm_msg[256];
|
||||
snprintf(norm_msg, sizeof(norm_msg), "DEBUG: Normalized value='%s'", normalized_value);
|
||||
log_info(norm_msg);
|
||||
|
||||
// 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 (?, ?, ?)";
|
||||
|
||||
log_info("DEBUG: Preparing SQL statement");
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
log_error("Failed to prepare config update statement");
|
||||
const char* err_msg = sqlite3_errmsg(g_db);
|
||||
log_error(err_msg);
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("DEBUG: Binding parameters");
|
||||
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);
|
||||
|
||||
log_info("DEBUG: Executing SQL statement");
|
||||
int result = sqlite3_step(stmt);
|
||||
if (result != SQLITE_DONE) {
|
||||
log_error("Failed to update configuration in database");
|
||||
const char* err_msg = sqlite3_errmsg(g_db);
|
||||
log_error(err_msg);
|
||||
sqlite3_finalize(stmt);
|
||||
return -1;
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
log_info("DEBUG: SQL execution successful");
|
||||
char log_msg[512];
|
||||
snprintf(log_msg, sizeof(log_msg), "Configuration updated: %s = %s", key, normalized_value);
|
||||
log_success(log_msg);
|
||||
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
|
||||
log_info("DEBUG: Applying configuration change");
|
||||
int result = apply_config_change(change->config_key, change->new_value);
|
||||
if (result == 0) {
|
||||
// Send success response
|
||||
log_info("DEBUG: Configuration change applied successfully, sending 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) {
|
||||
log_error("DEBUG: Failed to send success response");
|
||||
log_error(error_msg);
|
||||
} else {
|
||||
log_success("DEBUG: Success response sent");
|
||||
}
|
||||
|
||||
// Remove the pending change
|
||||
remove_pending_change(change);
|
||||
return 1; // Success
|
||||
} else {
|
||||
// Send error response
|
||||
log_error("DEBUG: Configuration change failed, sending 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) {
|
||||
log_error("DEBUG: Failed to send error response");
|
||||
log_error(send_error_msg);
|
||||
} else {
|
||||
log_success("DEBUG: Error response sent");
|
||||
}
|
||||
|
||||
// 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
|
||||
log_info("DEBUG: Generating confirmation message");
|
||||
char* confirmation = generate_config_change_confirmation(key, current_value, value);
|
||||
if (confirmation) {
|
||||
log_info("DEBUG: Confirmation message generated, sending response");
|
||||
char error_msg[256];
|
||||
int send_result = send_nip17_response(admin_pubkey, confirmation, error_msg, sizeof(error_msg));
|
||||
if (send_result == 0) {
|
||||
log_success("DEBUG: Confirmation response sent successfully");
|
||||
} else {
|
||||
log_error("DEBUG: Failed to send confirmation response");
|
||||
log_error(error_msg);
|
||||
}
|
||||
free(confirmation);
|
||||
} else {
|
||||
log_error("DEBUG: Failed to generate confirmation message");
|
||||
}
|
||||
|
||||
free(change_id);
|
||||
return 1; // Confirmation sent
|
||||
}
|
||||
|
||||
// Generate stats JSON from database queries
|
||||
char* generate_stats_json(void) {
|
||||
extern sqlite3* g_db;
|
||||
@@ -478,7 +1227,7 @@ char* generate_stats_text(void) {
|
||||
|
||||
// Parse the JSON to extract values for human-readable format
|
||||
cJSON* stats_obj = cJSON_Parse(stats_json);
|
||||
char* stats_text = malloc(4096);
|
||||
char* stats_text = malloc(16384); // Increased buffer size for comprehensive stats
|
||||
if (!stats_text) {
|
||||
free(stats_json);
|
||||
if (stats_obj) cJSON_Delete(stats_obj);
|
||||
@@ -486,14 +1235,34 @@ char* generate_stats_text(void) {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
||||
// 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");
|
||||
@@ -504,26 +1273,103 @@ char* generate_stats_text(void) {
|
||||
last_30d = d30 ? (long long)cJSON_GetNumberValue(d30) : 0;
|
||||
}
|
||||
|
||||
snprintf(stats_text, 4096,
|
||||
// Start building the comprehensive stats text
|
||||
int offset = 0;
|
||||
|
||||
// Header
|
||||
offset += snprintf(stats_text + offset, 16384 - offset,
|
||||
"📊 Relay Statistics\n"
|
||||
"━━━━━━━━━━━━━━━━━━━━\n"
|
||||
"Total Events: %lld\n"
|
||||
"Database Size: %.2f MB (%lld bytes)\n"
|
||||
"\n"
|
||||
"📈 Recent Activity\n"
|
||||
"━━━━━━━━━━━━━━━━━━━\n"
|
||||
"Last 24 hours: %lld events\n"
|
||||
"Last 7 days: %lld events\n"
|
||||
"Last 30 days: %lld events\n"
|
||||
"\n"
|
||||
"✅ Statistics retrieved successfully",
|
||||
total, db_mb, db_bytes, last_24h, last_7d, last_30d
|
||||
);
|
||||
|
||||
"━━━━━━━━━━━━━━━━━━━━\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"
|
||||
"Oldest Event\t%s\tTimestamp of oldest event\n"
|
||||
"Newest Event\t%s\tTimestamp of newest event\n"
|
||||
"\n",
|
||||
db_mb, db_bytes, total, 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, 4096,
|
||||
snprintf(stats_text, 16384,
|
||||
"📊 Relay Statistics\n"
|
||||
"━━━━━━━━━━━━━━━━━━━━\n"
|
||||
"Raw data: %s\n"
|
||||
@@ -532,7 +1378,7 @@ char* generate_stats_text(void) {
|
||||
stats_json
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
free(stats_json);
|
||||
return stats_text;
|
||||
}
|
||||
@@ -640,6 +1486,11 @@ cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message,
|
||||
cJSON_Delete(command_array);
|
||||
}
|
||||
}
|
||||
} else if (result > 0) {
|
||||
// Command was handled and response was sent, don't create generic response
|
||||
log_info("NIP-17: Command handled with custom response, skipping generic response");
|
||||
cJSON_Delete(inner_dm);
|
||||
return NULL;
|
||||
|
||||
// Get sender pubkey for response from the decrypted DM event
|
||||
cJSON* sender_pubkey_obj = cJSON_GetObjectItem(inner_dm, "pubkey");
|
||||
@@ -861,6 +1712,39 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
// Check if it's a confirmation response (yes/no)
|
||||
int confirmation_result = handle_config_confirmation(sender_pubkey, dm_content);
|
||||
if (confirmation_result != 0) {
|
||||
if (confirmation_result > 0) {
|
||||
log_success("NIP-17: Configuration confirmation processed successfully");
|
||||
} else if (confirmation_result == -2) {
|
||||
// No pending changes
|
||||
char no_pending_msg[256];
|
||||
snprintf(no_pending_msg, sizeof(no_pending_msg),
|
||||
"❌ No Pending Changes\n"
|
||||
"━━━━━━━━━━━━━━━━━━━━━━\n"
|
||||
"\n"
|
||||
"You don't have any pending configuration changes to confirm.\n"
|
||||
"\n"
|
||||
"Send a configuration command first (e.g., 'auth_enabled true')."
|
||||
);
|
||||
send_nip17_response(sender_pubkey, no_pending_msg, NULL, 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if it's a configuration change request
|
||||
int config_result = process_config_change_request(sender_pubkey, dm_content);
|
||||
if (config_result != 0) {
|
||||
if (config_result > 0) {
|
||||
log_success("NIP-17: Configuration change request processed successfully");
|
||||
return 1; // Return positive value to indicate response was handled
|
||||
} else {
|
||||
log_error("NIP-17: Configuration change request failed");
|
||||
return -1; // Return error to prevent generic success response
|
||||
}
|
||||
}
|
||||
|
||||
log_info("NIP-17: Plain text content from admin not recognized as command, treating as user DM");
|
||||
return 0; // Admin sent unrecognized plain text, treat as user DM
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -45,6 +45,8 @@ int nostr_nip42_verify_auth_event(cJSON *event, const char *challenge_id,
|
||||
// Global state
|
||||
sqlite3* g_db = NULL; // Non-static so config.c can access it
|
||||
int g_server_running = 1; // Non-static so websockets.c can access it
|
||||
volatile sig_atomic_t g_shutdown_flag = 0; // Non-static so config.c can access it for restart functionality
|
||||
int g_restart_requested = 0; // Non-static so config.c can access it for restart functionality
|
||||
struct lws_context *ws_context = NULL; // Non-static so websockets.c can access it
|
||||
|
||||
// NIP-11 relay information structure
|
||||
|
||||
@@ -95,6 +95,8 @@ extern unified_config_cache_t g_unified_cache;
|
||||
// Forward declarations for global state
|
||||
extern sqlite3* g_db;
|
||||
extern int g_server_running;
|
||||
extern volatile sig_atomic_t g_shutdown_flag;
|
||||
extern int g_restart_requested;
|
||||
extern struct lws_context *ws_context;
|
||||
|
||||
// Global subscription manager
|
||||
@@ -1153,7 +1155,7 @@ int start_websocket_relay(int port_override, int strict_port) {
|
||||
log_success(startup_msg);
|
||||
|
||||
// Main event loop with proper signal handling
|
||||
while (g_server_running) {
|
||||
while (g_server_running && !g_shutdown_flag) {
|
||||
int result = lws_service(ws_context, 1000);
|
||||
|
||||
if (result < 0) {
|
||||
|
||||
Reference in New Issue
Block a user