v0.4.13 - DM system appears fully functional

This commit is contained in:
Your Name
2025-10-08 07:11:22 -04:00
parent d655258311
commit dcf421ff93
6 changed files with 913 additions and 26 deletions

View File

@@ -3304,9 +3304,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

View File

@@ -1 +1 @@
1220948
1523367

View File

@@ -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