- 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.
1734 lines
66 KiB
C
1734 lines
66 KiB
C
#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;
|
||
} |