Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a89f84f76e | ||
|
|
5a916cc221 | ||
|
|
dcf421ff93 |
Binary file not shown.
Binary file not shown.
@@ -58,7 +58,7 @@
|
|||||||
<div class="inline-buttons">
|
<div class="inline-buttons">
|
||||||
<button type="button" id="connect-relay-btn">CONNECT TO RELAY</button>
|
<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="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>
|
||||||
|
|
||||||
<div class="status disconnected" id="relay-connection-status">NOT CONNECTED</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 relayConnectionStatus = document.getElementById('relay-connection-status');
|
||||||
const connectRelayBtn = document.getElementById('connect-relay-btn');
|
const connectRelayBtn = document.getElementById('connect-relay-btn');
|
||||||
const disconnectRelayBtn = document.getElementById('disconnect-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 configDisplay = document.getElementById('config-display');
|
||||||
const configTableBody = document.getElementById('config-table-body');
|
const configTableBody = document.getElementById('config-table-body');
|
||||||
|
|
||||||
@@ -369,28 +369,28 @@
|
|||||||
relayConnectionStatus.className = 'status connected';
|
relayConnectionStatus.className = 'status connected';
|
||||||
connectRelayBtn.disabled = true;
|
connectRelayBtn.disabled = true;
|
||||||
disconnectRelayBtn.disabled = true;
|
disconnectRelayBtn.disabled = true;
|
||||||
testWebSocketBtn.disabled = true;
|
restartRelayBtn.disabled = true;
|
||||||
break;
|
break;
|
||||||
case 'connected':
|
case 'connected':
|
||||||
relayConnectionStatus.textContent = 'CONNECTED';
|
relayConnectionStatus.textContent = 'CONNECTED';
|
||||||
relayConnectionStatus.className = 'status connected';
|
relayConnectionStatus.className = 'status connected';
|
||||||
connectRelayBtn.disabled = true;
|
connectRelayBtn.disabled = true;
|
||||||
disconnectRelayBtn.disabled = false;
|
disconnectRelayBtn.disabled = false;
|
||||||
testWebSocketBtn.disabled = false;
|
restartRelayBtn.disabled = false;
|
||||||
break;
|
break;
|
||||||
case 'disconnected':
|
case 'disconnected':
|
||||||
relayConnectionStatus.textContent = 'NOT CONNECTED';
|
relayConnectionStatus.textContent = 'NOT CONNECTED';
|
||||||
relayConnectionStatus.className = 'status disconnected';
|
relayConnectionStatus.className = 'status disconnected';
|
||||||
connectRelayBtn.disabled = false;
|
connectRelayBtn.disabled = false;
|
||||||
disconnectRelayBtn.disabled = true;
|
disconnectRelayBtn.disabled = true;
|
||||||
testWebSocketBtn.disabled = true;
|
restartRelayBtn.disabled = true;
|
||||||
break;
|
break;
|
||||||
case 'error':
|
case 'error':
|
||||||
relayConnectionStatus.textContent = 'CONNECTION FAILED';
|
relayConnectionStatus.textContent = 'CONNECTION FAILED';
|
||||||
relayConnectionStatus.className = 'status error';
|
relayConnectionStatus.className = 'status error';
|
||||||
connectRelayBtn.disabled = false;
|
connectRelayBtn.disabled = false;
|
||||||
disconnectRelayBtn.disabled = true;
|
disconnectRelayBtn.disabled = true;
|
||||||
testWebSocketBtn.disabled = true;
|
restartRelayBtn.disabled = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1670,22 +1670,12 @@
|
|||||||
disconnectFromRelay();
|
disconnectFromRelay();
|
||||||
});
|
});
|
||||||
|
|
||||||
testWebSocketBtn.addEventListener('click', function (e) {
|
restartRelayBtn.addEventListener('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const url = relayConnectionUrl.value.trim();
|
sendRestartCommand().catch(error => {
|
||||||
if (!url) {
|
log(`Restart command failed: ${error.message}`, 'ERROR');
|
||||||
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');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ================================
|
// ================================
|
||||||
@@ -3148,6 +3138,83 @@
|
|||||||
// DATABASE STATISTICS FUNCTIONS
|
// 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)
|
// Send stats_query command to get database statistics using Administrator API (inner events)
|
||||||
async function sendStatsQuery() {
|
async function sendStatsQuery() {
|
||||||
if (!isLoggedIn || !userPubkey) {
|
if (!isLoggedIn || !userPubkey) {
|
||||||
@@ -3304,9 +3371,12 @@
|
|||||||
const events7d = document.getElementById('events-7d');
|
const events7d = document.getElementById('events-7d');
|
||||||
const events30d = document.getElementById('events-30d');
|
const events30d = document.getElementById('events-30d');
|
||||||
|
|
||||||
if (events24h) events24h.textContent = data.events_24h || '-';
|
// Access the nested time_stats object from backend response
|
||||||
if (events7d) events7d.textContent = data.events_7d || '-';
|
const timeStats = data.time_stats || {};
|
||||||
if (events30d) events30d.textContent = data.events_30d || '-';
|
|
||||||
|
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
|
// Populate top pubkeys table
|
||||||
|
|||||||
BIN
c-relay-x86_64
Executable file
BIN
c-relay-x86_64
Executable file
Binary file not shown.
41
src/config.c
41
src/config.c
@@ -16,6 +16,9 @@
|
|||||||
// External database connection (from main.c)
|
// External database connection (from main.c)
|
||||||
extern sqlite3* g_db;
|
extern sqlite3* g_db;
|
||||||
|
|
||||||
|
// External shutdown flag (from main.c)
|
||||||
|
extern volatile sig_atomic_t g_shutdown_flag;
|
||||||
|
|
||||||
// Global unified configuration cache instance
|
// Global unified configuration cache instance
|
||||||
unified_config_cache_t g_unified_cache = {
|
unified_config_cache_t g_unified_cache = {
|
||||||
.cache_lock = PTHREAD_MUTEX_INITIALIZER,
|
.cache_lock = PTHREAD_MUTEX_INITIALIZER,
|
||||||
@@ -3730,6 +3733,44 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error
|
|||||||
snprintf(error_message, error_size, "failed to send system status response");
|
snprintf(error_message, error_size, "failed to send system status response");
|
||||||
return -1;
|
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 {
|
else {
|
||||||
snprintf(error_message, error_size, "invalid: unknown system command '%s'", command);
|
snprintf(error_message, error_size, "invalid: unknown system command '%s'", command);
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
916
src/dm_admin.c
916
src/dm_admin.c
@@ -47,6 +47,79 @@ extern int broadcast_event_to_subscriptions(cJSON* event);
|
|||||||
// Forward declarations for stats generation
|
// Forward declarations for stats generation
|
||||||
extern char* generate_stats_json(void);
|
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
|
// 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);
|
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;
|
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
|
// Generate stats JSON from database queries
|
||||||
char* generate_stats_json(void) {
|
char* generate_stats_json(void) {
|
||||||
extern sqlite3* g_db;
|
extern sqlite3* g_db;
|
||||||
@@ -478,7 +1227,7 @@ char* generate_stats_text(void) {
|
|||||||
|
|
||||||
// Parse the JSON to extract values for human-readable format
|
// Parse the JSON to extract values for human-readable format
|
||||||
cJSON* stats_obj = cJSON_Parse(stats_json);
|
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) {
|
if (!stats_text) {
|
||||||
free(stats_json);
|
free(stats_json);
|
||||||
if (stats_obj) cJSON_Delete(stats_obj);
|
if (stats_obj) cJSON_Delete(stats_obj);
|
||||||
@@ -486,14 +1235,34 @@ char* generate_stats_text(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stats_obj) {
|
if (stats_obj) {
|
||||||
|
// Extract basic metrics
|
||||||
cJSON* total_events = cJSON_GetObjectItem(stats_obj, "total_events");
|
cJSON* total_events = cJSON_GetObjectItem(stats_obj, "total_events");
|
||||||
cJSON* db_size = cJSON_GetObjectItem(stats_obj, "database_size_bytes");
|
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* 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 total = total_events ? (long long)cJSON_GetNumberValue(total_events) : 0;
|
||||||
long long db_bytes = db_size ? (long long)cJSON_GetNumberValue(db_size) : 0;
|
long long db_bytes = db_size ? (long long)cJSON_GetNumberValue(db_size) : 0;
|
||||||
double db_mb = db_bytes / (1024.0 * 1024.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;
|
long long last_24h = 0, last_7d = 0, last_30d = 0;
|
||||||
if (time_stats) {
|
if (time_stats) {
|
||||||
cJSON* h24 = cJSON_GetObjectItem(time_stats, "last_24h");
|
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;
|
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"
|
"📊 Relay Statistics\n"
|
||||||
"━━━━━━━━━━━━━━━━━━━━\n"
|
"━━━━━━━━━━━━━━━━━━━━\n");
|
||||||
"Total Events: %lld\n"
|
|
||||||
"Database Size: %.2f MB (%lld bytes)\n"
|
// Database Overview section
|
||||||
"\n"
|
offset += snprintf(stats_text + offset, 16384 - offset,
|
||||||
"📈 Recent Activity\n"
|
"Database Overview:\n"
|
||||||
"━━━━━━━━━━━━━━━━━━━\n"
|
"Metric\tValue\tDescription\n"
|
||||||
"Last 24 hours: %lld events\n"
|
"Database Size\t%.2f MB (%lld bytes)\tCurrent database file size\n"
|
||||||
"Last 7 days: %lld events\n"
|
"Total Events\t%lld\tTotal number of events stored\n"
|
||||||
"Last 30 days: %lld events\n"
|
"Oldest Event\t%s\tTimestamp of oldest event\n"
|
||||||
"\n"
|
"Newest Event\t%s\tTimestamp of newest event\n"
|
||||||
"✅ Statistics retrieved successfully",
|
"\n",
|
||||||
total, db_mb, db_bytes, last_24h, last_7d, last_30d
|
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);
|
cJSON_Delete(stats_obj);
|
||||||
} else {
|
} else {
|
||||||
// Fallback if JSON parsing fails
|
// Fallback if JSON parsing fails
|
||||||
snprintf(stats_text, 4096,
|
snprintf(stats_text, 16384,
|
||||||
"📊 Relay Statistics\n"
|
"📊 Relay Statistics\n"
|
||||||
"━━━━━━━━━━━━━━━━━━━━\n"
|
"━━━━━━━━━━━━━━━━━━━━\n"
|
||||||
"Raw data: %s\n"
|
"Raw data: %s\n"
|
||||||
@@ -640,6 +1486,11 @@ cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message,
|
|||||||
cJSON_Delete(command_array);
|
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
|
// Get sender pubkey for response from the decrypted DM event
|
||||||
cJSON* sender_pubkey_obj = cJSON_GetObjectItem(inner_dm, "pubkey");
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
else {
|
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");
|
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
|
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
|
// Global state
|
||||||
sqlite3* g_db = NULL; // Non-static so config.c can access it
|
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
|
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
|
struct lws_context *ws_context = NULL; // Non-static so websockets.c can access it
|
||||||
|
|
||||||
// NIP-11 relay information structure
|
// NIP-11 relay information structure
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ extern unified_config_cache_t g_unified_cache;
|
|||||||
// Forward declarations for global state
|
// Forward declarations for global state
|
||||||
extern sqlite3* g_db;
|
extern sqlite3* g_db;
|
||||||
extern int g_server_running;
|
extern int g_server_running;
|
||||||
|
extern volatile sig_atomic_t g_shutdown_flag;
|
||||||
|
extern int g_restart_requested;
|
||||||
extern struct lws_context *ws_context;
|
extern struct lws_context *ws_context;
|
||||||
|
|
||||||
// Global subscription manager
|
// Global subscription manager
|
||||||
@@ -1153,7 +1155,7 @@ int start_websocket_relay(int port_override, int strict_port) {
|
|||||||
log_success(startup_msg);
|
log_success(startup_msg);
|
||||||
|
|
||||||
// Main event loop with proper signal handling
|
// 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);
|
int result = lws_service(ws_context, 1000);
|
||||||
|
|
||||||
if (result < 0) {
|
if (result < 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user