#define _GNU_SOURCE #include "config.h" #include "../nostr_core_lib/nostr_core/nostr_core.h" #include "../nostr_core_lib/nostr_core/nip017.h" #include "../nostr_core_lib/nostr_core/nip044.h" #include #include #include #include #include #include #include #include // External database connection (from main.c) extern sqlite3* g_db; // Logging functions (defined in main.c) extern void log_info(const char* message); extern void log_success(const char* message); extern void log_warning(const char* message); extern void log_error(const char* message); // Forward declarations for unified handlers extern int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi); extern int handle_config_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi); extern int handle_config_set_unified(cJSON* event, const char* config_key, const char* config_value, char* error_message, size_t error_size, struct lws* wsi); extern int handle_config_update_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi); extern int handle_system_command_unified(cJSON* event, const char* command, char* error_message, size_t error_size, struct lws* wsi); extern int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi); extern int handle_auth_rule_modification_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi); // Forward declarations for tag parsing utilities extern const char* get_first_tag_name(cJSON* event); extern const char* get_tag_value(cJSON* event, const char* tag_name, int value_index); // Forward declarations for config functions extern const char* get_relay_pubkey_cached(void); extern char* get_relay_private_key(void); extern const char* get_config_value(const char* key); extern int get_config_bool(const char* key, int default_value); extern const char* get_admin_pubkey_cached(void); // Forward declarations for database functions extern int store_event(cJSON* event); 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); // Forward declarations for NIP-17 processing int is_nip17_gift_wrap_for_relay(cJSON* event); int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t error_size, struct lws* wsi); cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message, size_t error_size, struct lws* wsi); // ================================ // DIRECT MESSAGING ADMIN SYSTEM // ================================ // Process direct command arrays (DM control system) // This handles commands sent as direct JSON arrays, not wrapped in inner events int process_dm_admin_command(cJSON* command_array, cJSON* event, char* error_message, size_t error_size, struct lws* wsi) { if (!command_array || !cJSON_IsArray(command_array) || !event) { log_error("DM Admin: Invalid command array or event"); snprintf(error_message, error_size, "invalid: null command array or event"); return -1; } int array_size = cJSON_GetArraySize(command_array); if (array_size < 1) { log_error("DM Admin: Empty command array"); snprintf(error_message, error_size, "invalid: empty command array"); return -1; } // Get the command type from the first element cJSON* command_item = cJSON_GetArrayItem(command_array, 0); if (!command_item || !cJSON_IsString(command_item)) { log_error("DM Admin: First element is not a string command"); snprintf(error_message, error_size, "invalid: command must be a string"); return -1; } const char* command_type = cJSON_GetStringValue(command_item); // Create synthetic tags from the command array for unified handler compatibility cJSON* synthetic_tags = cJSON_CreateArray(); // Add the command as the first tag cJSON* command_tag = cJSON_CreateArray(); cJSON_AddItemToArray(command_tag, cJSON_CreateString(command_type)); // Add remaining array elements as tag parameters for (int i = 1; i < array_size; i++) { cJSON* param = cJSON_GetArrayItem(command_array, i); if (param) { if (cJSON_IsString(param)) { cJSON_AddItemToArray(command_tag, cJSON_CreateString(cJSON_GetStringValue(param))); } else { // Convert non-string parameters to strings for tag compatibility char* param_str = cJSON_Print(param); if (param_str) { // Remove quotes from JSON string representation if (param_str[0] == '"' && param_str[strlen(param_str)-1] == '"') { param_str[strlen(param_str)-1] = '\0'; cJSON_AddItemToArray(command_tag, cJSON_CreateString(param_str + 1)); } else { cJSON_AddItemToArray(command_tag, cJSON_CreateString(param_str)); } free(param_str); } } } } cJSON_AddItemToArray(synthetic_tags, command_tag); // Add existing event tags cJSON* existing_tags = cJSON_GetObjectItem(event, "tags"); if (existing_tags && cJSON_IsArray(existing_tags)) { cJSON* tag = NULL; cJSON_ArrayForEach(tag, existing_tags) { cJSON_AddItemToArray(synthetic_tags, cJSON_Duplicate(tag, 1)); } } // Temporarily replace event tags with synthetic tags cJSON_ReplaceItemInObject(event, "tags", synthetic_tags); // Route to appropriate handler based on command type int result = -1; if (strcmp(command_type, "auth_query") == 0) { const char* query_type = get_tag_value(event, "auth_query", 1); if (!query_type) { log_error("DM Admin: Missing auth_query type parameter"); snprintf(error_message, error_size, "invalid: missing auth_query type"); } else { result = handle_auth_query_unified(event, query_type, error_message, error_size, wsi); } } else if (strcmp(command_type, "config_query") == 0) { const char* query_type = get_tag_value(event, "config_query", 1); if (!query_type) { log_error("DM Admin: Missing config_query type parameter"); snprintf(error_message, error_size, "invalid: missing config_query type"); } else { result = handle_config_query_unified(event, query_type, error_message, error_size, wsi); } } else if (strcmp(command_type, "config_set") == 0) { const char* config_key = get_tag_value(event, "config_set", 1); const char* config_value = get_tag_value(event, "config_set", 2); if (!config_key || !config_value) { log_error("DM Admin: Missing config_set parameters"); snprintf(error_message, error_size, "invalid: missing config_set key or value"); } else { result = handle_config_set_unified(event, config_key, config_value, error_message, error_size, wsi); } } else if (strcmp(command_type, "config_update") == 0) { result = handle_config_update_unified(event, error_message, error_size, wsi); } else if (strcmp(command_type, "system_command") == 0) { const char* command = get_tag_value(event, "system_command", 1); if (!command) { log_error("DM Admin: Missing system_command type parameter"); snprintf(error_message, error_size, "invalid: missing system_command type"); } else { result = handle_system_command_unified(event, command, error_message, error_size, wsi); } } else if (strcmp(command_type, "stats_query") == 0) { result = handle_stats_query_unified(event, error_message, error_size, wsi); } else if (strcmp(command_type, "whitelist") == 0 || strcmp(command_type, "blacklist") == 0) { result = handle_auth_rule_modification_unified(event, error_message, error_size, wsi); } else { log_error("DM Admin: Unknown command type"); printf(" Unknown command: %s\n", command_type); snprintf(error_message, error_size, "invalid: unknown DM command type '%s'", command_type); } if (result != 0) { log_error("DM Admin: Command processing failed"); } 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; } // Clean up the message - convert to lowercase and trim char clean_msg[512]; size_t msg_len = strlen(message); size_t copy_len = msg_len < sizeof(clean_msg) - 1 ? msg_len : sizeof(clean_msg) - 1; memcpy(clean_msg, message, copy_len); clean_msg[copy_len] = '\0'; // Convert to lowercase for (size_t i = 0; i < copy_len; i++) { if (clean_msg[i] >= 'A' && clean_msg[i] <= 'Z') { clean_msg[i] = clean_msg[i] + 32; } } // Remove leading/trailing whitespace char* start = clean_msg; while (*start == ' ' || *start == '\t') start++; char* end = start + strlen(start) - 1; while (end > start && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) { *end = '\0'; end--; } // Pattern 1: "enable auth" -> "auth_enabled true" if (strstr(start, "enable auth") == start) { strcpy(key, "auth_enabled"); strcpy(value, "true"); return 1; } // Pattern 2: "disable auth" -> "auth_enabled false" if (strstr(start, "disable auth") == start) { strcpy(key, "auth_enabled"); strcpy(value, "false"); return 1; } // Pattern 3: "enable nip42" -> "nip42_auth_required true" if (strstr(start, "enable nip42") == start) { strcpy(key, "nip42_auth_required"); strcpy(value, "true"); return 1; } // Pattern 4: "disable nip42" -> "nip42_auth_required false" if (strstr(start, "disable nip42") == start) { strcpy(key, "nip42_auth_required"); strcpy(value, "false"); return 1; } // Pattern 5: "set KEY to VALUE" or "change KEY to VALUE" char* set_pos = strstr(start, "set "); char* change_pos = strstr(start, "change "); char* to_pos = strstr(start, " to "); if ((set_pos == start || change_pos == start) && to_pos) { char* key_start = (set_pos == start) ? start + 4 : start + 7; size_t key_len = to_pos - key_start; if (key_len > 0 && key_len < 127) { memcpy(key, key_start, key_len); key[key_len] = '\0'; // Trim key char* key_end = key + strlen(key) - 1; while (key_end > key && (*key_end == ' ' || *key_end == '\t')) { *key_end = '\0'; key_end--; } char* value_start = to_pos + 4; strcpy(value, value_start); return 1; } } // Pattern 6: "KEY = VALUE" char* equals_pos = strstr(start, " = "); if (equals_pos) { size_t key_len = equals_pos - start; if (key_len > 0 && key_len < 127) { memcpy(key, start, key_len); key[key_len] = '\0'; strcpy(value, equals_pos + 3); return 1; } } // Pattern 7: "KEY: VALUE" (colon-separated) char* colon_pos = strstr(start, ": "); if (colon_pos) { size_t key_len = colon_pos - start; if (key_len > 0 && key_len < 127) { memcpy(key, start, key_len); key[key_len] = '\0'; strcpy(value, colon_pos + 2); return 1; } } // Pattern 8: "KEY VALUE" (simple space-separated) char* space_pos = strchr(start, ' '); if (space_pos) { size_t key_len = space_pos - start; if (key_len > 0 && key_len < 127) { memcpy(key, start, key_len); key[key_len] = '\0'; strcpy(value, space_pos + 1); return 1; } } return 0; // No pattern matched } // Validate configuration key and value int validate_config_change(const char* key, const char* value) { if (!key || !value) { return 0; } // Find the configuration key int found = 0; const char* expected_type = NULL; int min_val = 0, max_val = 0; for (int i = 0; known_configs[i].key != NULL; i++) { if (strcmp(key, known_configs[i].key) == 0) { found = 1; expected_type = known_configs[i].type; min_val = known_configs[i].min_val; max_val = known_configs[i].max_val; break; } } if (!found) { return 0; // Unknown configuration key } // Validate value based on type if (strcmp(expected_type, "bool") == 0) { if (strcmp(value, "true") == 0 || strcmp(value, "false") == 0 || strcmp(value, "1") == 0 || strcmp(value, "0") == 0 || strcmp(value, "yes") == 0 || strcmp(value, "no") == 0 || strcmp(value, "on") == 0 || strcmp(value, "off") == 0) { return 1; } return 0; } else if (strcmp(expected_type, "int") == 0) { char* endptr; long val = strtol(value, &endptr, 10); if (*endptr != '\0') { return 0; // Not a valid integer } if (val < min_val || val > max_val) { return 0; // Out of range } return 1; } else if (strcmp(expected_type, "string") == 0) { // String values are generally valid, but check length if (strlen(value) > 255) { return 0; // Too long } return 1; } return 0; } // Generate a unique change ID based on admin pubkey and timestamp void generate_change_id(const char* admin_pubkey, char* change_id) { char input[128]; snprintf(input, sizeof(input), "%s_%ld", admin_pubkey, time(NULL)); // Simple hash - just use first 32 chars of the input size_t input_len = strlen(input); for (int i = 0; i < 32 && i < (int)input_len; i++) { change_id[i] = input[i]; } change_id[32] = '\0'; } // Store a pending configuration change char* store_pending_config_change(const char* admin_pubkey, const char* key, const char* old_value, const char* new_value) { if (!admin_pubkey || !key || !old_value || !new_value) { return NULL; } // Clean up expired changes first cleanup_expired_pending_changes(); // Create new pending change pending_config_change_t* change = malloc(sizeof(pending_config_change_t)); if (!change) { return NULL; } strncpy(change->admin_pubkey, admin_pubkey, sizeof(change->admin_pubkey) - 1); change->admin_pubkey[sizeof(change->admin_pubkey) - 1] = '\0'; strncpy(change->config_key, key, sizeof(change->config_key) - 1); change->config_key[sizeof(change->config_key) - 1] = '\0'; strncpy(change->old_value, old_value, sizeof(change->old_value) - 1); change->old_value[sizeof(change->old_value) - 1] = '\0'; strncpy(change->new_value, new_value, sizeof(change->new_value) - 1); change->new_value[sizeof(change->new_value) - 1] = '\0'; change->timestamp = time(NULL); generate_change_id(admin_pubkey, change->change_id); // Add to linked list change->next = pending_changes_head; pending_changes_head = change; pending_changes_count++; // Return a copy of the change ID char* change_id_copy = malloc(33); if (change_id_copy) { strcpy(change_id_copy, change->change_id); } return change_id_copy; } // Find a pending change by admin pubkey and change ID pending_config_change_t* find_pending_change(const char* admin_pubkey, const char* change_id) { if (!admin_pubkey) { return NULL; } pending_config_change_t* current = pending_changes_head; while (current) { if (strcmp(current->admin_pubkey, admin_pubkey) == 0) { if (!change_id || strcmp(current->change_id, change_id) == 0) { return current; } } current = current->next; } return NULL; } // Find the most recent pending change for an admin pending_config_change_t* find_latest_pending_change(const char* admin_pubkey) { if (!admin_pubkey) { return NULL; } pending_config_change_t* latest = NULL; pending_config_change_t* current = pending_changes_head; while (current) { if (strcmp(current->admin_pubkey, admin_pubkey) == 0) { if (!latest || current->timestamp > latest->timestamp) { latest = current; } } current = current->next; } return latest; } // Remove a pending change from the list void remove_pending_change(pending_config_change_t* change_to_remove) { if (!change_to_remove) { return; } if (pending_changes_head == change_to_remove) { pending_changes_head = change_to_remove->next; } else { pending_config_change_t* current = pending_changes_head; while (current && current->next != change_to_remove) { current = current->next; } if (current) { current->next = change_to_remove->next; } } free(change_to_remove); pending_changes_count--; } // Clean up expired pending changes (older than 5 minutes) void cleanup_expired_pending_changes(void) { time_t now = time(NULL); pending_config_change_t* current = pending_changes_head; while (current) { pending_config_change_t* next = current->next; if (now - current->timestamp > CONFIG_CHANGE_TIMEOUT) { remove_pending_change(current); } current = next; } } // Apply a configuration change to the database int apply_config_change(const char* key, const char* value) { if (!key || !value) { return -1; } extern sqlite3* g_db; if (!g_db) { log_error("Database not available for config change"); return -1; } // Normalize boolean values char normalized_value[256]; strncpy(normalized_value, value, sizeof(normalized_value) - 1); normalized_value[sizeof(normalized_value) - 1] = '\0'; // Convert various boolean representations to "true"/"false" if (strcmp(value, "1") == 0 || strcmp(value, "yes") == 0 || strcmp(value, "on") == 0) { strcpy(normalized_value, "true"); } else if (strcmp(value, "0") == 0 || strcmp(value, "no") == 0 || strcmp(value, "off") == 0) { strcpy(normalized_value, "false"); } // Determine the data type based on the configuration key const char* data_type = "string"; // Default to string for (int i = 0; known_configs[i].key != NULL; i++) { if (strcmp(key, known_configs[i].key) == 0) { if (strcmp(known_configs[i].type, "bool") == 0) { data_type = "boolean"; } else if (strcmp(known_configs[i].type, "int") == 0) { data_type = "integer"; } else if (strcmp(known_configs[i].type, "string") == 0) { data_type = "string"; } break; } } // Update or insert the configuration value sqlite3_stmt* stmt; const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type) VALUES (?, ?, ?)"; if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) { log_error("Failed to prepare config update statement"); const char* err_msg = sqlite3_errmsg(g_db); log_error(err_msg); return -1; } sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, normalized_value, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, data_type, -1, SQLITE_STATIC); int result = sqlite3_step(stmt); if (result != SQLITE_DONE) { 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); return 0; } // Generate confirmation message for config change char* generate_config_change_confirmation(const char* key, const char* old_value, const char* new_value) { if (!key || !old_value || !new_value) { return NULL; } char* confirmation = malloc(2048); if (!confirmation) { return NULL; } // Get description for the config key const char* description = ""; if (strcmp(key, "auth_enabled") == 0) { description = "This controls whether authentication is required for the relay."; } else if (strcmp(key, "nip42_auth_required") == 0) { description = "This controls whether NIP-42 authentication is required."; } else if (strcmp(key, "nip40_expiration_enabled") == 0) { description = "This controls whether NIP-40 event expiration is enabled."; } else if (strcmp(key, "max_connections") == 0) { description = "This sets the maximum number of concurrent connections."; } else if (strcmp(key, "max_subscriptions_per_client") == 0) { description = "This sets the maximum subscriptions per client."; } else if (strcmp(key, "pow_min_difficulty") == 0) { description = "This sets the minimum proof-of-work difficulty required."; } else if (strstr(key, "relay_") == key) { description = "This changes relay metadata information."; } snprintf(confirmation, 2048, "šŸ”§ Configuration Change Request\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "\n" "Setting: %s\n" "Current Value: %s\n" "New Value: %s\n" "\n" "%s%s" "\n" "āš ļø Reply with 'yes' to confirm or 'no' to cancel.\n" "ā° This request will expire in 5 minutes.", key, old_value, new_value, strlen(description) > 0 ? "ā„¹ļø " : "", description ); return confirmation; } // Handle confirmation responses (yes/no) int handle_config_confirmation(const char* admin_pubkey, const char* response) { if (!admin_pubkey || !response) { return -1; } // Clean up expired changes first cleanup_expired_pending_changes(); // Convert response to lowercase char response_lower[64]; size_t response_len = strlen(response); size_t copy_len = response_len < sizeof(response_lower) - 1 ? response_len : sizeof(response_lower) - 1; memcpy(response_lower, response, copy_len); response_lower[copy_len] = '\0'; for (size_t i = 0; i < copy_len; i++) { if (response_lower[i] >= 'A' && response_lower[i] <= 'Z') { response_lower[i] = response_lower[i] + 32; } } // Trim whitespace char* start = response_lower; while (*start == ' ' || *start == '\t') start++; char* end = start + strlen(start) - 1; while (end > start && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) { *end = '\0'; end--; } // Check if it's a confirmation response int is_yes = (strcmp(start, "yes") == 0 || strcmp(start, "y") == 0 || strcmp(start, "confirm") == 0 || strcmp(start, "ok") == 0); int is_no = (strcmp(start, "no") == 0 || strcmp(start, "n") == 0 || strcmp(start, "cancel") == 0 || strcmp(start, "abort") == 0); if (!is_yes && !is_no) { return 0; // Not a confirmation response } // Find the most recent pending change for this admin pending_config_change_t* change = find_latest_pending_change(admin_pubkey); if (!change) { return -2; // No pending changes } if (is_yes) { // Apply the configuration change int result = apply_config_change(change->config_key, change->new_value); if (result == 0) { // Send success response char success_msg[1024]; snprintf(success_msg, sizeof(success_msg), "āœ… Configuration Updated\n" "━━━━━━━━━━━━━━━━━━━━━━━━\n" "\n" "%s: %s → %s\n" "\n" "Change applied successfully.", change->config_key, change->old_value, change->new_value ); char error_msg[256]; int send_result = send_nip17_response(admin_pubkey, success_msg, error_msg, sizeof(error_msg)); if (send_result != 0) { log_error(error_msg); } // Remove the pending change remove_pending_change(change); return 1; // Success } else { // Send error response char error_msg[1024]; snprintf(error_msg, sizeof(error_msg), "āŒ Configuration Update Failed\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "\n" "Failed to apply change: %s = %s\n" "\n" "Please check the relay logs for details.", change->config_key, change->new_value ); char send_error_msg[256]; int send_result = send_nip17_response(admin_pubkey, error_msg, send_error_msg, sizeof(send_error_msg)); if (send_result != 0) { log_error(send_error_msg); } // Remove the pending change remove_pending_change(change); return -3; // Application failed } } else if (is_no) { // Cancel the change char cancel_msg[512]; snprintf(cancel_msg, sizeof(cancel_msg), "🚫 Configuration Change Cancelled\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "\n" "Change cancelled: %s\n" "\n" "No changes were made to the relay configuration.", change->config_key ); send_nip17_response(admin_pubkey, cancel_msg, NULL, 0); // Remove the pending change remove_pending_change(change); return 2; // Cancelled } return 0; } // Process a configuration change request int process_config_change_request(const char* admin_pubkey, const char* message) { if (!admin_pubkey || !message) { return -1; } char key[128], value[256]; // Parse the configuration command if (!parse_config_command(message, key, value)) { return 0; // Not a config command } // Validate the configuration change if (!validate_config_change(key, value)) { char error_msg[2048]; snprintf(error_msg, sizeof(error_msg), "āŒ Invalid Configuration\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "\n" "Invalid configuration: %s = %s\n" "\n" "The configuration key '%s' is either unknown or the value '%s' is invalid.\n" "\n" "Supported keys include: auth_enabled, max_connections, default_limit, relay_description, etc.\n" "Use 'config' command to see all current settings.", key, value, key, value ); send_nip17_response(admin_pubkey, error_msg, NULL, 0); return -2; } // Get current value const char* current_value = get_config_value(key); if (!current_value) { current_value = "unset"; } // Check if the value is already set to the requested value if (strcmp(current_value, value) == 0) { char already_set_msg[1024]; snprintf(already_set_msg, sizeof(already_set_msg), "ā„¹ļø Configuration Already Set\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "\n" "%s is already set to: %s\n" "\n" "No change needed.", key, value ); send_nip17_response(admin_pubkey, already_set_msg, NULL, 0); return 3; } // Store the pending change char* change_id = store_pending_config_change(admin_pubkey, key, current_value, value); if (!change_id) { char error_msg[256]; snprintf(error_msg, sizeof(error_msg), "āŒ Failed to store configuration change request.\n" "Please try again." ); send_nip17_response(admin_pubkey, error_msg, NULL, 0); return -3; } // Generate and send confirmation message char* confirmation = generate_config_change_confirmation(key, current_value, value); if (confirmation) { char error_msg[256]; int send_result = send_nip17_response(admin_pubkey, confirmation, error_msg, sizeof(error_msg)); if (send_result != 0) { log_error(error_msg); } free(confirmation); } free(change_id); return 1; // Confirmation sent } // Generate stats JSON from database queries char* generate_stats_json(void) { extern sqlite3* g_db; if (!g_db) { log_error("Database not available for stats generation"); return NULL; } // Build response with database statistics cJSON* response = cJSON_CreateObject(); cJSON_AddStringToObject(response, "query_type", "stats_query"); cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL)); // Get database file size extern char g_database_path[512]; struct stat db_stat; long long db_size = 0; if (stat(g_database_path, &db_stat) == 0) { db_size = db_stat.st_size; } cJSON_AddNumberToObject(response, "database_size_bytes", db_size); // Query total events count sqlite3_stmt* stmt; if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM events", -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { cJSON_AddNumberToObject(response, "total_events", sqlite3_column_int64(stmt, 0)); } sqlite3_finalize(stmt); } // Query event kinds distribution cJSON* event_kinds = cJSON_CreateArray(); if (sqlite3_prepare_v2(g_db, "SELECT kind, count, percentage FROM event_kinds_view ORDER BY count DESC", -1, &stmt, NULL) == SQLITE_OK) { while (sqlite3_step(stmt) == SQLITE_ROW) { cJSON* kind_obj = cJSON_CreateObject(); cJSON_AddNumberToObject(kind_obj, "kind", sqlite3_column_int(stmt, 0)); cJSON_AddNumberToObject(kind_obj, "count", sqlite3_column_int64(stmt, 1)); cJSON_AddNumberToObject(kind_obj, "percentage", sqlite3_column_double(stmt, 2)); cJSON_AddItemToArray(event_kinds, kind_obj); } sqlite3_finalize(stmt); } cJSON_AddItemToObject(response, "event_kinds", event_kinds); // Query time-based statistics cJSON* time_stats = cJSON_CreateObject(); if (sqlite3_prepare_v2(g_db, "SELECT period, total_events FROM time_stats_view", -1, &stmt, NULL) == SQLITE_OK) { while (sqlite3_step(stmt) == SQLITE_ROW) { const char* period = (const char*)sqlite3_column_text(stmt, 0); sqlite3_int64 count = sqlite3_column_int64(stmt, 1); if (strcmp(period, "total") == 0) { cJSON_AddNumberToObject(time_stats, "total", count); } else if (strcmp(period, "24h") == 0) { cJSON_AddNumberToObject(time_stats, "last_24h", count); } else if (strcmp(period, "7d") == 0) { cJSON_AddNumberToObject(time_stats, "last_7d", count); } else if (strcmp(period, "30d") == 0) { cJSON_AddNumberToObject(time_stats, "last_30d", count); } } sqlite3_finalize(stmt); } cJSON_AddItemToObject(response, "time_stats", time_stats); // Query top pubkeys cJSON* top_pubkeys = cJSON_CreateArray(); if (sqlite3_prepare_v2(g_db, "SELECT pubkey, event_count, percentage FROM top_pubkeys_view ORDER BY event_count DESC LIMIT 10", -1, &stmt, NULL) == SQLITE_OK) { while (sqlite3_step(stmt) == SQLITE_ROW) { cJSON* pubkey_obj = cJSON_CreateObject(); const char* pubkey = (const char*)sqlite3_column_text(stmt, 0); cJSON_AddStringToObject(pubkey_obj, "pubkey", pubkey ? pubkey : ""); cJSON_AddNumberToObject(pubkey_obj, "event_count", sqlite3_column_int64(stmt, 1)); cJSON_AddNumberToObject(pubkey_obj, "percentage", sqlite3_column_double(stmt, 2)); cJSON_AddItemToArray(top_pubkeys, pubkey_obj); } sqlite3_finalize(stmt); } cJSON_AddItemToObject(response, "top_pubkeys", top_pubkeys); // Get database creation timestamp (oldest event) if (sqlite3_prepare_v2(g_db, "SELECT MIN(created_at) FROM events", -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { sqlite3_int64 oldest_timestamp = sqlite3_column_int64(stmt, 0); if (oldest_timestamp > 0) { cJSON_AddNumberToObject(response, "database_created_at", (double)oldest_timestamp); } } sqlite3_finalize(stmt); } // Get latest event timestamp if (sqlite3_prepare_v2(g_db, "SELECT MAX(created_at) FROM events", -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { sqlite3_int64 latest_timestamp = sqlite3_column_int64(stmt, 0); if (latest_timestamp > 0) { cJSON_AddNumberToObject(response, "latest_event_at", (double)latest_timestamp); } } sqlite3_finalize(stmt); } // Convert to JSON string char* json_string = cJSON_Print(response); cJSON_Delete(response); if (!json_string) { log_error("Failed to generate stats JSON"); } return json_string; } // Unified NIP-17 response sender - handles all common response logic int send_nip17_response(const char* sender_pubkey, const char* response_content, char* error_message, size_t error_size) { if (!sender_pubkey || !response_content || !error_message) { if (error_message) { strncpy(error_message, "NIP-17: Invalid parameters for response", error_size - 1); } return -1; } // Get relay keys for signing const char* relay_pubkey = get_relay_pubkey_cached(); char* relay_privkey_hex = get_relay_private_key(); if (!relay_pubkey || !relay_privkey_hex) { if (relay_privkey_hex) free(relay_privkey_hex); strncpy(error_message, "NIP-17: Could not get relay keys for response", error_size - 1); return -1; } // Convert relay private key to bytes unsigned char relay_privkey[32]; if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) { free(relay_privkey_hex); strncpy(error_message, "NIP-17: Failed to convert relay private key for response", error_size - 1); return -1; } free(relay_privkey_hex); // Create DM response event using library function cJSON* dm_response = nostr_nip17_create_chat_event( response_content, // message content (const char**)&sender_pubkey, // recipient pubkeys 1, // num recipients NULL, // subject (optional) NULL, // reply_to_event_id (optional) NULL, // reply_relay_url (optional) relay_pubkey // sender pubkey ); if (!dm_response) { strncpy(error_message, "NIP-17: Failed to create DM response event", error_size - 1); return -1; } // Create and sign gift wrap using library function cJSON* gift_wraps[1]; int send_result = nostr_nip17_send_dm( dm_response, // dm_event (const char**)&sender_pubkey, // recipient_pubkeys 1, // num_recipients relay_privkey, // sender_private_key gift_wraps, // gift_wraps_out 1 // max_gift_wraps ); cJSON_Delete(dm_response); if (send_result != 1 || !gift_wraps[0]) { strncpy(error_message, "NIP-17: Failed to create and sign response gift wrap", error_size - 1); return -1; } // Fix the p tag in the gift wrap - library function may use wrong pubkey cJSON* gift_wrap_tags = cJSON_GetObjectItem(gift_wraps[0], "tags"); if (gift_wrap_tags && cJSON_IsArray(gift_wrap_tags)) { // Find and replace the p tag with the correct user pubkey cJSON* tag = NULL; cJSON_ArrayForEach(tag, gift_wrap_tags) { if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) { cJSON* tag_name = cJSON_GetArrayItem(tag, 0); if (tag_name && cJSON_IsString(tag_name) && strcmp(cJSON_GetStringValue(tag_name), "p") == 0) { // Replace the p tag value with the correct user pubkey cJSON_ReplaceItemInArray(tag, 1, cJSON_CreateString(sender_pubkey)); break; } } } } // Store the gift wrap in database int store_result = store_event(gift_wraps[0]); if (store_result != 0) { cJSON_Delete(gift_wraps[0]); strncpy(error_message, "NIP-17: Failed to store response gift wrap", error_size - 1); return -1; } // Broadcast the response event to active subscriptions broadcast_event_to_subscriptions(gift_wraps[0]); cJSON_Delete(gift_wraps[0]); return 0; } // Generate config text from database char* generate_config_text(void) { extern sqlite3* g_db; if (!g_db) { log_error("NIP-17: Database not available for config query"); return NULL; } // Build comprehensive config text from database char* config_text = malloc(8192); if (!config_text) { log_error("NIP-17: Failed to allocate memory for config text"); return NULL; } int offset = 0; // Header offset += snprintf(config_text + offset, 8192 - offset, "šŸ”§ Relay Configuration\n" "━━━━━━━━━━━━━━━━━━━━━━━━\n"); // Query all config values from database sqlite3_stmt* stmt; if (sqlite3_prepare_v2(g_db, "SELECT key, value FROM config ORDER BY key", -1, &stmt, NULL) == SQLITE_OK) { while (sqlite3_step(stmt) == SQLITE_ROW && offset < 8192 - 200) { const char* key = (const char*)sqlite3_column_text(stmt, 0); const char* value = (const char*)sqlite3_column_text(stmt, 1); if (key && value) { offset += snprintf(config_text + offset, 8192 - offset, "%s: %s\n", key, value); } } sqlite3_finalize(stmt); } else { free(config_text); log_error("NIP-17: Failed to query config from database"); return NULL; } // Footer offset += snprintf(config_text + offset, 8192 - offset, "\nāœ… Configuration retrieved successfully"); return config_text; } // Generate human-readable stats text char* generate_stats_text(void) { char* stats_json = generate_stats_json(); if (!stats_json) { log_error("NIP-17: Failed to generate stats for plain text command"); return NULL; } // Parse the JSON to extract values for human-readable format cJSON* stats_obj = cJSON_Parse(stats_json); char* stats_text = malloc(16384); // Increased buffer size for comprehensive stats if (!stats_text) { free(stats_json); if (stats_obj) cJSON_Delete(stats_obj); return NULL; } if (stats_obj) { // Extract basic metrics cJSON* total_events = cJSON_GetObjectItem(stats_obj, "total_events"); cJSON* db_size = cJSON_GetObjectItem(stats_obj, "database_size_bytes"); cJSON* oldest_event = cJSON_GetObjectItem(stats_obj, "database_created_at"); cJSON* newest_event = cJSON_GetObjectItem(stats_obj, "latest_event_at"); cJSON* time_stats = cJSON_GetObjectItem(stats_obj, "time_stats"); cJSON* event_kinds = cJSON_GetObjectItem(stats_obj, "event_kinds"); cJSON* top_pubkeys = cJSON_GetObjectItem(stats_obj, "top_pubkeys"); long long total = total_events ? (long long)cJSON_GetNumberValue(total_events) : 0; long long db_bytes = db_size ? (long long)cJSON_GetNumberValue(db_size) : 0; double db_mb = db_bytes / (1024.0 * 1024.0); // Format timestamps char oldest_str[64] = "-"; char newest_str[64] = "-"; if (oldest_event && cJSON_GetNumberValue(oldest_event) > 0) { time_t oldest_ts = (time_t)cJSON_GetNumberValue(oldest_event); struct tm* tm_info = localtime(&oldest_ts); strftime(oldest_str, sizeof(oldest_str), "%m/%d/%Y, %I:%M:%S %p", tm_info); } if (newest_event && cJSON_GetNumberValue(newest_event) > 0) { time_t newest_ts = (time_t)cJSON_GetNumberValue(newest_event); struct tm* tm_info = localtime(&newest_ts); strftime(newest_str, sizeof(newest_str), "%m/%d/%Y, %I:%M:%S %p", tm_info); } // Extract time-based stats long long last_24h = 0, last_7d = 0, last_30d = 0; if (time_stats) { cJSON* h24 = cJSON_GetObjectItem(time_stats, "last_24h"); cJSON* d7 = cJSON_GetObjectItem(time_stats, "last_7d"); cJSON* d30 = cJSON_GetObjectItem(time_stats, "last_30d"); last_24h = h24 ? (long long)cJSON_GetNumberValue(h24) : 0; last_7d = d7 ? (long long)cJSON_GetNumberValue(d7) : 0; last_30d = d30 ? (long long)cJSON_GetNumberValue(d30) : 0; } // Start building the comprehensive stats text int offset = 0; // Header offset += snprintf(stats_text + offset, 16384 - offset, "šŸ“Š Relay Statistics\n" "━━━━━━━━━━━━━━━━━━━━\n"); // Database Overview section offset += snprintf(stats_text + offset, 16384 - offset, "Database Overview:\n" "Metric\tValue\tDescription\n" "Database Size\t%.2f MB (%lld bytes)\tCurrent database file size\n" "Total Events\t%lld\tTotal number of events stored\n" "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, 16384, "šŸ“Š Relay Statistics\n" "━━━━━━━━━━━━━━━━━━━━\n" "Raw data: %s\n" "\n" "āš ļø Could not parse statistics data", stats_json ); } free(stats_json); return stats_text; } // Main NIP-17 processing function cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message, size_t error_size, struct lws* wsi) { if (!gift_wrap_event || !error_message) { return NULL; } // Step 1: Validate it's addressed to us if (!is_nip17_gift_wrap_for_relay(gift_wrap_event)) { strncpy(error_message, "NIP-17: Event is not a valid gift wrap for this relay", error_size - 1); return NULL; } // Step 2: Get relay private key for decryption char* relay_privkey_hex = get_relay_private_key(); if (!relay_privkey_hex) { strncpy(error_message, "NIP-17: Could not get relay private key for decryption", error_size - 1); return NULL; } // Convert hex private key to bytes unsigned char relay_privkey[32]; if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) { log_error("NIP-17: Failed to convert relay private key from hex"); free(relay_privkey_hex); strncpy(error_message, "NIP-17: Failed to convert relay private key", error_size - 1); return NULL; } free(relay_privkey_hex); // Step 3: Decrypt and parse inner event using library function cJSON* inner_dm = nostr_nip17_receive_dm(gift_wrap_event, relay_privkey); if (!inner_dm) { log_error("NIP-17: nostr_nip17_receive_dm returned NULL"); // Debug: Print the gift wrap event char* gift_wrap_debug = cJSON_Print(gift_wrap_event); if (gift_wrap_debug) { char debug_msg[1024]; snprintf(debug_msg, sizeof(debug_msg), "NIP-17: Gift wrap event: %.500s", gift_wrap_debug); log_error(debug_msg); free(gift_wrap_debug); } // Debug: Check if private key is valid char privkey_hex[65]; for (int i = 0; i < 32; i++) { sprintf(privkey_hex + (i * 2), "%02x", relay_privkey[i]); } privkey_hex[64] = '\0'; strncpy(error_message, "NIP-17: Failed to decrypt and parse inner DM event", error_size - 1); return NULL; } // Step 4: Process admin command int result = process_nip17_admin_command(inner_dm, error_message, error_size, wsi); // Step 5: For plain text commands (stats/config), the response is already handled // Only create a generic response for other command types that don't handle their own responses if (result == 0) { // Extract content to check if it's a plain text command cJSON* content_obj = cJSON_GetObjectItem(inner_dm, "content"); if (content_obj && cJSON_IsString(content_obj)) { const char* dm_content = cJSON_GetStringValue(content_obj); // Check if it's a plain text command that already handled its response char content_lower[256]; size_t content_len = strlen(dm_content); size_t copy_len = content_len < sizeof(content_lower) - 1 ? content_len : sizeof(content_lower) - 1; memcpy(content_lower, dm_content, copy_len); content_lower[copy_len] = '\0'; // Convert to lowercase for (size_t i = 0; i < copy_len; i++) { if (content_lower[i] >= 'A' && content_lower[i] <= 'Z') { content_lower[i] = content_lower[i] + 32; } } // If it's a plain text stats or config command, don't create additional response if (strstr(content_lower, "stats") != NULL || strstr(content_lower, "statistics") != NULL || strstr(content_lower, "config") != NULL || strstr(content_lower, "configuration") != NULL) { cJSON_Delete(inner_dm); return NULL; // No additional response needed } // Check if it's a JSON array command that might be stats cJSON* command_array = cJSON_Parse(dm_content); if (command_array && cJSON_IsArray(command_array) && cJSON_GetArraySize(command_array) > 0) { cJSON* first_item = cJSON_GetArrayItem(command_array, 0); if (cJSON_IsString(first_item) && strcmp(cJSON_GetStringValue(first_item), "stats") == 0) { cJSON_Delete(command_array); cJSON_Delete(inner_dm); return NULL; // No additional response needed } cJSON_Delete(command_array); } } } else if (result > 0) { // Command was handled and response was sent, don't create 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"); if (sender_pubkey_obj && cJSON_IsString(sender_pubkey_obj)) { const char* sender_pubkey = cJSON_GetStringValue(sender_pubkey_obj); // Create success response using library function char response_content[1024]; snprintf(response_content, sizeof(response_content), "[\"command_processed\", \"success\", \"%s\"]", "NIP-17 admin command executed"); // Get relay pubkey for creating DM event const char* relay_pubkey = get_relay_pubkey_cached(); if (relay_pubkey) { cJSON* success_dm = nostr_nip17_create_chat_event( response_content, // message content (const char**)&sender_pubkey, // recipient pubkeys 1, // num recipients NULL, // subject (optional) NULL, // reply_to_event_id (optional) NULL, // reply_relay_url (optional) relay_pubkey // sender pubkey ); if (success_dm) { cJSON* success_gift_wraps[1]; int send_result = nostr_nip17_send_dm( success_dm, // dm_event (const char**)&sender_pubkey, // recipient_pubkeys 1, // num_recipients relay_privkey, // sender_private_key success_gift_wraps, // gift_wraps_out 1 // max_gift_wraps ); cJSON_Delete(success_dm); if (send_result == 1 && success_gift_wraps[0]) { // Store the response gift wrap in database store_event(success_gift_wraps[0]); // Return the response event for broadcasting cJSON_Delete(inner_dm); return success_gift_wraps[0]; } } } } } cJSON_Delete(inner_dm); return NULL; } // Check if decrypted content is a direct command array (DM system) // Returns 1 if it's a valid command array, 0 if it should fall back to inner event parsing int is_dm_command_array(const char* decrypted_content) { if (!decrypted_content) { return 0; } // Quick check: must start with '[' for JSON array if (decrypted_content[0] != '[') { return 0; } // Try to parse as JSON array cJSON* parsed = cJSON_Parse(decrypted_content); if (!parsed) { return 0; } int is_array = cJSON_IsArray(parsed); cJSON_Delete(parsed); return is_array; } // ============================================================================= // NIP-17 GIFT WRAP PROCESSING FUNCTIONS // ============================================================================= // Check if an event is a NIP-17 gift wrap addressed to this relay int is_nip17_gift_wrap_for_relay(cJSON* event) { if (!event || !cJSON_IsObject(event)) { return 0; } // Check kind cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); if (!kind_obj || !cJSON_IsNumber(kind_obj) || (int)cJSON_GetNumberValue(kind_obj) != 1059) { return 0; } // Check tags for "p" tag with relay pubkey cJSON* tags = cJSON_GetObjectItem(event, "tags"); if (!tags || !cJSON_IsArray(tags)) { return 0; } const char* relay_pubkey = get_relay_pubkey_cached(); if (!relay_pubkey) { log_error("NIP-17: Could not get relay pubkey for validation"); return 0; } // Look for "p" tag with relay pubkey cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags) { if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) { cJSON* tag_name = cJSON_GetArrayItem(tag, 0); cJSON* tag_value = cJSON_GetArrayItem(tag, 1); if (tag_name && cJSON_IsString(tag_name) && strcmp(cJSON_GetStringValue(tag_name), "p") == 0 && tag_value && cJSON_IsString(tag_value) && strcmp(cJSON_GetStringValue(tag_value), relay_pubkey) == 0) { return 1; // Found matching p tag } } } return 0; // No matching p tag found } // Process NIP-17 admin command from decrypted DM content int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t error_size, struct lws* wsi) { if (!dm_event || !error_message) { return -1; } // Extract content from DM cJSON* content_obj = cJSON_GetObjectItem(dm_event, "content"); if (!content_obj || !cJSON_IsString(content_obj)) { strncpy(error_message, "NIP-17: DM missing content", error_size - 1); return -1; } const char* dm_content = cJSON_GetStringValue(content_obj); // Check if sender is admin before processing any commands cJSON* sender_pubkey_obj = cJSON_GetObjectItem(dm_event, "pubkey"); if (!sender_pubkey_obj || !cJSON_IsString(sender_pubkey_obj)) { return 0; // Not an error, just treat as user DM } const char* sender_pubkey = cJSON_GetStringValue(sender_pubkey_obj); // Check if sender is admin const char* admin_pubkey = get_admin_pubkey_cached(); int is_admin = admin_pubkey && strlen(admin_pubkey) > 0 && strcmp(sender_pubkey, admin_pubkey) == 0; // Parse DM content as JSON array of commands cJSON* command_array = cJSON_Parse(dm_content); if (!command_array || !cJSON_IsArray(command_array)) { // If content is not a JSON array, check for plain text commands if (is_admin) { // Convert content to lowercase for case-insensitive matching char content_lower[256]; size_t content_len = strlen(dm_content); size_t copy_len = content_len < sizeof(content_lower) - 1 ? content_len : sizeof(content_lower) - 1; memcpy(content_lower, dm_content, copy_len); content_lower[copy_len] = '\0'; // Convert to lowercase for (size_t i = 0; i < copy_len; i++) { if (content_lower[i] >= 'A' && content_lower[i] <= 'Z') { content_lower[i] = content_lower[i] + 32; } } // Check for stats commands if (strstr(content_lower, "stats") != NULL || strstr(content_lower, "statistics") != NULL) { char* stats_text = generate_stats_text(); if (!stats_text) { return -1; } char error_msg[256]; int result = send_nip17_response(sender_pubkey, stats_text, error_msg, sizeof(error_msg)); free(stats_text); if (result != 0) { log_error(error_msg); return -1; } return 0; } // Check for config commands else if (strstr(content_lower, "config") != NULL || strstr(content_lower, "configuration") != NULL) { char* config_text = generate_config_text(); if (!config_text) { return -1; } char error_msg[256]; int result = send_nip17_response(sender_pubkey, config_text, error_msg, sizeof(error_msg)); free(config_text); if (result != 0) { log_error(error_msg); return -1; } 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) { // 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) { 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 } } return 0; // Admin sent unrecognized plain text, treat as user DM } } else { // Not admin, treat as user DM return 0; } } // Check if this is a "stats" command if (cJSON_GetArraySize(command_array) > 0) { cJSON* first_item = cJSON_GetArrayItem(command_array, 0); if (cJSON_IsString(first_item) && strcmp(cJSON_GetStringValue(first_item), "stats") == 0) { // Get sender pubkey for response cJSON* sender_pubkey_obj = cJSON_GetObjectItem(dm_event, "pubkey"); if (!sender_pubkey_obj || !cJSON_IsString(sender_pubkey_obj)) { cJSON_Delete(command_array); strncpy(error_message, "NIP-17: DM missing sender pubkey", error_size - 1); return -1; } const char* sender_pubkey = cJSON_GetStringValue(sender_pubkey_obj); // Generate stats JSON (for JSON array commands, use JSON format) char* stats_json = generate_stats_json(); if (!stats_json) { cJSON_Delete(command_array); strncpy(error_message, "NIP-17: Failed to generate stats", error_size - 1); return -1; } char error_msg[256]; int result = send_nip17_response(sender_pubkey, stats_json, error_msg, sizeof(error_msg)); free(stats_json); cJSON_Delete(command_array); if (result != 0) { log_error(error_msg); strncpy(error_message, error_msg, error_size - 1); return -1; } return 0; } } // For other commands, delegate to existing admin processing // Create a synthetic kind 23456 event with the DM content cJSON* synthetic_event = cJSON_CreateObject(); cJSON_AddNumberToObject(synthetic_event, "kind", 23456); cJSON_AddStringToObject(synthetic_event, "content", dm_content); // Copy pubkey from DM cJSON* pubkey_obj = cJSON_GetObjectItem(dm_event, "pubkey"); if (pubkey_obj && cJSON_IsString(pubkey_obj)) { cJSON_AddStringToObject(synthetic_event, "pubkey", cJSON_GetStringValue(pubkey_obj)); } // Copy tags from DM cJSON* tags = cJSON_GetObjectItem(dm_event, "tags"); if (tags) { cJSON_AddItemToObject(synthetic_event, "tags", cJSON_Duplicate(tags, 1)); } // Process as regular admin event int result = process_admin_event_in_config(synthetic_event, error_message, error_size, wsi); cJSON_Delete(synthetic_event); cJSON_Delete(command_array); return result; }