Files
c-relay/src/dm_admin.c
Your Name d449513861 Add MUSL static binary build system using Alpine Docker
- Create Dockerfile.alpine-musl for truly portable static binaries
- Update build_static.sh to use Docker with sudo fallback
- Fix source code portability issues for MUSL:
  * Add missing headers in config.c, dm_admin.c
  * Remove glibc-specific headers in nip009.c, subscriptions.c
- Update nostr_core_lib submodule with fortification fix
- Add comprehensive documentation in docs/musl_static_build.md

Binary characteristics:
- Size: 7.6MB (vs 12MB+ for glibc static)
- Dependencies: Zero (truly portable)
- Compatibility: Any Linux distribution
- Build time: ~2 minutes with Docker caching

Resolves fortification symbol issues (__snprintf_chk, __fprintf_chk)
that prevented MUSL static linking.
2025-10-11 10:17:20 -04:00

1734 lines
66 KiB
C
Raw Blame History

This file contains invisible Unicode characters

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

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

#define _GNU_SOURCE
#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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <sys/stat.h>
#include <cjson/cJSON.h>
#include <libwebsockets.h>
// 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;
}