#define _POSIX_C_SOURCE 200809L #include "nostr_handler.h" #include #include #include #include #include #include "../../nostr_core_lib/cjson/cJSON.h" #include "../../nostr_core_lib/nostr_core/nostr_core.h" #include "debug.h" #include "trigger_manager.h" #include "nostr_block_list.h" #include "cashu_wallet.h" #include "agent.h" /* * Older nostr_core_lib versions only expose: * - NOSTR_POOL_EOSE_MOST_RECENT * - NOSTR_POOL_EOSE_FIRST * Newer versions may add NOSTR_POOL_EOSE_ANY. * * Provide a compatibility alias so code using NOSTR_POOL_EOSE_ANY * still compiles against older libraries. */ #ifndef NOSTR_POOL_EOSE_ANY #define NOSTR_POOL_EOSE_ANY NOSTR_POOL_EOSE_FIRST #endif #define NIP17_MAX_RELAYS 32 #define NIP17_MAX_GIFT_WRAPS 8 static didactyl_config_t* g_cfg = NULL; static nostr_relay_pool_t* g_pool = NULL; static dm_callback_t g_dm_callback = NULL; static void* g_dm_user_data = NULL; static int g_poll_counter = 0; static time_t g_start_time = 0; static nostr_pool_relay_status_t* g_last_relay_statuses = NULL; static unsigned char* g_startup_published = NULL; static int g_startup_publish_tracking_enabled = 0; static int g_startup_kind1_already_exists = 0; static char g_startup_display_name[128] = "Didactyl"; static char* g_admin_kind0_json = NULL; static char* g_admin_kind10002_json = NULL; static char** g_admin_wot_contacts = NULL; static int g_admin_wot_contact_count = 0; typedef struct { time_t created_at; char* content; } admin_kind1_note_t; static admin_kind1_note_t* g_admin_kind1_notes = NULL; static int g_admin_kind1_note_count = 0; static char* g_agent_kind0_json = NULL; static char* g_agent_kind10002_json = NULL; static char** g_agent_contacts = NULL; static int g_agent_contact_count = 0; static admin_kind1_note_t* g_agent_kind1_notes = NULL; static int g_agent_kind1_note_count = 0; static pthread_mutex_t g_admin_ctx_mutex = PTHREAD_MUTEX_INITIALIZER; typedef enum { MANAGED_SUB_ADMIN_PROFILE = 0, MANAGED_SUB_ADMIN_NOTES, MANAGED_SUB_AGENT_PROFILE, MANAGED_SUB_AGENT_NOTES, MANAGED_SUB_SELF_SKILLS, MANAGED_SUB_ADMIN_SKILLS, MANAGED_SUB_DM_KIND4, MANAGED_SUB_DM_KIND1059, MANAGED_SUB_WALLET, MANAGED_SUB_COUNT } managed_subscription_id_t; typedef struct { const char* name; nostr_pool_subscription_t* handle; cJSON* filter; void (*on_event)(cJSON* event, const char* relay_url, void* user_data); void (*on_eose)(cJSON** events, int event_count, void* user_data); void* user_data; int close_on_eose; int enable_deduplication; nostr_pool_eose_result_mode_t result_mode; int relay_timeout_seconds; int eose_timeout_seconds; int enabled; } managed_subscription_t; typedef struct external_subscription_wrapper { nostr_pool_subscription_t* handle; void (*on_event)(cJSON* event, const char* relay_url, void* user_data); void (*on_eose)(cJSON** events, int event_count, void* user_data); void* user_data; struct external_subscription_wrapper* next; } external_subscription_wrapper_t; static managed_subscription_t g_managed_subs[MANAGED_SUB_COUNT]; static int g_managed_subs_initialized = 0; static pthread_mutex_t g_subscription_mutex = PTHREAD_MUTEX_INITIALIZER; static external_subscription_wrapper_t* g_external_sub_wrappers = NULL; static cJSON* g_self_skill_events = NULL; static pthread_mutex_t g_self_skill_mutex = PTHREAD_MUTEX_INITIALIZER; static volatile int g_self_skill_eose_received = 0; #define DM_DEDUP_CACHE_SIZE 256 static nostr_self_skill_eose_cb_t g_self_skill_eose_cb = NULL; static void* g_self_skill_eose_user_data = NULL; static struct trigger_manager* g_trigger_manager = NULL; static char g_seen_dm_ids[DM_DEDUP_CACHE_SIZE][65]; static int g_seen_dm_count = 0; static int g_seen_dm_next = 0; static pthread_mutex_t g_dm_dedup_mutex = PTHREAD_MUTEX_INITIALIZER; #define SENDER_PROTOCOL_CACHE_SIZE 128 #define DM_HISTORY_RING_SIZE 256 static int didactyl_debug_to_nostr_log_level(void) { switch (g_debug_level) { case DEBUG_LEVEL_TRACE: return NOSTR_LOG_LEVEL_TRACE; case DEBUG_LEVEL_DEBUG: return NOSTR_LOG_LEVEL_DEBUG; case DEBUG_LEVEL_INFO: return NOSTR_LOG_LEVEL_INFO; case DEBUG_LEVEL_WARN: return NOSTR_LOG_LEVEL_WARN; case DEBUG_LEVEL_ERROR: return NOSTR_LOG_LEVEL_ERROR; case DEBUG_LEVEL_NONE: default: return NOSTR_LOG_LEVEL_ERROR; } } static debug_level_t nostr_log_to_didactyl_level(int level) { switch (level) { case NOSTR_LOG_LEVEL_TRACE: return DEBUG_LEVEL_TRACE; case NOSTR_LOG_LEVEL_DEBUG: return DEBUG_LEVEL_DEBUG; case NOSTR_LOG_LEVEL_INFO: return DEBUG_LEVEL_INFO; case NOSTR_LOG_LEVEL_WARN: return DEBUG_LEVEL_WARN; case NOSTR_LOG_LEVEL_ERROR: default: return DEBUG_LEVEL_ERROR; } } static void nostr_core_log_bridge(int level, const char* component, const char* message, void* user_data) { (void)user_data; debug_level_t mapped = nostr_log_to_didactyl_level(level); const char* comp = (component && component[0] != '\0') ? component : "core"; const char* msg = message ? message : ""; switch (mapped) { case DEBUG_LEVEL_TRACE: { int is_websocket = (strcmp(comp, "websocket") == 0); int is_frame = (strncmp(msg, "SEND ", 5) == 0 || strncmp(msg, "RECV ", 5) == 0); size_t msg_len = strlen(msg); if (is_websocket && is_frame && msg_len > 384U) { const char* direction = (strncmp(msg, "SEND ", 5) == 0) ? "SEND" : "RECV"; const char* frame_type = "frame"; if (strstr(msg, "[\"EVENT\"")) frame_type = "EVENT"; else if (strstr(msg, "[\"REQ\"")) frame_type = "REQ"; else if (strstr(msg, "[\"CLOSE\"")) frame_type = "CLOSE"; else if (strstr(msg, "[\"EOSE\"")) frame_type = "EOSE"; else if (strstr(msg, "[\"NOTICE\"")) frame_type = "NOTICE"; DEBUG_TRACE("[didactyl] [nostr:%s] %s ", comp, direction, frame_type, msg_len); break; } DEBUG_TRACE("[didactyl] [nostr:%s] %s", comp, msg); break; } case DEBUG_LEVEL_DEBUG: DEBUG_LOG("[didactyl] [nostr:%s] %s", comp, msg); break; case DEBUG_LEVEL_INFO: DEBUG_INFO("[didactyl] [nostr:%s] %s", comp, msg); break; case DEBUG_LEVEL_WARN: DEBUG_WARN("[didactyl] [nostr:%s] %s", comp, msg); break; case DEBUG_LEVEL_ERROR: default: DEBUG_ERROR("[didactyl] [nostr:%s] %s", comp, msg); break; } } #if defined(__GNUC__) || defined(__clang__) extern void nostr_set_log_callback(nostr_log_callback_t callback, void* user_data) __attribute__((weak)); extern void nostr_set_log_level(nostr_log_level_t level) __attribute__((weak)); #endif static void nostr_core_set_log_bridge_if_supported(void) { #if defined(__GNUC__) || defined(__clang__) if (nostr_set_log_callback) { nostr_set_log_callback(nostr_core_log_bridge, NULL); } if (nostr_set_log_level) { nostr_set_log_level((nostr_log_level_t)didactyl_debug_to_nostr_log_level()); } #endif } static void nostr_core_clear_log_bridge_if_supported(void) { #if defined(__GNUC__) || defined(__clang__) if (nostr_set_log_callback) { nostr_set_log_callback(NULL, NULL); } #endif } typedef struct { char pubkey_hex[65]; dm_protocol_t protocol; time_t seen_at; } sender_protocol_entry_t; typedef struct { time_t created_at; didactyl_dm_history_role_t role; char peer_pubkey_hex[65]; char* content; } dm_history_entry_t; static sender_protocol_entry_t g_sender_protocol_cache[SENDER_PROTOCOL_CACHE_SIZE]; static pthread_mutex_t g_sender_protocol_mutex = PTHREAD_MUTEX_INITIALIZER; static dm_history_entry_t g_dm_history_ring[DM_HISTORY_RING_SIZE]; static int g_dm_history_count = 0; static int g_dm_history_next = 0; static pthread_mutex_t g_dm_history_mutex = PTHREAD_MUTEX_INITIALIZER; static int dm_id_seen_or_remember(const char* event_id_hex) { if (!event_id_hex || strlen(event_id_hex) != 64U) { return 0; } int seen = 0; pthread_mutex_lock(&g_dm_dedup_mutex); for (int i = 0; i < g_seen_dm_count; i++) { if (strncmp(g_seen_dm_ids[i], event_id_hex, 64U) == 0) { seen = 1; break; } } if (!seen) { int slot = 0; if (g_seen_dm_count < DM_DEDUP_CACHE_SIZE) { slot = g_seen_dm_count; g_seen_dm_count++; } else { slot = g_seen_dm_next; g_seen_dm_next = (g_seen_dm_next + 1) % DM_DEDUP_CACHE_SIZE; } memcpy(g_seen_dm_ids[slot], event_id_hex, 64U); g_seen_dm_ids[slot][64] = '\0'; } pthread_mutex_unlock(&g_dm_dedup_mutex); return seen; } static didactyl_sender_tier_t sender_tier_from_pubkey(const char* sender_pubkey_hex) { if (!g_cfg || !sender_pubkey_hex) { return DIDACTYL_SENDER_STRANGER; } if (strcmp(sender_pubkey_hex, g_cfg->admin.pubkey) == 0) { return DIDACTYL_SENDER_ADMIN; } if (g_cfg->security.wot.enabled && nostr_handler_is_wot_contact(sender_pubkey_hex)) { return DIDACTYL_SENDER_WOT; } return DIDACTYL_SENDER_STRANGER; } static void sender_protocol_remember(const char* sender_pubkey_hex, dm_protocol_t protocol) { if (!sender_pubkey_hex || strlen(sender_pubkey_hex) != 64U) { return; } if (protocol != DM_PROTOCOL_NIP04 && protocol != DM_PROTOCOL_NIP17) { return; } pthread_mutex_lock(&g_sender_protocol_mutex); int slot = -1; time_t oldest_time = 0; int oldest_idx = 0; for (int i = 0; i < SENDER_PROTOCOL_CACHE_SIZE; i++) { if (g_sender_protocol_cache[i].pubkey_hex[0] == '\0') { slot = i; break; } if (strncmp(g_sender_protocol_cache[i].pubkey_hex, sender_pubkey_hex, 64U) == 0) { slot = i; break; } if (i == 0 || g_sender_protocol_cache[i].seen_at < oldest_time) { oldest_time = g_sender_protocol_cache[i].seen_at; oldest_idx = i; } } if (slot < 0) { slot = oldest_idx; } memcpy(g_sender_protocol_cache[slot].pubkey_hex, sender_pubkey_hex, 64U); g_sender_protocol_cache[slot].pubkey_hex[64] = '\0'; g_sender_protocol_cache[slot].protocol = protocol; g_sender_protocol_cache[slot].seen_at = time(NULL); pthread_mutex_unlock(&g_sender_protocol_mutex); } static dm_protocol_t sender_protocol_lookup(const char* sender_pubkey_hex) { if (!sender_pubkey_hex || strlen(sender_pubkey_hex) != 64U) { return DM_PROTOCOL_NIP04; } dm_protocol_t out = DM_PROTOCOL_NIP04; pthread_mutex_lock(&g_sender_protocol_mutex); for (int i = 0; i < SENDER_PROTOCOL_CACHE_SIZE; i++) { if (g_sender_protocol_cache[i].pubkey_hex[0] == '\0') { continue; } if (strncmp(g_sender_protocol_cache[i].pubkey_hex, sender_pubkey_hex, 64U) == 0) { out = g_sender_protocol_cache[i].protocol; break; } } pthread_mutex_unlock(&g_sender_protocol_mutex); return out; } static void dm_history_clear_locked(void) { for (int i = 0; i < DM_HISTORY_RING_SIZE; i++) { free(g_dm_history_ring[i].content); g_dm_history_ring[i].content = NULL; g_dm_history_ring[i].created_at = 0; g_dm_history_ring[i].role = DIDACTYL_DM_HISTORY_ASSISTANT; g_dm_history_ring[i].peer_pubkey_hex[0] = '\0'; } g_dm_history_count = 0; g_dm_history_next = 0; } static void dm_history_remember(const char* peer_pubkey_hex, const char* content, didactyl_dm_history_role_t role, time_t created_at) { if (!peer_pubkey_hex || strlen(peer_pubkey_hex) != 64U || !content) { return; } char* dup = strdup(content); if (!dup) { return; } pthread_mutex_lock(&g_dm_history_mutex); int slot = 0; if (g_dm_history_count < DM_HISTORY_RING_SIZE) { slot = g_dm_history_count; g_dm_history_count++; } else { slot = g_dm_history_next; g_dm_history_next = (g_dm_history_next + 1) % DM_HISTORY_RING_SIZE; } free(g_dm_history_ring[slot].content); g_dm_history_ring[slot].content = dup; g_dm_history_ring[slot].created_at = (created_at > 0) ? created_at : time(NULL); g_dm_history_ring[slot].role = role; memcpy(g_dm_history_ring[slot].peer_pubkey_hex, peer_pubkey_hex, 64U); g_dm_history_ring[slot].peer_pubkey_hex[64] = '\0'; pthread_mutex_unlock(&g_dm_history_mutex); } int nostr_handler_dm_history_remember(const char* peer_pubkey_hex, const char* content, didactyl_dm_history_role_t role) { if (!peer_pubkey_hex || !content) { return -1; } dm_history_remember(peer_pubkey_hex, content, role, time(NULL)); return 0; } static const char* relay_status_str(nostr_pool_relay_status_t status) { switch (status) { case NOSTR_POOL_RELAY_DISCONNECTED: return "disconnected"; case NOSTR_POOL_RELAY_CONNECTING: return "connecting"; case NOSTR_POOL_RELAY_CONNECTED: return "connected"; case NOSTR_POOL_RELAY_ERROR: return "error"; default: return "unknown"; } } static void publish_pending_startup_events_for_relay_index(int relay_index, const char* reason); static int publish_kind_event_to_relays(int kind, const char* content, cJSON* tags, const char** relay_urls, int relay_count, const char* reason_label, nostr_publish_result_t* out_result); static void on_admin_context_event(cJSON* event, const char* relay_url, void* user_data); static int parse_kind3_wot_contacts(cJSON* tags); static int parse_kind10002_relays(cJSON* tags); static void upsert_kind1_note(time_t created_at, const char* content); static void free_agent_context_locked(void); static int parse_kind3_agent_contacts(cJSON* tags); static int parse_kind10002_agent_relays(cJSON* tags); static void upsert_agent_kind1_note(time_t created_at, const char* content); static void on_agent_context_event(cJSON* event, const char* relay_url, void* user_data); static int startup_self_kind1_exists(void); static void load_startup_display_name(void); static void build_startup_kind1_content(char* out, size_t out_size, const char* fallback); static void register_trigger_from_self_skill_event(cJSON* event); static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data); static void on_wallet_event(cJSON* event, const char* relay_url, void* user_data); static void dm_history_clear_locked(void); static void dm_history_remember(const char* peer_pubkey_hex, const char* content, didactyl_dm_history_role_t role, time_t created_at); static int relay_list_contains_local(char** relays, int relay_count, const char* relay_url); static void free_relay_list_local(char** relays, int relay_count); static int extract_relays_from_kind10002_tags(cJSON* tags, char*** out_relays, int* out_relay_count); static int merge_relays_into_runtime_config(char** discovered_relays, int discovered_count); static void managed_subscriptions_init_defaults(void); static int managed_subscription_close_locked(managed_subscription_id_t id); static int managed_subscription_apply_locked(managed_subscription_id_t id); static int managed_subscription_register_locked(managed_subscription_id_t id, cJSON* filter, void (*on_event)(cJSON* event, const char* relay_url, void* user_data), void (*on_eose)(cJSON** events, int event_count, void* user_data), void* user_data, int close_on_eose, int enable_deduplication, nostr_pool_eose_result_mode_t result_mode, int relay_timeout_seconds, int eose_timeout_seconds, int enabled); static int managed_subscriptions_resubscribe_all_locked(void); static int managed_subscription_id_from_name(const char* name, managed_subscription_id_t* out_id); static int apply_runtime_kind10002_relays(cJSON* tags, const char* source_label); static int relay_list_contains_local(char** relays, int relay_count, const char* relay_url) { if (!relays || relay_count <= 0 || !relay_url || relay_url[0] == '\0') { return 0; } for (int i = 0; i < relay_count; i++) { if (relays[i] && strcmp(relays[i], relay_url) == 0) { return 1; } } return 0; } static void free_relay_list_local(char** relays, int relay_count) { if (!relays) { return; } for (int i = 0; i < relay_count; i++) { free(relays[i]); } free(relays); } static int extract_relays_from_kind10002_tags(cJSON* tags, char*** out_relays, int* out_relay_count) { if (!out_relays || !out_relay_count) { return -1; } *out_relays = NULL; *out_relay_count = 0; if (!tags || !cJSON_IsArray(tags)) { return 0; } char** relays = NULL; int relay_count = 0; int n = cJSON_GetArraySize(tags); for (int i = 0; i < n; i++) { cJSON* tag = cJSON_GetArrayItem(tags, i); if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { continue; } cJSON* key = cJSON_GetArrayItem(tag, 0); cJSON* val = cJSON_GetArrayItem(tag, 1); if (!key || !val || !cJSON_IsString(key) || !key->valuestring || !cJSON_IsString(val) || !val->valuestring || val->valuestring[0] == '\0') { continue; } if (strcmp(key->valuestring, "r") != 0) { continue; } if (relay_list_contains_local(relays, relay_count, val->valuestring)) { continue; } char** grown = (char**)realloc(relays, (size_t)(relay_count + 1) * sizeof(char*)); if (!grown) { free_relay_list_local(relays, relay_count); return -1; } relays = grown; relays[relay_count] = strdup(val->valuestring); if (!relays[relay_count]) { free_relay_list_local(relays, relay_count); return -1; } relay_count++; } *out_relays = relays; *out_relay_count = relay_count; return relay_count; } static int merge_relays_into_runtime_config(char** discovered_relays, int discovered_count) { if (!g_cfg || discovered_count < 0) { return -1; } int old_count = g_cfg->relay_count; int max_count = old_count + discovered_count; if (max_count <= 0) { return -1; } char** merged = (char**)calloc((size_t)max_count, sizeof(char*)); if (!merged) { return -1; } int merged_count = 0; for (int i = 0; i < old_count; i++) { const char* relay = (g_cfg->relays && g_cfg->relays[i]) ? g_cfg->relays[i] : NULL; if (!relay || relay[0] == '\0' || relay_list_contains_local(merged, merged_count, relay)) { continue; } merged[merged_count] = strdup(relay); if (!merged[merged_count]) { free_relay_list_local(merged, merged_count); return -1; } merged_count++; } int existing_unique_count = merged_count; for (int i = 0; i < discovered_count; i++) { const char* relay = (discovered_relays && discovered_relays[i]) ? discovered_relays[i] : NULL; if (!relay || relay[0] == '\0' || relay_list_contains_local(merged, merged_count, relay)) { continue; } merged[merged_count] = strdup(relay); if (!merged[merged_count]) { free_relay_list_local(merged, merged_count); return -1; } merged_count++; } if (merged_count <= 0) { free(merged); return -1; } if (g_cfg->relays) { for (int i = 0; i < g_cfg->relay_count; i++) { free(g_cfg->relays[i]); } free(g_cfg->relays); } g_cfg->relays = merged; g_cfg->relay_count = merged_count; return merged_count - existing_unique_count; } static void managed_subscriptions_init_defaults(void) { if (g_managed_subs_initialized) { return; } memset(g_managed_subs, 0, sizeof(g_managed_subs)); g_managed_subs[MANAGED_SUB_ADMIN_PROFILE].name = "admin_context_profile"; g_managed_subs[MANAGED_SUB_ADMIN_NOTES].name = "admin_context_notes"; g_managed_subs[MANAGED_SUB_AGENT_PROFILE].name = "agent_context_profile"; g_managed_subs[MANAGED_SUB_AGENT_NOTES].name = "agent_context_notes"; g_managed_subs[MANAGED_SUB_SELF_SKILLS].name = "self_skills"; g_managed_subs[MANAGED_SUB_ADMIN_SKILLS].name = "admin_skills"; g_managed_subs[MANAGED_SUB_DM_KIND4].name = "dms_kind4"; g_managed_subs[MANAGED_SUB_DM_KIND1059].name = "dms_kind1059"; g_managed_subs[MANAGED_SUB_WALLET].name = "wallet_events"; g_managed_subs_initialized = 1; } static int managed_subscription_close_locked(managed_subscription_id_t id) { if (id < 0 || id >= MANAGED_SUB_COUNT) { return -1; } managed_subscription_t* sub = &g_managed_subs[id]; if (sub->handle) { (void)nostr_pool_subscription_close(sub->handle); sub->handle = NULL; } return 0; } static void managed_subscription_on_event_filtered(cJSON* event, const char* relay_url, void* user_data) { managed_subscription_t* sub = (managed_subscription_t*)user_data; if (!sub || !sub->on_event) return; if (nostr_block_list_is_event_filtered(event)) return; sub->on_event(event, relay_url, sub->user_data); } static void managed_subscription_on_eose_passthrough(cJSON** events, int event_count, void* user_data) { managed_subscription_t* sub = (managed_subscription_t*)user_data; if (!sub || !sub->on_eose) return; sub->on_eose(events, event_count, sub->user_data); } static int managed_subscription_apply_locked(managed_subscription_id_t id) { if (id < 0 || id >= MANAGED_SUB_COUNT || !g_cfg || !g_pool) { return -1; } managed_subscription_t* sub = &g_managed_subs[id]; (void)managed_subscription_close_locked(id); if (!sub->enabled) { return 0; } if (!sub->filter || !sub->on_event) { return -1; } cJSON* filter_copy = cJSON_Duplicate(sub->filter, 1); if (!filter_copy) { return -1; } sub->handle = nostr_relay_pool_subscribe( g_pool, (const char**)g_cfg->relays, g_cfg->relay_count, filter_copy, managed_subscription_on_event_filtered, sub->on_eose ? managed_subscription_on_eose_passthrough : NULL, sub, sub->close_on_eose, sub->enable_deduplication, sub->result_mode, sub->relay_timeout_seconds, sub->eose_timeout_seconds); cJSON_Delete(filter_copy); if (!sub->handle) { return -1; } return 0; } static int managed_subscription_register_locked(managed_subscription_id_t id, cJSON* filter, void (*on_event)(cJSON* event, const char* relay_url, void* user_data), void (*on_eose)(cJSON** events, int event_count, void* user_data), void* user_data, int close_on_eose, int enable_deduplication, nostr_pool_eose_result_mode_t result_mode, int relay_timeout_seconds, int eose_timeout_seconds, int enabled) { if (id < 0 || id >= MANAGED_SUB_COUNT) { return -1; } managed_subscriptions_init_defaults(); managed_subscription_t* sub = &g_managed_subs[id]; cJSON* filter_dup = NULL; if (filter) { filter_dup = cJSON_Duplicate(filter, 1); if (!filter_dup) { return -1; } } cJSON_Delete(sub->filter); sub->filter = filter_dup; sub->on_event = on_event; sub->on_eose = on_eose; sub->user_data = user_data; sub->close_on_eose = close_on_eose; sub->enable_deduplication = enable_deduplication; sub->result_mode = result_mode; sub->relay_timeout_seconds = relay_timeout_seconds; sub->eose_timeout_seconds = eose_timeout_seconds; sub->enabled = enabled ? 1 : 0; return managed_subscription_apply_locked(id); } static int managed_subscriptions_resubscribe_all_locked(void) { int rc = 0; managed_subscriptions_init_defaults(); for (int i = 0; i < MANAGED_SUB_COUNT; i++) { if (managed_subscription_apply_locked((managed_subscription_id_t)i) != 0) { rc = -1; } } return rc; } static int managed_subscription_id_from_name(const char* name, managed_subscription_id_t* out_id) { if (!name || !out_id) { return -1; } managed_subscriptions_init_defaults(); for (int i = 0; i < MANAGED_SUB_COUNT; i++) { if (g_managed_subs[i].name && strcmp(g_managed_subs[i].name, name) == 0) { *out_id = (managed_subscription_id_t)i; return 0; } } return -1; } static int apply_runtime_kind10002_relays(cJSON* tags, const char* source_label) { if (!g_cfg || !g_pool || !tags || !cJSON_IsArray(tags)) { return 0; } char** discovered_relays = NULL; int discovered_count = 0; if (extract_relays_from_kind10002_tags(tags, &discovered_relays, &discovered_count) < 0) { return -1; } if (discovered_count <= 0) { free(discovered_relays); return 0; } int added_relays = merge_relays_into_runtime_config(discovered_relays, discovered_count); free_relay_list_local(discovered_relays, discovered_count); if (added_relays < 0) { return -1; } if (added_relays == 0) { return 0; } if (nostr_handler_sync_relays_from_config() != 0) { DEBUG_WARN("[didactyl] runtime relay sync failed after kind10002 update (%s)", source_label ? source_label : "unknown"); return -1; } pthread_mutex_lock(&g_subscription_mutex); managed_subscriptions_init_defaults(); int sub_rc = managed_subscriptions_resubscribe_all_locked(); pthread_mutex_unlock(&g_subscription_mutex); if (sub_rc != 0) { DEBUG_WARN("[didactyl] some subscriptions failed to resubscribe after relay update (%s)", source_label ? source_label : "unknown"); } DEBUG_INFO("[didactyl] runtime relay sync from kind10002 (%s): discovered=%d added=%d total=%d", source_label ? source_label : "unknown", discovered_count, added_relays, g_cfg->relay_count); return 0; } static void log_relay_state_changes(void) { if (!g_pool || !g_cfg || !g_last_relay_statuses) { return; } for (int i = 0; i < g_cfg->relay_count; i++) { const char* relay = g_cfg->relays[i]; nostr_pool_relay_status_t now = nostr_relay_pool_get_relay_status(g_pool, relay); nostr_pool_relay_status_t prev = g_last_relay_statuses[i]; if (now != prev) { const char* conn_err = nostr_relay_pool_get_relay_last_connection_error(g_pool, relay); if (conn_err && (now == NOSTR_POOL_RELAY_DISCONNECTED || now == NOSTR_POOL_RELAY_ERROR)) { DEBUG_INFO("[didactyl] relay state changed: %s %s -> %s (cause: %s)", relay, relay_status_str(prev), relay_status_str(now), conn_err); } else { DEBUG_INFO("[didactyl] relay state changed: %s %s -> %s", relay, relay_status_str(prev), relay_status_str(now)); } g_last_relay_statuses[i] = now; if (now == NOSTR_POOL_RELAY_CONNECTED) { publish_pending_startup_events_for_relay_index(i, "relay_connected"); } } } } void nostr_handler_refresh_relay_statuses(void) { if (!g_pool || !g_cfg || !g_last_relay_statuses) { return; } for (int i = 0; i < g_cfg->relay_count; i++) { g_last_relay_statuses[i] = nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]); } } int nostr_handler_sync_relays_from_config(void) { if (!g_cfg || !g_pool || g_cfg->relay_count <= 0 || !g_cfg->relays) { return -1; } for (int i = 0; i < g_cfg->relay_count; i++) { const char* relay = g_cfg->relays[i]; if (!relay || relay[0] == '\0') { continue; } if (nostr_relay_pool_add_relay(g_pool, relay) != NOSTR_SUCCESS) { DEBUG_WARN("[didactyl] failed to add discovered relay to pool: %s", relay); } } free(g_last_relay_statuses); g_last_relay_statuses = (nostr_pool_relay_status_t*)calloc((size_t)g_cfg->relay_count, sizeof(nostr_pool_relay_status_t)); if (!g_last_relay_statuses) { return -1; } for (int i = 0; i < g_cfg->relay_count; i++) { g_last_relay_statuses[i] = nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]); } return 0; } static void log_publish_targets(const char* action) { if (!g_cfg) { return; } DEBUG_INFO("[didactyl] %s target relays (%d):", action ? action : "publish", g_cfg->relay_count); for (int i = 0; i < g_cfg->relay_count; i++) { DEBUG_INFO("[didactyl] -> %s", g_cfg->relays[i]); } } static int startup_self_kind1_exists(void) { if (!g_cfg || !g_pool) { return 0; } cJSON* filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); cJSON* authors = cJSON_CreateArray(); if (!filter || !kinds || !authors) { cJSON_Delete(filter); cJSON_Delete(kinds); cJSON_Delete(authors); return 0; } cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1)); cJSON_AddItemToObject(filter, "kinds", kinds); cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex)); cJSON_AddItemToObject(filter, "authors", authors); cJSON_AddNumberToObject(filter, "limit", 1); int event_count = 0; cJSON** events = nostr_relay_pool_query_sync( g_pool, (const char**)g_cfg->relays, g_cfg->relay_count, filter, &event_count, 3000); cJSON_Delete(filter); if (events) { for (int i = 0; i < event_count; i++) { cJSON_Delete(events[i]); } free(events); } return event_count > 0 ? 1 : 0; } static void load_startup_display_name(void) { snprintf(g_startup_display_name, sizeof(g_startup_display_name), "%s", "Didactyl"); if (!g_cfg) { return; } for (int i = 0; i < g_cfg->startup_event_count; i++) { startup_event_t* se = &g_cfg->startup_events[i]; if (se->kind != 0 || !se->content) { continue; } cJSON* content_json = cJSON_Parse(se->content); if (!content_json || !cJSON_IsObject(content_json)) { cJSON_Delete(content_json); continue; } cJSON* display_name = cJSON_GetObjectItemCaseSensitive(content_json, "display_name"); cJSON* name = cJSON_GetObjectItemCaseSensitive(content_json, "name"); if (display_name && cJSON_IsString(display_name) && display_name->valuestring && display_name->valuestring[0] != '\0') { snprintf(g_startup_display_name, sizeof(g_startup_display_name), "%s", display_name->valuestring); cJSON_Delete(content_json); return; } if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') { snprintf(g_startup_display_name, sizeof(g_startup_display_name), "%s", name->valuestring); cJSON_Delete(content_json); return; } cJSON_Delete(content_json); } } static void build_startup_kind1_content(char* out, size_t out_size, const char* fallback) { if (!out || out_size == 0) { return; } if (fallback && fallback[0] != '\0') { snprintf(out, out_size, "%s (%s startup)", fallback, g_startup_display_name); } else { snprintf(out, out_size, "%s startup complete and online", g_startup_display_name); } } static const char* find_tag_value_local(cJSON* tags, const char* key) { if (!tags || !cJSON_IsArray(tags) || !key || key[0] == '\0') { return NULL; } int n = cJSON_GetArraySize(tags); for (int i = 0; i < n; i++) { cJSON* tag = cJSON_GetArrayItem(tags, i); if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { continue; } cJSON* k = cJSON_GetArrayItem(tag, 0); cJSON* v = cJSON_GetArrayItem(tag, 1); if (!k || !v || !cJSON_IsString(k) || !k->valuestring || !cJSON_IsString(v) || !v->valuestring) { continue; } if (strcmp(k->valuestring, key) == 0) { return v->valuestring; } } return NULL; } static int nip44_encrypt_self_local(const char* plaintext, char** out_ciphertext) { if (!g_cfg || !out_ciphertext) { return -1; } const char* plain = plaintext ? plaintext : ""; size_t out_cap = (strlen(plain) * 4U) + 1024U; char* ciphertext = (char*)malloc(out_cap); if (!ciphertext) { return -1; } int rc = nostr_nip44_encrypt(g_cfg->keys.private_key, g_cfg->keys.public_key, plain, ciphertext, out_cap); if (rc != NOSTR_SUCCESS) { free(ciphertext); return -1; } *out_ciphertext = ciphertext; return 0; } static int nip44_decrypt_self_local(const char* ciphertext, char** out_plaintext) { if (!g_cfg || !ciphertext || !out_plaintext) { return -1; } size_t out_cap = strlen(ciphertext) + 1024U; char* plaintext = (char*)malloc(out_cap); if (!plaintext) { return -1; } int rc = nostr_nip44_decrypt(g_cfg->keys.private_key, g_cfg->keys.public_key, ciphertext, plaintext, out_cap); if (rc != NOSTR_SUCCESS) { free(plaintext); return -1; } *out_plaintext = plaintext; return 0; } static cJSON* duplicate_public_d_tags_local(cJSON* tags) { cJSON* out = cJSON_CreateArray(); if (!out) { return NULL; } if (!tags || !cJSON_IsArray(tags)) { return out; } int n = cJSON_GetArraySize(tags); for (int i = 0; i < n; i++) { cJSON* tag = cJSON_GetArrayItem(tags, i); if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { continue; } cJSON* key = cJSON_GetArrayItem(tag, 0); if (!key || !cJSON_IsString(key) || !key->valuestring || strcmp(key->valuestring, "d") != 0) { continue; } cJSON* dup = cJSON_Duplicate(tag, 1); if (!dup) { cJSON_Delete(out); return NULL; } cJSON_AddItemToArray(out, dup); } return out; } static cJSON* duplicate_private_non_d_tags_local(cJSON* tags) { cJSON* out = cJSON_CreateArray(); if (!out) { return NULL; } if (!tags || !cJSON_IsArray(tags)) { return out; } int n = cJSON_GetArraySize(tags); for (int i = 0; i < n; i++) { cJSON* tag = cJSON_GetArrayItem(tags, i); if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { continue; } cJSON* key = cJSON_GetArrayItem(tag, 0); if (!key || !cJSON_IsString(key) || !key->valuestring || strcmp(key->valuestring, "d") == 0) { continue; } cJSON* dup = cJSON_Duplicate(tag, 1); if (!dup) { cJSON_Delete(out); return NULL; } cJSON_AddItemToArray(out, dup); } return out; } static int encode_private_skill_event_payload_local(const char* content, cJSON* tags, char** out_encrypted_content, cJSON** out_public_tags) { if (!out_encrypted_content || !out_public_tags) { return -1; } *out_encrypted_content = NULL; *out_public_tags = NULL; cJSON* payload = cJSON_CreateObject(); cJSON* private_tags = duplicate_private_non_d_tags_local(tags); cJSON* public_tags = duplicate_public_d_tags_local(tags); if (!payload || !private_tags || !public_tags) { cJSON_Delete(payload); cJSON_Delete(private_tags); cJSON_Delete(public_tags); return -1; } cJSON_AddStringToObject(payload, "content", content ? content : ""); cJSON_AddItemToObject(payload, "private_tags", private_tags); char* payload_json = cJSON_PrintUnformatted(payload); cJSON_Delete(payload); if (!payload_json) { cJSON_Delete(public_tags); return -1; } char* encrypted_content = NULL; int rc = nip44_encrypt_self_local(payload_json, &encrypted_content); free(payload_json); if (rc != 0) { cJSON_Delete(public_tags); return -1; } *out_encrypted_content = encrypted_content; *out_public_tags = public_tags; return 0; } static int tag_tuple_exists_local(cJSON* tags, const char* key, const char* value) { if (!tags || !cJSON_IsArray(tags) || !key || !value) { return 0; } int n = cJSON_GetArraySize(tags); for (int i = 0; i < n; i++) { cJSON* tag = cJSON_GetArrayItem(tags, i); if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { continue; } cJSON* ex_key = cJSON_GetArrayItem(tag, 0); cJSON* ex_value = cJSON_GetArrayItem(tag, 1); if (!ex_key || !ex_value || !cJSON_IsString(ex_key) || !ex_key->valuestring || !cJSON_IsString(ex_value) || !ex_value->valuestring) { continue; } if (strcmp(ex_key->valuestring, key) == 0 && strcmp(ex_value->valuestring, value) == 0) { return 1; } } return 0; } static int merge_private_tags_into_event_local(cJSON* event, cJSON* private_tags) { if (!event || !cJSON_IsObject(event) || !private_tags || !cJSON_IsArray(private_tags)) { return -1; } cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags"); if (!tags || !cJSON_IsArray(tags)) { tags = cJSON_CreateArray(); if (!tags) { return -1; } cJSON_AddItemToObject(event, "tags", tags); } int n = cJSON_GetArraySize(private_tags); for (int i = 0; i < n; i++) { cJSON* tag = cJSON_GetArrayItem(private_tags, i); if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { continue; } cJSON* key = cJSON_GetArrayItem(tag, 0); cJSON* value = cJSON_GetArrayItem(tag, 1); if (!key || !value || !cJSON_IsString(key) || !key->valuestring || !cJSON_IsString(value) || !value->valuestring) { continue; } if (strcmp(key->valuestring, "d") == 0) { continue; } if (tag_tuple_exists_local(tags, key->valuestring, value->valuestring)) { continue; } cJSON* dup = cJSON_Duplicate(tag, 1); if (!dup) { return -1; } cJSON_AddItemToArray(tags, dup); } return 0; } static int normalize_private_skill_event_local(cJSON* event) { if (!event || !cJSON_IsObject(event)) { return -1; } cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind"); cJSON* content = cJSON_GetObjectItemCaseSensitive(event, "content"); if (!kind || !cJSON_IsNumber(kind) || (int)kind->valuedouble != 31124 || !content || !cJSON_IsString(content) || !content->valuestring) { return 0; } char* plaintext = NULL; if (nip44_decrypt_self_local(content->valuestring, &plaintext) != 0) { return 0; } cJSON* payload = cJSON_Parse(plaintext); if (!payload || !cJSON_IsObject(payload)) { cJSON_Delete(payload); free(plaintext); return 0; } cJSON* payload_content = cJSON_GetObjectItemCaseSensitive(payload, "content"); cJSON* private_tags = cJSON_GetObjectItemCaseSensitive(payload, "private_tags"); if (!payload_content || !cJSON_IsString(payload_content) || !payload_content->valuestring || !private_tags || !cJSON_IsArray(private_tags)) { cJSON_Delete(payload); free(plaintext); return 0; } cJSON* content_item = cJSON_CreateString(payload_content->valuestring); if (content_item) { cJSON_ReplaceItemInObject(event, "content", content_item); } (void)merge_private_tags_into_event_local(event, private_tags); cJSON_Delete(payload); free(plaintext); return 0; } static int parse_enabled_flag_local(const char* enabled_s) { if (!enabled_s || enabled_s[0] == '\0') { return 1; } return (strcmp(enabled_s, "false") == 0 || strcmp(enabled_s, "0") == 0) ? 0 : 1; } static __attribute__((unused)) int parse_skill_address_local(const char* addr, int* out_kind, char out_pubkey_hex[65], char out_d_tag[65]) { if (!addr || !out_kind || !out_pubkey_hex || !out_d_tag) { return -1; } const char* p1 = strchr(addr, ':'); if (!p1) return -1; const char* p2 = strchr(p1 + 1, ':'); if (!p2) return -1; int kind = atoi(addr); if (kind != 31123 && kind != 31124) { return -1; } size_t pub_len = (size_t)(p2 - (p1 + 1)); if (pub_len != 64U) { return -1; } size_t d_len = strlen(p2 + 1); if (d_len == 0U || d_len >= 65U) { return -1; } memcpy(out_pubkey_hex, p1 + 1, pub_len); out_pubkey_hex[pub_len] = '\0'; memcpy(out_d_tag, p2 + 1, d_len + 1U); *out_kind = kind; return 0; } static int self_skill_is_adopted_local(int kind, const char* pubkey_hex, const char* d_tag) { if ((kind != 31123 && kind != 31124) || !pubkey_hex || !d_tag || d_tag[0] == '\0') { return 0; } char addr[256]; snprintf(addr, sizeof(addr), "%d:%s:%s", kind, pubkey_hex, d_tag); int adopted = 0; pthread_mutex_lock(&g_self_skill_mutex); if (g_self_skill_events && cJSON_IsArray(g_self_skill_events)) { int n = cJSON_GetArraySize(g_self_skill_events); for (int i = 0; i < n && !adopted; i++) { cJSON* ev = cJSON_GetArrayItem(g_self_skill_events, i); cJSON* ev_kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL; cJSON* ev_pubkey = ev ? cJSON_GetObjectItemCaseSensitive(ev, "pubkey") : NULL; cJSON* ev_tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL; if (!ev_kind || !cJSON_IsNumber(ev_kind) || (int)ev_kind->valuedouble != 10123 || !ev_pubkey || !cJSON_IsString(ev_pubkey) || !ev_pubkey->valuestring || strcmp(ev_pubkey->valuestring, pubkey_hex) != 0 || !ev_tags || !cJSON_IsArray(ev_tags)) { continue; } int tag_count = cJSON_GetArraySize(ev_tags); for (int ti = 0; ti < tag_count; ti++) { cJSON* tag = cJSON_GetArrayItem(ev_tags, ti); if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { continue; } cJSON* k = cJSON_GetArrayItem(tag, 0); cJSON* v = cJSON_GetArrayItem(tag, 1); if (!k || !v || !cJSON_IsString(k) || !k->valuestring || !cJSON_IsString(v) || !v->valuestring) { continue; } if (strcmp(k->valuestring, "a") == 0 && strcmp(v->valuestring, addr) == 0) { adopted = 1; break; } } } } pthread_mutex_unlock(&g_self_skill_mutex); return adopted; } static void register_trigger_from_self_skill_event(cJSON* event) { if (!event || !g_cfg || !g_trigger_manager) { return; } cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind"); cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey"); cJSON* content = cJSON_GetObjectItemCaseSensitive(event, "content"); cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags"); if (!kind || !cJSON_IsNumber(kind) || !pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring || !content || !cJSON_IsString(content) || !content->valuestring || !tags || !cJSON_IsArray(tags)) { return; } int kind_val = (int)kind->valuedouble; if (kind_val != 31123 && kind_val != 31124) { return; } if (strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) != 0) { return; } const char* d_tag = find_tag_value_local(tags, "d"); const char* trigger = find_tag_value_local(tags, "trigger"); const char* filter = find_tag_value_local(tags, "filter"); const char* action = find_tag_value_local(tags, "action"); const char* enabled = find_tag_value_local(tags, "enabled"); int trigger_supported = trigger && (strcmp(trigger, "nostr-subscription") == 0 || strcmp(trigger, "webhook") == 0 || strcmp(trigger, "cron") == 0 || strcmp(trigger, "chain") == 0 || strcmp(trigger, "dm") == 0); if (!d_tag || d_tag[0] == '\0' || !trigger_supported || !filter || filter[0] == '\0') { return; } if (!self_skill_is_adopted_local(kind_val, pubkey->valuestring, d_tag)) { DEBUG_INFO("[didactyl] live self-skill trigger ignored (not adopted) d_tag=%s", d_tag); return; } if (action && strcmp(action, "template") == 0) { DEBUG_WARN("[didactyl] live self-skill trigger action template is deprecated; forcing llm for d_tag=%s", d_tag); } trigger_action_type_t action_type = TRIGGER_ACTION_LLM; int is_enabled = parse_enabled_flag_local(enabled); const char* llm = find_tag_value_local(tags, "llm"); const char* tools = find_tag_value_local(tags, "tools"); const char* max_tokens_s = find_tag_value_local(tags, "max_tokens"); const char* temperature_s = find_tag_value_local(tags, "temperature"); const char* seed_s = find_tag_value_local(tags, "seed"); int has_max_tokens = (max_tokens_s && max_tokens_s[0] != '\0') ? 1 : 0; int max_tokens = has_max_tokens ? atoi(max_tokens_s) : 0; int has_temperature = (temperature_s && temperature_s[0] != '\0') ? 1 : 0; double temperature = has_temperature ? atof(temperature_s) : 0.0; int has_seed = (seed_s && seed_s[0] != '\0') ? 1 : 0; int seed = has_seed ? atoi(seed_s) : 0; int rc = trigger_manager_add((trigger_manager_t*)g_trigger_manager, d_tag, content->valuestring, filter, action_type, trigger, is_enabled, llm, tools, has_max_tokens, max_tokens, has_temperature, temperature, has_seed, seed); if (rc == 0) { DEBUG_INFO("[didactyl] live self-skill trigger registered d_tag=%s action=%s enabled=%d", d_tag, "llm", is_enabled); } else { DEBUG_WARN("[didactyl] live self-skill trigger register failed d_tag=%s", d_tag); } } static void self_skill_cache_upsert_event_locked(cJSON* event) { if (!event || !cJSON_IsObject(event)) { return; } cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind"); cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey"); cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags"); cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at"); if (!kind || !cJSON_IsNumber(kind) || !pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring || !tags || !cJSON_IsArray(tags)) { return; } int kind_val = (int)kind->valuedouble; if (kind_val != 31123 && kind_val != 31124 && kind_val != 10123) { return; } if (!g_cfg) { return; } int is_agent_author = (strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) == 0); int is_admin_author = (g_cfg->admin.pubkey[0] != '\0' && strcmp(pubkey->valuestring, g_cfg->admin.pubkey) == 0); if (kind_val == 10123) { if (!is_agent_author && !is_admin_author) { return; } } if (is_admin_author && kind_val == 31124) { return; } const char* d_tag = find_tag_value_local(tags, "d"); if ((kind_val == 31123 || kind_val == 31124) && (!d_tag || d_tag[0] == '\0')) { return; } if (!g_self_skill_events || !cJSON_IsArray(g_self_skill_events)) { cJSON_Delete(g_self_skill_events); g_self_skill_events = cJSON_CreateArray(); if (!g_self_skill_events) { return; } } double created = (created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0.0; int n = cJSON_GetArraySize(g_self_skill_events); for (int i = 0; i < n; i++) { cJSON* existing = cJSON_GetArrayItem(g_self_skill_events, i); if (!existing || !cJSON_IsObject(existing)) { continue; } cJSON* ex_kind = cJSON_GetObjectItemCaseSensitive(existing, "kind"); cJSON* ex_pubkey = cJSON_GetObjectItemCaseSensitive(existing, "pubkey"); cJSON* ex_tags = cJSON_GetObjectItemCaseSensitive(existing, "tags"); if (!ex_kind || !cJSON_IsNumber(ex_kind) || !ex_pubkey || !cJSON_IsString(ex_pubkey) || !ex_pubkey->valuestring || !ex_tags || !cJSON_IsArray(ex_tags)) { continue; } int ex_kind_val = (int)ex_kind->valuedouble; if (ex_kind_val != kind_val || strcmp(ex_pubkey->valuestring, pubkey->valuestring) != 0) { continue; } if (kind_val == 31123 || kind_val == 31124) { const char* ex_d = find_tag_value_local(ex_tags, "d"); if (!ex_d || !d_tag || strcmp(ex_d, d_tag) != 0) { continue; } } cJSON* ex_created_at = cJSON_GetObjectItemCaseSensitive(existing, "created_at"); double ex_created = (ex_created_at && cJSON_IsNumber(ex_created_at)) ? ex_created_at->valuedouble : 0.0; if (created < ex_created) { return; } cJSON* dup = cJSON_Duplicate(event, 1); if (!dup) { return; } cJSON_ReplaceItemInArray(g_self_skill_events, i, dup); return; } cJSON* dup = cJSON_Duplicate(event, 1); if (!dup) { return; } cJSON_AddItemToArray(g_self_skill_events, dup); } static int hex_to_pubkey(const char* hex, unsigned char out_pubkey[32]) { if (!hex || !out_pubkey || strlen(hex) != 64U) { return -1; } return nostr_hex_to_bytes(hex, out_pubkey, 32) == 0 ? 0 : -1; } static int duplicate_relay_list(const char** relay_urls, int relay_count, char*** out_relays) { if (!out_relays) { return -1; } *out_relays = NULL; if (!relay_urls || relay_count <= 0) { return 0; } char** relays = (char**)calloc((size_t)relay_count, sizeof(char*)); if (!relays) { return -1; } for (int i = 0; i < relay_count; i++) { relays[i] = strdup(relay_urls[i] ? relay_urls[i] : ""); if (!relays[i]) { for (int j = 0; j < i; j++) { free(relays[j]); } free(relays); return -1; } } *out_relays = relays; return 0; } void nostr_handler_publish_result_free(nostr_publish_result_t* result) { if (!result) { return; } if (result->relays) { for (int i = 0; i < result->relay_count; i++) { free(result->relays[i]); } free(result->relays); } memset(result, 0, sizeof(*result)); } static void extract_d_tag(cJSON* tags, char* out_d_tag, size_t out_size) { if (!out_d_tag || out_size == 0) { return; } out_d_tag[0] = '\0'; if (!tags || !cJSON_IsArray(tags)) { return; } int tag_count = cJSON_GetArraySize(tags); for (int i = 0; i < tag_count; i++) { cJSON* tag = cJSON_GetArrayItem(tags, i); if (!tag || !cJSON_IsArray(tag)) { continue; } cJSON* key = cJSON_GetArrayItem(tag, 0); cJSON* value = cJSON_GetArrayItem(tag, 1); if (!key || !value || !cJSON_IsString(key) || !cJSON_IsString(value) || !key->valuestring || !value->valuestring) { continue; } if (strcmp(key->valuestring, "d") == 0) { snprintf(out_d_tag, out_size, "%s", value->valuestring); return; } } } static void fill_publish_result(nostr_publish_result_t* out_result, int kind, cJSON* tags, const char* event_id_hex, const char** relay_urls, int relay_count, int accepted_by_pool_count) { if (!out_result) { return; } nostr_handler_publish_result_free(out_result); out_result->kind = kind; out_result->relay_count = relay_count; out_result->accepted_by_pool_count = accepted_by_pool_count; out_result->success = accepted_by_pool_count > 0 ? 1 : 0; if (event_id_hex) { snprintf(out_result->event_id, sizeof(out_result->event_id), "%s", event_id_hex); unsigned char event_id_bytes[32]; char note_bech32[128]; if (strlen(event_id_hex) == 64U && nostr_hex_to_bytes(event_id_hex, event_id_bytes, sizeof(event_id_bytes)) == 0 && nostr_key_to_bech32(event_id_bytes, "note", note_bech32) == 0) { snprintf(out_result->note_uri, sizeof(out_result->note_uri), "nostr:%s", note_bech32); } } extract_d_tag(tags, out_result->d_tag, sizeof(out_result->d_tag)); (void)kind; (void)relay_urls; (void)duplicate_relay_list(relay_urls, relay_count, &out_result->relays); } static cJSON* create_dm_tags_for_recipient(const char* recipient_pubkey_hex) { cJSON* tags = cJSON_CreateArray(); if (!tags) { return NULL; } cJSON* p_tag = cJSON_CreateArray(); if (!p_tag) { cJSON_Delete(tags); return NULL; } cJSON_AddItemToArray(p_tag, cJSON_CreateString("p")); cJSON_AddItemToArray(p_tag, cJSON_CreateString(recipient_pubkey_hex)); cJSON_AddItemToArray(tags, p_tag); return tags; } static int extract_first_p_tag(cJSON* tags, char out_pubkey_hex[65]) { if (!tags || !cJSON_IsArray(tags)) { return -1; } int n = cJSON_GetArraySize(tags); for (int i = 0; i < n; i++) { cJSON* tag = cJSON_GetArrayItem(tags, i); if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { continue; } cJSON* key = cJSON_GetArrayItem(tag, 0); cJSON* val = cJSON_GetArrayItem(tag, 1); if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val)) { continue; } if (strcmp(key->valuestring, "p") == 0 && strlen(val->valuestring) == 64U) { memcpy(out_pubkey_hex, val->valuestring, 65U); return 0; } } return -1; } static void trace_event_json(const char* prefix, cJSON* event) { if (g_debug_level < DEBUG_LEVEL_TRACE || !event) { return; } char* event_json = cJSON_PrintUnformatted(event); if (!event_json) { DEBUG_TRACE("[didactyl] %s ", prefix ? prefix : "event"); return; } DEBUG_TRACE("[didactyl] %s %s", prefix ? prefix : "event", event_json); free(event_json); } static void trace_plaintext_dm(const char* prefix, const char* plaintext) { if (g_debug_level < DEBUG_LEVEL_TRACE) { return; } DEBUG_TRACE("[didactyl] %s %s", prefix ? prefix : "dm plaintext", plaintext ? plaintext : ""); } static void on_event(cJSON* event, const char* relay_url, void* user_data) { (void)user_data; DEBUG_TRACE("[didactyl] DEBUG on_event ENTRY from %s (event=%p g_cfg=%p g_dm_callback=%s)", relay_url ? relay_url : "NULL", (void*)event, (void*)g_cfg, g_dm_callback ? "set" : "NULL"); if (!event || !g_cfg || !g_dm_callback) { DEBUG_TRACE("[didactyl] DEBUG on_event NULL guard: event=%p g_cfg=%p g_dm_callback=%s", (void*)event, (void*)g_cfg, g_dm_callback ? "set" : "NULL"); return; } if (g_cfg->security.verify_signatures && nostr_verify_event_signature(event) != 0) { DEBUG_WARN("[didactyl] dropped event with invalid signature via %s", relay_url ? relay_url : "unknown relay"); return; } cJSON* id = cJSON_GetObjectItemCaseSensitive(event, "id"); cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind"); cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey"); cJSON* content = cJSON_GetObjectItemCaseSensitive(event, "content"); cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags"); if (!kind || !pubkey || !content || !tags || !cJSON_IsNumber(kind) || !cJSON_IsString(pubkey) || !cJSON_IsString(content)) { DEBUG_TRACE("[didactyl] DEBUG on_event: missing required fields (kind=%p pubkey=%p content=%p tags=%p)", (void*)kind, (void*)pubkey, (void*)content, (void*)tags); return; } const char* event_id_hex = (id && cJSON_IsString(id) && id->valuestring && strlen(id->valuestring) == 64U) ? id->valuestring : NULL; int kind_val = (int)kind->valuedouble; DEBUG_TRACE("[didactyl] DEBUG on_event: kind=%d id=%.16s... from=%.16s... via %s", kind_val, event_id_hex ? event_id_hex : "", pubkey->valuestring ? pubkey->valuestring : "", relay_url ? relay_url : "unknown"); char sender_pubkey_hex[65] = {0}; char* decrypted = NULL; const char* dedup_id_hex = event_id_hex; dm_protocol_t received_protocol = DM_PROTOCOL_NIP04; if (kind_val == 4) { if (g_cfg->dm_protocol == DM_PROTOCOL_NIP17) { DEBUG_TRACE("[didactyl] DEBUG on_event: ignoring kind4 in dm_protocol=nip17 mode"); return; } char recipient_pubkey_hex[65] = {0}; if (extract_first_p_tag(tags, recipient_pubkey_hex) != 0) { DEBUG_TRACE("[didactyl] DEBUG on_event: no p-tag found in kind4 event %.16s...", event_id_hex ? event_id_hex : ""); return; } if (strcmp(recipient_pubkey_hex, g_cfg->keys.public_key_hex) != 0) { DEBUG_TRACE("[didactyl] DEBUG on_event: p-tag mismatch (got=%.16s... want=%.16s...)", recipient_pubkey_hex, g_cfg->keys.public_key_hex); return; } memcpy(sender_pubkey_hex, pubkey->valuestring, 65U); unsigned char sender_pubkey[32]; if (hex_to_pubkey(sender_pubkey_hex, sender_pubkey) != 0) { return; } decrypted = (char*)malloc(NOSTR_NIP04_MAX_PLAINTEXT_SIZE); if (!decrypted) { fprintf(stderr, "[didactyl] failed to allocate DM decrypt buffer\n"); return; } decrypted[0] = '\0'; trace_event_json("received encrypted DM event:", event); if (nostr_nip04_decrypt(g_cfg->keys.private_key, sender_pubkey, content->valuestring, decrypted, NOSTR_NIP04_MAX_PLAINTEXT_SIZE) != NOSTR_SUCCESS) { fprintf(stdout, "[didactyl] failed to decrypt incoming DM from %.16s...\n", sender_pubkey_hex); free(decrypted); return; } trace_plaintext_dm("received decrypted DM content:", decrypted); received_protocol = DM_PROTOCOL_NIP04; } else if (kind_val == 1059) { if (g_cfg->dm_protocol == DM_PROTOCOL_NIP04) { DEBUG_TRACE("[didactyl] DEBUG on_event: ignoring kind1059 in dm_protocol=nip04 mode"); return; } cJSON* rumor = nostr_nip17_receive_dm(event, g_cfg->keys.private_key); if (!rumor) { DEBUG_TRACE("[didactyl] DEBUG on_event: failed to unwrap/decrypt NIP-17 gift wrap %.16s...", event_id_hex ? event_id_hex : ""); return; } cJSON* rumor_id = cJSON_GetObjectItemCaseSensitive(rumor, "id"); cJSON* rumor_kind = cJSON_GetObjectItemCaseSensitive(rumor, "kind"); cJSON* rumor_pubkey = cJSON_GetObjectItemCaseSensitive(rumor, "pubkey"); cJSON* rumor_content = cJSON_GetObjectItemCaseSensitive(rumor, "content"); cJSON* rumor_created_at = cJSON_GetObjectItemCaseSensitive(rumor, "created_at"); if (!rumor_kind || !rumor_pubkey || !rumor_content || !rumor_created_at || !cJSON_IsNumber(rumor_kind) || !cJSON_IsString(rumor_pubkey) || !cJSON_IsString(rumor_content) || !cJSON_IsNumber(rumor_created_at) || !rumor_pubkey->valuestring || strlen(rumor_pubkey->valuestring) != 64U) { cJSON_Delete(rumor); return; } time_t rumor_created_at_ts = (time_t)rumor_created_at->valuedouble; if (rumor_created_at_ts < g_start_time) { DEBUG_TRACE("[didactyl] DEBUG on_event: skipping old NIP-17 rumor created_at=%ld start=%ld", (long)rumor_created_at_ts, (long)g_start_time); cJSON_Delete(rumor); return; } int rumor_kind_val = (int)rumor_kind->valuedouble; if (rumor_kind_val != 14 && rumor_kind_val != 15 && rumor_kind_val != 7) { DEBUG_TRACE("[didactyl] DEBUG on_event: ignoring NIP-17 rumor kind=%d", rumor_kind_val); cJSON_Delete(rumor); return; } memcpy(sender_pubkey_hex, rumor_pubkey->valuestring, 65U); const char* rumor_id_hex = (rumor_id && cJSON_IsString(rumor_id) && rumor_id->valuestring && strlen(rumor_id->valuestring) == 64U) ? rumor_id->valuestring : NULL; if (rumor_id_hex) { dedup_id_hex = rumor_id_hex; } decrypted = strdup(rumor_content->valuestring ? rumor_content->valuestring : ""); cJSON_Delete(rumor); if (!decrypted) { return; } trace_plaintext_dm("received NIP-17 DM content:", decrypted); received_protocol = DM_PROTOCOL_NIP17; } else { DEBUG_TRACE("[didactyl] DEBUG on_event: ignoring unsupported kind=%d", kind_val); return; } if (!sender_pubkey_hex[0] || strlen(sender_pubkey_hex) != 64U) { free(decrypted); return; } if (strcmp(sender_pubkey_hex, g_cfg->keys.public_key_hex) == 0) { DEBUG_TRACE("[didactyl] DEBUG on_event: ignoring self-sent DM %.16s... via %s", sender_pubkey_hex, relay_url ? relay_url : "unknown relay"); free(decrypted); return; } didactyl_sender_tier_t tier = sender_tier_from_pubkey(sender_pubkey_hex); DEBUG_TRACE("[didactyl] DEBUG on_event: sender=%.16s... tier=%d (admin=%.16s...)", sender_pubkey_hex, (int)tier, g_cfg->admin.pubkey); sender_protocol_remember(sender_pubkey_hex, received_protocol); if (tier == DIDACTYL_SENDER_STRANGER) { if (!g_cfg->security.stranger.enabled) { DEBUG_LOG("[didactyl] ignored DM from stranger %.16s... via %s", sender_pubkey_hex, relay_url ? relay_url : "unknown relay"); free(decrypted); return; } if (g_cfg->security.stranger_response[0] != '\0') { (void)nostr_handler_send_dm_auto(sender_pubkey_hex, g_cfg->security.stranger_response); } free(decrypted); return; } if (dedup_id_hex && dm_id_seen_or_remember(dedup_id_hex)) { DEBUG_LOG("[didactyl] skipped duplicate DM event %.16s... from %.16s... via %s", dedup_id_hex, sender_pubkey_hex, relay_url ? relay_url : "unknown relay"); free(decrypted); return; } DEBUG_INFO("[didactyl] received kind %d event %.16s... from %.16s... via %s tier=%d protocol=%s", kind_val, dedup_id_hex ? dedup_id_hex : "", sender_pubkey_hex, relay_url ? relay_url : "unknown relay", (int)tier, received_protocol == DM_PROTOCOL_NIP17 ? "nip17" : "nip04"); g_dm_callback(sender_pubkey_hex, decrypted, tier, g_dm_user_data); free(decrypted); } static void on_eose(cJSON** events, int event_count, void* user_data) { (void)events; (void)user_data; DEBUG_TRACE("[didactyl] DEBUG on_eose called: event_count=%d", event_count); } static void on_self_skill_eose(cJSON** events, int event_count, void* user_data) { (void)events; (void)user_data; g_self_skill_eose_received = 1; DEBUG_INFO("[didactyl] self-skill EOSE received: cached skill events=%d", event_count); if (g_self_skill_eose_cb) { g_self_skill_eose_cb(event_count, g_self_skill_eose_user_data); } } static void free_admin_context_locked(void) { free(g_admin_kind0_json); g_admin_kind0_json = NULL; free(g_admin_kind10002_json); g_admin_kind10002_json = NULL; if (g_admin_wot_contacts) { for (int i = 0; i < g_admin_wot_contact_count; i++) { free(g_admin_wot_contacts[i]); } free(g_admin_wot_contacts); } g_admin_wot_contacts = NULL; g_admin_wot_contact_count = 0; if (g_admin_kind1_notes) { for (int i = 0; i < g_admin_kind1_note_count; i++) { free(g_admin_kind1_notes[i].content); } free(g_admin_kind1_notes); } g_admin_kind1_notes = NULL; g_admin_kind1_note_count = 0; } static void free_agent_context_locked(void) { free(g_agent_kind0_json); g_agent_kind0_json = NULL; free(g_agent_kind10002_json); g_agent_kind10002_json = NULL; if (g_agent_contacts) { for (int i = 0; i < g_agent_contact_count; i++) { free(g_agent_contacts[i]); } free(g_agent_contacts); } g_agent_contacts = NULL; g_agent_contact_count = 0; if (g_agent_kind1_notes) { for (int i = 0; i < g_agent_kind1_note_count; i++) { free(g_agent_kind1_notes[i].content); } free(g_agent_kind1_notes); } g_agent_kind1_notes = NULL; g_agent_kind1_note_count = 0; } static int parse_kind3_wot_contacts(cJSON* tags) { if (!tags || !cJSON_IsArray(tags)) { return 0; } if (g_admin_wot_contacts) { for (int i = 0; i < g_admin_wot_contact_count; i++) { free(g_admin_wot_contacts[i]); } free(g_admin_wot_contacts); g_admin_wot_contacts = NULL; g_admin_wot_contact_count = 0; } int n = cJSON_GetArraySize(tags); for (int i = 0; i < n; i++) { cJSON* tag = cJSON_GetArrayItem(tags, i); if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { continue; } cJSON* key = cJSON_GetArrayItem(tag, 0); cJSON* val = cJSON_GetArrayItem(tag, 1); if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) { continue; } if (strcmp(key->valuestring, "p") != 0 || strlen(val->valuestring) != 64U) { continue; } char* dup = strdup(val->valuestring); if (!dup) { return -1; } char** grown = (char**)realloc(g_admin_wot_contacts, (size_t)(g_admin_wot_contact_count + 1) * sizeof(char*)); if (!grown) { free(dup); return -1; } g_admin_wot_contacts = grown; g_admin_wot_contacts[g_admin_wot_contact_count++] = dup; } return 0; } static int parse_kind10002_relays(cJSON* tags) { if (!tags || !cJSON_IsArray(tags)) { free(g_admin_kind10002_json); g_admin_kind10002_json = strdup("[]"); return g_admin_kind10002_json ? 0 : -1; } cJSON* relays = cJSON_CreateArray(); if (!relays) { return -1; } int n = cJSON_GetArraySize(tags); for (int i = 0; i < n; i++) { cJSON* tag = cJSON_GetArrayItem(tags, i); if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { continue; } cJSON* key = cJSON_GetArrayItem(tag, 0); cJSON* val = cJSON_GetArrayItem(tag, 1); if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) { continue; } if (strcmp(key->valuestring, "r") != 0 || val->valuestring[0] == '\0') { continue; } cJSON_AddItemToArray(relays, cJSON_CreateString(val->valuestring)); } char* relays_json = cJSON_PrintUnformatted(relays); cJSON_Delete(relays); if (!relays_json) { return -1; } free(g_admin_kind10002_json); g_admin_kind10002_json = relays_json; return 0; } static void upsert_kind1_note(time_t created_at, const char* content) { if (!content) { return; } int limit = g_cfg->admin_context.kind_1_limit > 0 ? g_cfg->admin_context.kind_1_limit : 10; if (limit > 256) { limit = 256; } char* dup = strdup(content); if (!dup) { return; } admin_kind1_note_t* grown = (admin_kind1_note_t*)realloc(g_admin_kind1_notes, (size_t)(g_admin_kind1_note_count + 1) * sizeof(admin_kind1_note_t)); if (!grown) { free(dup); return; } g_admin_kind1_notes = grown; g_admin_kind1_notes[g_admin_kind1_note_count].created_at = created_at; g_admin_kind1_notes[g_admin_kind1_note_count].content = dup; g_admin_kind1_note_count++; while (g_admin_kind1_note_count > limit) { free(g_admin_kind1_notes[0].content); memmove(&g_admin_kind1_notes[0], &g_admin_kind1_notes[1], (size_t)(g_admin_kind1_note_count - 1) * sizeof(admin_kind1_note_t)); g_admin_kind1_note_count--; } } static int parse_kind3_agent_contacts(cJSON* tags) { if (!tags || !cJSON_IsArray(tags)) { return 0; } if (g_agent_contacts) { for (int i = 0; i < g_agent_contact_count; i++) { free(g_agent_contacts[i]); } free(g_agent_contacts); g_agent_contacts = NULL; g_agent_contact_count = 0; } int n = cJSON_GetArraySize(tags); for (int i = 0; i < n; i++) { cJSON* tag = cJSON_GetArrayItem(tags, i); if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { continue; } cJSON* key = cJSON_GetArrayItem(tag, 0); cJSON* val = cJSON_GetArrayItem(tag, 1); if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) { continue; } if (strcmp(key->valuestring, "p") != 0 || strlen(val->valuestring) != 64U) { continue; } char* dup = strdup(val->valuestring); if (!dup) { return -1; } char** grown = (char**)realloc(g_agent_contacts, (size_t)(g_agent_contact_count + 1) * sizeof(char*)); if (!grown) { free(dup); return -1; } g_agent_contacts = grown; g_agent_contacts[g_agent_contact_count++] = dup; } return 0; } static int parse_kind10002_agent_relays(cJSON* tags) { if (!tags || !cJSON_IsArray(tags)) { free(g_agent_kind10002_json); g_agent_kind10002_json = strdup("[]"); return g_agent_kind10002_json ? 0 : -1; } cJSON* relays = cJSON_CreateArray(); if (!relays) { return -1; } int n = cJSON_GetArraySize(tags); for (int i = 0; i < n; i++) { cJSON* tag = cJSON_GetArrayItem(tags, i); if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { continue; } cJSON* key = cJSON_GetArrayItem(tag, 0); cJSON* val = cJSON_GetArrayItem(tag, 1); if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) { continue; } if (strcmp(key->valuestring, "r") != 0 || val->valuestring[0] == '\0') { continue; } cJSON_AddItemToArray(relays, cJSON_CreateString(val->valuestring)); } char* relays_json = cJSON_PrintUnformatted(relays); cJSON_Delete(relays); if (!relays_json) { return -1; } free(g_agent_kind10002_json); g_agent_kind10002_json = relays_json; return 0; } static void upsert_agent_kind1_note(time_t created_at, const char* content) { if (!content) { return; } int limit = g_cfg && g_cfg->admin_context.kind_1_limit > 0 ? g_cfg->admin_context.kind_1_limit : 10; if (limit > 256) { limit = 256; } char* dup = strdup(content); if (!dup) { return; } admin_kind1_note_t* grown = (admin_kind1_note_t*)realloc(g_agent_kind1_notes, (size_t)(g_agent_kind1_note_count + 1) * sizeof(admin_kind1_note_t)); if (!grown) { free(dup); return; } g_agent_kind1_notes = grown; g_agent_kind1_notes[g_agent_kind1_note_count].created_at = created_at; g_agent_kind1_notes[g_agent_kind1_note_count].content = dup; g_agent_kind1_note_count++; while (g_agent_kind1_note_count > limit) { free(g_agent_kind1_notes[0].content); memmove(&g_agent_kind1_notes[0], &g_agent_kind1_notes[1], (size_t)(g_agent_kind1_note_count - 1) * sizeof(admin_kind1_note_t)); g_agent_kind1_note_count--; } } static void on_admin_context_event(cJSON* event, const char* relay_url, void* user_data) { (void)relay_url; (void)user_data; if (!event || !g_cfg || !g_cfg->admin_context.enabled) { return; } if (g_cfg->security.verify_signatures && nostr_verify_event_signature(event) != 0) { return; } cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind"); cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey"); cJSON* content = cJSON_GetObjectItemCaseSensitive(event, "content"); cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags"); cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at"); if (!kind || !pubkey || !cJSON_IsNumber(kind) || !cJSON_IsString(pubkey) || !pubkey->valuestring) { return; } if (strcmp(pubkey->valuestring, g_cfg->admin.pubkey) != 0) { return; } int k = (int)kind->valuedouble; pthread_mutex_lock(&g_admin_ctx_mutex); if (k == 0 && g_cfg->admin_context.track_kind_0 && content && cJSON_IsString(content) && content->valuestring) { free(g_admin_kind0_json); g_admin_kind0_json = strdup(content->valuestring); } else if (k == 3 && g_cfg->admin_context.track_kind_3 && tags && cJSON_IsArray(tags)) { (void)parse_kind3_wot_contacts(tags); } else if (k == 10002 && g_cfg->admin_context.track_kind_10002 && tags && cJSON_IsArray(tags)) { (void)parse_kind10002_relays(tags); } else if (k == 1 && g_cfg->admin_context.track_kind_1 && content && cJSON_IsString(content) && content->valuestring) { time_t ts = (created_at && cJSON_IsNumber(created_at)) ? (time_t)created_at->valuedouble : time(NULL); upsert_kind1_note(ts, content->valuestring); } pthread_mutex_unlock(&g_admin_ctx_mutex); } static void on_agent_context_event(cJSON* event, const char* relay_url, void* user_data) { (void)user_data; if (!event || !g_cfg) { return; } if (g_cfg->security.verify_signatures && nostr_verify_event_signature(event) != 0) { return; } cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind"); cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey"); cJSON* content = cJSON_GetObjectItemCaseSensitive(event, "content"); cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags"); cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at"); if (!kind || !pubkey || !cJSON_IsNumber(kind) || !cJSON_IsString(pubkey) || !pubkey->valuestring) { return; } if (strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) != 0) { return; } int k = (int)kind->valuedouble; int runtime_sync_needed = 0; pthread_mutex_lock(&g_admin_ctx_mutex); if (k == 0 && content && cJSON_IsString(content) && content->valuestring) { free(g_agent_kind0_json); g_agent_kind0_json = strdup(content->valuestring); } else if (k == 3 && tags && cJSON_IsArray(tags)) { (void)parse_kind3_agent_contacts(tags); } else if (k == 10002 && tags && cJSON_IsArray(tags)) { (void)parse_kind10002_agent_relays(tags); runtime_sync_needed = 1; } else if (k == 1 && content && cJSON_IsString(content) && content->valuestring) { time_t ts = (created_at && cJSON_IsNumber(created_at)) ? (time_t)created_at->valuedouble : time(NULL); upsert_agent_kind1_note(ts, content->valuestring); } pthread_mutex_unlock(&g_admin_ctx_mutex); if (runtime_sync_needed) { (void)apply_runtime_kind10002_relays(tags, relay_url ? relay_url : "agent_context"); } } static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data) { (void)user_data; if (!event || !g_cfg) { return; } if (g_cfg->security.verify_signatures && nostr_verify_event_signature(event) != 0) { return; } cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind"); int kind_val = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : -1; if (kind_val == 31124) { (void)normalize_private_skill_event_local(event); } cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags"); cJSON* id = cJSON_GetObjectItemCaseSensitive(event, "id"); cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at"); const char* d_tag = (tags && cJSON_IsArray(tags)) ? find_tag_value_local(tags, "d") : NULL; DEBUG_INFO("[didactyl] self-skill cache received kind=%d d_tag=%s id=%s created_at=%lld via %s", kind_val, (d_tag && d_tag[0] != '\0') ? d_tag : "-", (id && cJSON_IsString(id) && id->valuestring) ? id->valuestring : "", (long long)((created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0), relay_url ? relay_url : "unknown relay"); register_trigger_from_self_skill_event(event); pthread_mutex_lock(&g_self_skill_mutex); self_skill_cache_upsert_event_locked(event); pthread_mutex_unlock(&g_self_skill_mutex); agent_invalidate_adopted_skills_cache(); } static void on_wallet_event(cJSON* event, const char* relay_url, void* user_data) { (void)event; (void)relay_url; (void)user_data; (void)cashu_wallet_load_from_relays(); } int nostr_handler_init(didactyl_config_t* config) { if (!config) { return -1; } g_cfg = config; g_poll_counter = 0; g_start_time = time(NULL); memset(g_seen_dm_ids, 0, sizeof(g_seen_dm_ids)); g_seen_dm_count = 0; g_seen_dm_next = 0; pthread_mutex_lock(&g_dm_history_mutex); dm_history_clear_locked(); pthread_mutex_unlock(&g_dm_history_mutex); managed_subscriptions_init_defaults(); pthread_mutex_lock(&g_subscription_mutex); for (int i = 0; i < MANAGED_SUB_COUNT; i++) { managed_subscription_t* sub = &g_managed_subs[i]; sub->enabled = 0; sub->on_event = NULL; sub->on_eose = NULL; sub->user_data = NULL; sub->close_on_eose = 0; sub->enable_deduplication = 0; sub->result_mode = NOSTR_POOL_EOSE_FULL_SET; sub->relay_timeout_seconds = 30; sub->eose_timeout_seconds = 120; (void)managed_subscription_close_locked((managed_subscription_id_t)i); cJSON_Delete(sub->filter); sub->filter = NULL; } pthread_mutex_unlock(&g_subscription_mutex); DEBUG_INFO("[didactyl] initializing relay pool with %d relays", g_cfg->relay_count); nostr_core_set_log_bridge_if_supported(); nostr_pool_reconnect_config_t reconnect = *nostr_pool_reconnect_config_default(); reconnect.enable_auto_reconnect = 1; reconnect.ping_interval_seconds = 20; reconnect.pong_timeout_seconds = 30; g_pool = nostr_relay_pool_create(&reconnect); if (!g_pool) { return -1; } if (nostr_relay_pool_set_auth(g_pool, g_cfg->keys.private_key, 1) != NOSTR_SUCCESS) { fprintf(stderr, "[didactyl] failed to enable relay pool auth\n"); return -1; } for (int i = 0; i < g_cfg->relay_count; i++) { if (nostr_relay_pool_add_relay(g_pool, g_cfg->relays[i]) != NOSTR_SUCCESS) { fprintf(stderr, "[didactyl] failed to add relay: %s\n", g_cfg->relays[i]); return -1; } DEBUG_INFO("[didactyl] added relay: %s", g_cfg->relays[i]); } free(g_last_relay_statuses); g_last_relay_statuses = (nostr_pool_relay_status_t*)calloc((size_t)g_cfg->relay_count, sizeof(nostr_pool_relay_status_t)); if (!g_last_relay_statuses) { return -1; } for (int i = 0; i < g_cfg->relay_count; i++) { g_last_relay_statuses[i] = nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]); } free(g_startup_published); g_startup_published = NULL; g_startup_publish_tracking_enabled = 0; pthread_mutex_lock(&g_self_skill_mutex); cJSON_Delete(g_self_skill_events); g_self_skill_events = cJSON_CreateArray(); pthread_mutex_unlock(&g_self_skill_mutex); if (!g_self_skill_events) { return -1; } return 0; } int nostr_handler_subscribe_admin_context(void) { if (!g_cfg || !g_pool || !g_cfg->admin_context.enabled) { return 0; } int rc = 0; cJSON* profile_filter = cJSON_CreateObject(); cJSON* profile_kinds = cJSON_CreateArray(); cJSON* profile_authors = cJSON_CreateArray(); if (!profile_filter || !profile_kinds || !profile_authors) { cJSON_Delete(profile_filter); cJSON_Delete(profile_kinds); cJSON_Delete(profile_authors); return -1; } if (g_cfg->admin_context.track_kind_0) cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(0)); if (g_cfg->admin_context.track_kind_3) cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(3)); if (g_cfg->admin_context.track_kind_10002) cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(10002)); int profile_enabled = cJSON_GetArraySize(profile_kinds) > 0 ? 1 : 0; if (profile_enabled) { cJSON_AddItemToObject(profile_filter, "kinds", profile_kinds); cJSON_AddItemToArray(profile_authors, cJSON_CreateString(g_cfg->admin.pubkey)); cJSON_AddItemToObject(profile_filter, "authors", profile_authors); cJSON_AddNumberToObject(profile_filter, "limit", 32); } else { cJSON_Delete(profile_kinds); cJSON_Delete(profile_authors); } pthread_mutex_lock(&g_subscription_mutex); managed_subscriptions_init_defaults(); if (managed_subscription_register_locked(MANAGED_SUB_ADMIN_PROFILE, profile_enabled ? profile_filter : NULL, on_admin_context_event, on_eose, NULL, 0, 1, NOSTR_POOL_EOSE_FULL_SET, 30, 120, profile_enabled) != 0) { rc = -1; } pthread_mutex_unlock(&g_subscription_mutex); cJSON_Delete(profile_filter); int notes_enabled = g_cfg->admin_context.track_kind_1 ? 1 : 0; cJSON* notes_filter = NULL; if (notes_enabled) { notes_filter = cJSON_CreateObject(); cJSON* notes_kinds = cJSON_CreateArray(); cJSON* notes_authors = cJSON_CreateArray(); if (!notes_filter || !notes_kinds || !notes_authors) { cJSON_Delete(notes_filter); cJSON_Delete(notes_kinds); cJSON_Delete(notes_authors); return -1; } int kind1_limit = g_cfg->admin_context.kind_1_limit > 0 ? g_cfg->admin_context.kind_1_limit : 10; if (kind1_limit > 256) { kind1_limit = 256; } cJSON_AddItemToArray(notes_kinds, cJSON_CreateNumber(1)); cJSON_AddItemToObject(notes_filter, "kinds", notes_kinds); cJSON_AddItemToArray(notes_authors, cJSON_CreateString(g_cfg->admin.pubkey)); cJSON_AddItemToObject(notes_filter, "authors", notes_authors); cJSON_AddNumberToObject(notes_filter, "limit", kind1_limit); } pthread_mutex_lock(&g_subscription_mutex); managed_subscriptions_init_defaults(); if (managed_subscription_register_locked(MANAGED_SUB_ADMIN_NOTES, notes_filter, on_admin_context_event, on_eose, NULL, 0, 1, NOSTR_POOL_EOSE_FULL_SET, 30, 120, notes_enabled) != 0) { rc = -1; } pthread_mutex_unlock(&g_subscription_mutex); cJSON_Delete(notes_filter); if (rc == 0) { DEBUG_INFO("[didactyl] admin context subscriptions active for admin %.16s...", g_cfg->admin.pubkey); } return rc; } int nostr_handler_subscribe_agent_context(void) { if (!g_cfg || !g_pool) { return -1; } int rc = 0; cJSON* profile_filter = cJSON_CreateObject(); cJSON* profile_kinds = cJSON_CreateArray(); cJSON* profile_authors = cJSON_CreateArray(); if (!profile_filter || !profile_kinds || !profile_authors) { cJSON_Delete(profile_filter); cJSON_Delete(profile_kinds); cJSON_Delete(profile_authors); return -1; } cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(0)); cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(3)); cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(10002)); cJSON_AddItemToObject(profile_filter, "kinds", profile_kinds); cJSON_AddItemToArray(profile_authors, cJSON_CreateString(g_cfg->keys.public_key_hex)); cJSON_AddItemToObject(profile_filter, "authors", profile_authors); cJSON_AddNumberToObject(profile_filter, "limit", 64); pthread_mutex_lock(&g_subscription_mutex); managed_subscriptions_init_defaults(); if (managed_subscription_register_locked(MANAGED_SUB_AGENT_PROFILE, profile_filter, on_agent_context_event, on_eose, NULL, 0, 1, NOSTR_POOL_EOSE_FULL_SET, 30, 120, 1) != 0) { rc = -1; } pthread_mutex_unlock(&g_subscription_mutex); cJSON_Delete(profile_filter); cJSON* notes_filter = cJSON_CreateObject(); cJSON* notes_kinds = cJSON_CreateArray(); cJSON* notes_authors = cJSON_CreateArray(); if (!notes_filter || !notes_kinds || !notes_authors) { cJSON_Delete(notes_filter); cJSON_Delete(notes_kinds); cJSON_Delete(notes_authors); return -1; } int kind1_limit = g_cfg->admin_context.kind_1_limit > 0 ? g_cfg->admin_context.kind_1_limit : 10; if (kind1_limit > 256) { kind1_limit = 256; } cJSON_AddItemToArray(notes_kinds, cJSON_CreateNumber(1)); cJSON_AddItemToObject(notes_filter, "kinds", notes_kinds); cJSON_AddItemToArray(notes_authors, cJSON_CreateString(g_cfg->keys.public_key_hex)); cJSON_AddItemToObject(notes_filter, "authors", notes_authors); cJSON_AddNumberToObject(notes_filter, "limit", kind1_limit); pthread_mutex_lock(&g_subscription_mutex); managed_subscriptions_init_defaults(); if (managed_subscription_register_locked(MANAGED_SUB_AGENT_NOTES, notes_filter, on_agent_context_event, on_eose, NULL, 0, 1, NOSTR_POOL_EOSE_FULL_SET, 30, 120, 1) != 0) { rc = -1; } pthread_mutex_unlock(&g_subscription_mutex); cJSON_Delete(notes_filter); if (rc == 0) { DEBUG_INFO("[didactyl] agent self-context subscriptions active for pubkey %.16s...", g_cfg->keys.public_key_hex); } return rc; } void nostr_handler_set_trigger_manager(struct trigger_manager* trigger_manager) { g_trigger_manager = trigger_manager; } int nostr_handler_subscribe_self_skills(void) { if (!g_cfg || !g_pool) { return -1; } int rc = 0; g_self_skill_eose_received = 0; cJSON* filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); cJSON* authors = cJSON_CreateArray(); if (!filter || !kinds || !authors) { cJSON_Delete(filter); cJSON_Delete(kinds); cJSON_Delete(authors); return -1; } cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31123)); cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31124)); cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10123)); cJSON_AddItemToObject(filter, "kinds", kinds); cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex)); cJSON_AddItemToObject(filter, "authors", authors); cJSON_AddNumberToObject(filter, "limit", 300); pthread_mutex_lock(&g_subscription_mutex); managed_subscriptions_init_defaults(); if (managed_subscription_register_locked(MANAGED_SUB_SELF_SKILLS, filter, on_self_skill_event, on_self_skill_eose, NULL, 0, 0, NOSTR_POOL_EOSE_ANY, 12, 14, 1) != 0) { rc = -1; } pthread_mutex_unlock(&g_subscription_mutex); cJSON_Delete(filter); int admin_enabled = (g_cfg->admin.pubkey[0] != '\0' && strcmp(g_cfg->admin.pubkey, g_cfg->keys.public_key_hex) != 0) ? 1 : 0; cJSON* admin_filter = NULL; if (admin_enabled) { admin_filter = cJSON_CreateObject(); cJSON* admin_kinds = cJSON_CreateArray(); cJSON* admin_authors = cJSON_CreateArray(); if (!admin_filter || !admin_kinds || !admin_authors) { cJSON_Delete(admin_filter); cJSON_Delete(admin_kinds); cJSON_Delete(admin_authors); return -1; } cJSON_AddItemToArray(admin_kinds, cJSON_CreateNumber(31123)); cJSON_AddItemToObject(admin_filter, "kinds", admin_kinds); cJSON_AddItemToArray(admin_authors, cJSON_CreateString(g_cfg->admin.pubkey)); cJSON_AddItemToObject(admin_filter, "authors", admin_authors); cJSON_AddNumberToObject(admin_filter, "limit", 150); } pthread_mutex_lock(&g_subscription_mutex); managed_subscriptions_init_defaults(); if (managed_subscription_register_locked(MANAGED_SUB_ADMIN_SKILLS, admin_filter, on_self_skill_event, on_eose, NULL, 0, 0, NOSTR_POOL_EOSE_FULL_SET, 30, 120, admin_enabled) != 0) { rc = -1; } pthread_mutex_unlock(&g_subscription_mutex); cJSON_Delete(admin_filter); if (rc == 0) { DEBUG_INFO("[didactyl] self/admin skill subscriptions active (self=%.16s..., admin=%.16s...)", g_cfg->keys.public_key_hex, g_cfg->admin.pubkey); } return rc; } int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) { if (!g_cfg || !g_pool || !callback) { return -1; } g_dm_callback = callback; g_dm_user_data = user_data; const int need_kind4 = (g_cfg->dm_protocol == DM_PROTOCOL_NIP04 || g_cfg->dm_protocol == DM_PROTOCOL_BOTH) ? 1 : 0; const int need_kind1059 = (g_cfg->dm_protocol == DM_PROTOCOL_NIP17 || g_cfg->dm_protocol == DM_PROTOCOL_BOTH) ? 1 : 0; DEBUG_INFO("[didactyl] DM subscribe: dm_protocol=%d need_kind4=%d need_kind1059=%d g_start_time=%ld", (int)g_cfg->dm_protocol, need_kind4, need_kind1059, (long)g_start_time); if (!need_kind4 && !need_kind1059) { DEBUG_WARN("[didactyl] DM subscription skipped: no protocol selected"); return -1; } int rc = 0; cJSON* filter4 = NULL; if (need_kind4) { cJSON* kinds4 = NULL; cJSON* p_values4 = NULL; filter4 = cJSON_CreateObject(); kinds4 = cJSON_CreateArray(); p_values4 = cJSON_CreateArray(); if (!filter4 || !kinds4 || !p_values4) { cJSON_Delete(filter4); cJSON_Delete(kinds4); cJSON_Delete(p_values4); return -1; } cJSON_AddItemToArray(kinds4, cJSON_CreateNumber(4)); cJSON_AddItemToObject(filter4, "kinds", kinds4); cJSON_AddItemToArray(p_values4, cJSON_CreateString(g_cfg->keys.public_key_hex)); cJSON_AddItemToObject(filter4, "#p", p_values4); cJSON_AddNumberToObject(filter4, "since", (double)g_start_time); cJSON_AddNumberToObject(filter4, "limit", 100); } cJSON* filter1059 = NULL; if (need_kind1059) { cJSON* kinds1059 = NULL; cJSON* p_values1059 = NULL; filter1059 = cJSON_CreateObject(); kinds1059 = cJSON_CreateArray(); p_values1059 = cJSON_CreateArray(); if (!filter1059 || !kinds1059 || !p_values1059) { cJSON_Delete(filter4); cJSON_Delete(filter1059); cJSON_Delete(kinds1059); cJSON_Delete(p_values1059); return -1; } cJSON_AddItemToArray(kinds1059, cJSON_CreateNumber(1059)); cJSON_AddItemToObject(filter1059, "kinds", kinds1059); cJSON_AddItemToArray(p_values1059, cJSON_CreateString(g_cfg->keys.public_key_hex)); cJSON_AddItemToObject(filter1059, "#p", p_values1059); cJSON_AddNumberToObject(filter1059, "limit", 400); } pthread_mutex_lock(&g_subscription_mutex); managed_subscriptions_init_defaults(); if (managed_subscription_register_locked(MANAGED_SUB_DM_KIND4, need_kind4 ? filter4 : NULL, on_event, on_eose, NULL, 0, 0, NOSTR_POOL_EOSE_FULL_SET, 30, 120, need_kind4) != 0) { rc = -1; } if (managed_subscription_register_locked(MANAGED_SUB_DM_KIND1059, need_kind1059 ? filter1059 : NULL, on_event, on_eose, NULL, 0, 0, NOSTR_POOL_EOSE_FULL_SET, 30, 120, need_kind1059) != 0) { rc = -1; } pthread_mutex_unlock(&g_subscription_mutex); cJSON_Delete(filter4); cJSON_Delete(filter1059); if (rc != 0) { return -1; } DEBUG_INFO("[didactyl] DM subscription active for pubkey %.16s...", g_cfg->keys.public_key_hex); DEBUG_TRACE("[didactyl] DEBUG DM subscription g_start_time=%ld now=%ld delta=%ld relay_count=%d", (long)g_start_time, (long)time(NULL), (long)(time(NULL) - g_start_time), g_cfg->relay_count); return 0; } int nostr_handler_subscribe_wallet_events(void) { if (!g_cfg || !g_pool) { return -1; } cJSON* filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); cJSON* authors = cJSON_CreateArray(); if (!filter || !kinds || !authors) { cJSON_Delete(filter); cJSON_Delete(kinds); cJSON_Delete(authors); return -1; } cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP60_WALLET_KIND)); cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP60_TOKEN_KIND)); cJSON_AddItemToArray(kinds, cJSON_CreateNumber(5)); cJSON_AddItemToObject(filter, "kinds", kinds); cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex)); cJSON_AddItemToObject(filter, "authors", authors); cJSON_AddNumberToObject(filter, "since", (double)g_start_time); cJSON_AddNumberToObject(filter, "limit", 500); int rc = 0; pthread_mutex_lock(&g_subscription_mutex); managed_subscriptions_init_defaults(); if (managed_subscription_register_locked(MANAGED_SUB_WALLET, filter, on_wallet_event, NULL, NULL, 0, 0, NOSTR_POOL_EOSE_FULL_SET, 30, 120, 1) != 0) { rc = -1; } pthread_mutex_unlock(&g_subscription_mutex); cJSON_Delete(filter); if (rc != 0) { return -1; } DEBUG_INFO("[didactyl] wallet event subscription active for pubkey %.16s...", g_cfg->keys.public_key_hex); return 0; } void nostr_handler_set_self_skill_eose_callback(nostr_self_skill_eose_cb_t callback, void* user_data) { g_self_skill_eose_cb = callback; g_self_skill_eose_user_data = user_data; } int nostr_handler_wait_for_self_skill_eose(int timeout_ms) { if (!g_pool || timeout_ms <= 0) { return -1; } struct timespec ts_start; if (clock_gettime(CLOCK_MONOTONIC, &ts_start) != 0) { return -1; } while (!g_self_skill_eose_received) { (void)nostr_handler_poll(100); struct timespec ts_now; if (clock_gettime(CLOCK_MONOTONIC, &ts_now) != 0) { return -1; } long long elapsed_ms = (long long)(ts_now.tv_sec - ts_start.tv_sec) * 1000LL + (long long)(ts_now.tv_nsec - ts_start.tv_nsec) / 1000000LL; if (elapsed_ms >= (long long)timeout_ms) { return -1; } } return 0; } int nostr_handler_validate_self_skill_cache(int* out_skill_count, int* out_adoption_count) { int skill_count = 0; int adoption_count = 0; pthread_mutex_lock(&g_self_skill_mutex); if (g_self_skill_events && cJSON_IsArray(g_self_skill_events)) { int n = cJSON_GetArraySize(g_self_skill_events); for (int i = 0; i < n; i++) { cJSON* ev = cJSON_GetArrayItem(g_self_skill_events, i); cJSON* kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL; cJSON* tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL; cJSON* id = ev ? cJSON_GetObjectItemCaseSensitive(ev, "id") : NULL; cJSON* created_at = ev ? cJSON_GetObjectItemCaseSensitive(ev, "created_at") : NULL; if (!kind || !cJSON_IsNumber(kind)) { continue; } int k = (int)kind->valuedouble; if (k == 31123 || k == 31124) { skill_count++; } else if (k == 10123) { adoption_count++; } const char* d_tag = (tags && cJSON_IsArray(tags)) ? find_tag_value_local(tags, "d") : NULL; DEBUG_INFO("[didactyl] self-skill cache event: kind=%d d_tag=%s id=%s created_at=%lld", k, (d_tag && d_tag[0] != '\0') ? d_tag : "-", (id && cJSON_IsString(id) && id->valuestring) ? id->valuestring : "", (long long)((created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0)); } } pthread_mutex_unlock(&g_self_skill_mutex); if (out_skill_count) { *out_skill_count = skill_count; } if (out_adoption_count) { *out_adoption_count = adoption_count; } return skill_count > 0 ? 0 : -1; } int nostr_handler_send_dm_with_role(const char* recipient_pubkey_hex, const char* message, didactyl_dm_history_role_t role) { if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message) { return -1; } unsigned char recipient_pubkey[32]; if (hex_to_pubkey(recipient_pubkey_hex, recipient_pubkey) != 0) { return -1; } trace_plaintext_dm("sending plaintext DM content:", message); char* encrypted = (char*)malloc(NOSTR_NIP04_MAX_ENCRYPTED_SIZE); if (!encrypted) { fprintf(stderr, "[didactyl] failed to allocate DM encrypt buffer\n"); return -1; } encrypted[0] = '\0'; if (nostr_nip04_encrypt(g_cfg->keys.private_key, recipient_pubkey, message, encrypted, NOSTR_NIP04_MAX_ENCRYPTED_SIZE) != NOSTR_SUCCESS) { free(encrypted); return -1; } cJSON* tags = create_dm_tags_for_recipient(recipient_pubkey_hex); if (!tags) { free(encrypted); return -1; } cJSON* event = nostr_create_and_sign_event(4, encrypted, tags, g_cfg->keys.private_key, time(NULL)); cJSON_Delete(tags); free(encrypted); if (!event) { return -1; } trace_event_json("sending encrypted DM event:", event); log_publish_targets("publish DM"); const char** connected_relays = (const char**)calloc((size_t)g_cfg->relay_count, sizeof(char*)); if (!connected_relays) { cJSON_Delete(event); return -1; } int connected_count = 0; for (int i = 0; i < g_cfg->relay_count; i++) { if (nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]) == NOSTR_POOL_RELAY_CONNECTED) { connected_relays[connected_count++] = g_cfg->relays[i]; } } int sent = 0; if (connected_count > 0) { sent = nostr_relay_pool_publish_async( g_pool, connected_relays, connected_count, event, NULL, NULL); for (int i = 0; i < connected_count; i++) { DEBUG_INFO("[didactyl] kind 4 event published to %s (async)", connected_relays[i]); } } else { DEBUG_WARN("[didactyl] kind 4 event not queued: no connected relays"); } cJSON* event_id = cJSON_GetObjectItemCaseSensitive(event, "id"); const char* out_event_id_hex = (event_id && cJSON_IsString(event_id) && event_id->valuestring) ? event_id->valuestring : ""; DEBUG_INFO("[didactyl] sent DM %.16s... to %.16s... via %d connected relay(s)", out_event_id_hex, recipient_pubkey_hex, sent); if (sent > 0) { dm_history_remember(recipient_pubkey_hex, message, role, time(NULL)); } free(connected_relays); cJSON_Delete(event); return sent > 0 ? 0 : -1; } int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message) { return nostr_handler_send_dm_with_role(recipient_pubkey_hex, message, DIDACTYL_DM_HISTORY_ASSISTANT); } int nostr_handler_send_dm_auto_with_role(const char* recipient_pubkey_hex, const char* message, didactyl_dm_history_role_t role) { if (!recipient_pubkey_hex || !message) { return -1; } if (!g_cfg) { return nostr_handler_send_dm_with_role(recipient_pubkey_hex, message, role); } if (g_cfg->dm_protocol == DM_PROTOCOL_NIP04) { return nostr_handler_send_dm_with_role(recipient_pubkey_hex, message, role); } if (g_cfg->dm_protocol == DM_PROTOCOL_NIP17) { return nostr_handler_send_dm_nip17_with_role(recipient_pubkey_hex, message, NULL, role); } dm_protocol_t target = sender_protocol_lookup(recipient_pubkey_hex); if (target == DM_PROTOCOL_NIP17) { int rc17 = nostr_handler_send_dm_nip17_with_role(recipient_pubkey_hex, message, NULL, role); if (rc17 == 0) { return 0; } DEBUG_WARN("[didactyl] auto DM fallback to NIP-04 for %.16s... after NIP-17 send failure", recipient_pubkey_hex); } return nostr_handler_send_dm_with_role(recipient_pubkey_hex, message, role); } int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* message) { return nostr_handler_send_dm_auto_with_role(recipient_pubkey_hex, message, DIDACTYL_DM_HISTORY_ASSISTANT); } static int publish_kind_event_to_relays(int kind, const char* content, cJSON* tags, const char** relay_urls, int relay_count, const char* reason_label, nostr_publish_result_t* out_result) { if (!g_cfg || !g_pool || !content || !relay_urls || relay_count <= 0) { return -1; } const char* content_to_publish = content; cJSON* tags_to_publish = tags; char* encrypted_content = NULL; cJSON* public_tags = NULL; if (kind == 31124) { if (encode_private_skill_event_payload_local(content, tags, &encrypted_content, &public_tags) != 0) { return -1; } content_to_publish = encrypted_content; tags_to_publish = public_tags; } cJSON* tags_copy = NULL; if (tags_to_publish) { tags_copy = cJSON_Duplicate(tags_to_publish, 1); if (!tags_copy) { free(encrypted_content); cJSON_Delete(public_tags); return -1; } } cJSON* event = nostr_create_and_sign_event(kind, content_to_publish, tags_copy, g_cfg->keys.private_key, time(NULL)); if (tags_copy) { cJSON_Delete(tags_copy); } if (!event) { free(encrypted_content); cJSON_Delete(public_tags); return -1; } int sent = nostr_relay_pool_publish_async( g_pool, relay_urls, relay_count, event, NULL, NULL); for (int i = 0; i < relay_count; i++) { DEBUG_INFO("[didactyl] kind %d event published to %s (async%s%s)", kind, relay_urls[i], reason_label ? ", reason=" : "", reason_label ? reason_label : ""); } cJSON* event_id = cJSON_GetObjectItemCaseSensitive(event, "id"); const char* event_id_hex = (event_id && cJSON_IsString(event_id) && event_id->valuestring) ? event_id->valuestring : ""; fill_publish_result(out_result, kind, tags, event_id_hex, relay_urls, relay_count, sent); if (sent > 0) { if (kind == 31124) { (void)normalize_private_skill_event_local(event); } pthread_mutex_lock(&g_self_skill_mutex); self_skill_cache_upsert_event_locked(event); pthread_mutex_unlock(&g_self_skill_mutex); } cJSON_Delete(event); free(encrypted_content); cJSON_Delete(public_tags); return sent > 0 ? 0 : -1; } int nostr_handler_publish_kind_event(int kind, const char* content, cJSON* tags, nostr_publish_result_t* out_result) { if (!g_cfg || !g_pool || !content) { return -1; } log_publish_targets("publish kind event"); const char** connected_relays = (const char**)calloc((size_t)g_cfg->relay_count, sizeof(char*)); if (!connected_relays) { return -1; } int connected_count = 0; for (int i = 0; i < g_cfg->relay_count; i++) { if (nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]) == NOSTR_POOL_RELAY_CONNECTED) { connected_relays[connected_count++] = g_cfg->relays[i]; } } if (connected_count <= 0) { DEBUG_WARN("[didactyl] kind %d event not queued: no connected relays", kind); free(connected_relays); return -1; } int rc = publish_kind_event_to_relays(kind, content, tags, connected_relays, connected_count, "manual_publish", out_result); free(connected_relays); DEBUG_INFO("[didactyl] published kind %d event via %d connected relay(s)", kind, connected_count); return rc; } char* nostr_handler_query_json(cJSON* filter, int timeout_ms) { if (!g_cfg || !g_pool || !filter) { return NULL; } int event_count = 0; cJSON** events = nostr_relay_pool_query_sync( g_pool, (const char**)g_cfg->relays, g_cfg->relay_count, filter, &event_count, timeout_ms); cJSON* arr = cJSON_CreateArray(); if (!arr) { return NULL; } if (events && event_count > 0) { for (int i = 0; i < event_count; i++) { if (!events[i]) { continue; } if (nostr_block_list_is_event_filtered(events[i])) { cJSON_Delete(events[i]); continue; } cJSON* dup = cJSON_Duplicate(events[i], 1); if (dup) { cJSON_AddItemToArray(arr, dup); } cJSON_Delete(events[i]); } free(events); } char* out = cJSON_PrintUnformatted(arr); cJSON_Delete(arr); return out; } static void external_subscription_on_event_filtered(cJSON* event, const char* relay_url, void* user_data) { external_subscription_wrapper_t* w = (external_subscription_wrapper_t*)user_data; if (!w || !w->on_event) return; if (nostr_block_list_is_event_filtered(event)) return; w->on_event(event, relay_url, w->user_data); } static void external_subscription_on_eose_passthrough(cJSON** events, int event_count, void* user_data) { external_subscription_wrapper_t* w = (external_subscription_wrapper_t*)user_data; if (!w || !w->on_eose) return; w->on_eose(events, event_count, w->user_data); } static void external_subscription_wrapper_track(external_subscription_wrapper_t* w) { if (!w) return; pthread_mutex_lock(&g_subscription_mutex); w->next = g_external_sub_wrappers; g_external_sub_wrappers = w; pthread_mutex_unlock(&g_subscription_mutex); } static void external_subscription_wrapper_untrack_and_free(nostr_pool_subscription_t* handle) { if (!handle) return; pthread_mutex_lock(&g_subscription_mutex); external_subscription_wrapper_t* prev = NULL; external_subscription_wrapper_t* cur = g_external_sub_wrappers; while (cur) { if (cur->handle == handle) { if (prev) prev->next = cur->next; else g_external_sub_wrappers = cur->next; free(cur); break; } prev = cur; cur = cur->next; } pthread_mutex_unlock(&g_subscription_mutex); } nostr_pool_subscription_t* nostr_handler_subscribe_with_filter( cJSON* filter, void (*on_event)(cJSON* event, const char* relay_url, void* user_data), void (*on_eose)(cJSON** events, int event_count, void* user_data), void* user_data, int close_on_eose, int enable_deduplication, nostr_pool_eose_result_mode_t result_mode, int relay_timeout_seconds, int eose_timeout_seconds) { if (!g_cfg || !g_pool || !filter || !on_event) { return NULL; } external_subscription_wrapper_t* w = (external_subscription_wrapper_t*)calloc(1, sizeof(*w)); if (!w) return NULL; w->on_event = on_event; w->on_eose = on_eose; w->user_data = user_data; nostr_pool_subscription_t* sub = nostr_relay_pool_subscribe( g_pool, (const char**)g_cfg->relays, g_cfg->relay_count, filter, external_subscription_on_event_filtered, on_eose ? external_subscription_on_eose_passthrough : NULL, w, close_on_eose, enable_deduplication, result_mode, relay_timeout_seconds, eose_timeout_seconds); if (!sub) { free(w); return NULL; } w->handle = sub; external_subscription_wrapper_track(w); return sub; } int nostr_handler_close_subscription(nostr_pool_subscription_t* subscription) { if (!subscription) { return 0; } int rc = nostr_pool_subscription_close(subscription); external_subscription_wrapper_untrack_and_free(subscription); return rc; } static void publish_pending_startup_events_for_relay_index(int relay_index, const char* reason) { if (!g_cfg || !g_pool || !g_startup_publish_tracking_enabled || !g_startup_published) { return; } if (relay_index < 0 || relay_index >= g_cfg->relay_count) { return; } const char* relay_url = g_cfg->relays[relay_index]; if (nostr_relay_pool_get_relay_status(g_pool, relay_url) != NOSTR_POOL_RELAY_CONNECTED) { return; } for (int i = 0; i < g_cfg->startup_event_count; i++) { startup_event_t* se = &g_cfg->startup_events[i]; if (!se->content) { continue; } if (se->kind == 1 && g_startup_kind1_already_exists) { continue; } size_t slot = (size_t)i * (size_t)g_cfg->relay_count + (size_t)relay_index; if (g_startup_published[slot]) { continue; } cJSON* tags = NULL; if (se->tags_json) { tags = cJSON_Parse(se->tags_json); if (!tags || !cJSON_IsArray(tags)) { cJSON_Delete(tags); tags = NULL; } } char kind1_content[512]; const char* content_to_publish = se->content; if (se->kind == 1) { build_startup_kind1_content(kind1_content, sizeof(kind1_content), se->content); content_to_publish = kind1_content; } const char* one_relay[1] = { relay_url }; if (publish_kind_event_to_relays(se->kind, content_to_publish, tags, one_relay, 1, reason, NULL) == 0) { g_startup_published[slot] = 1; if (se->kind == 1) { g_startup_kind1_already_exists = 1; } } else { DEBUG_WARN("[didactyl] startup event publish failed for kind=%d relay=%s", se->kind, relay_url); } cJSON_Delete(tags); } } static const char* startup_kind_label(int kind) { switch (kind) { case 0: return "profile"; case 1: return "note"; case 3: return "contacts"; case 10002: return "relay_list"; case 10123: return "adoption_list"; case 31123: return "private_skill"; case 31124: return "skill"; default: return "event"; } } int nostr_handler_reconcile_startup_events(void) { if (!g_cfg || !g_pool) { return -1; } free(g_startup_published); g_startup_published = NULL; g_startup_publish_tracking_enabled = 0; if (g_cfg->startup_event_count > 0 && g_cfg->relay_count > 0) { size_t total = (size_t)g_cfg->startup_event_count * (size_t)g_cfg->relay_count; g_startup_published = (unsigned char*)calloc(total, 1U); if (!g_startup_published) { return -1; } g_startup_publish_tracking_enabled = 1; } load_startup_display_name(); g_startup_kind1_already_exists = startup_self_kind1_exists(); if (g_startup_kind1_already_exists) { DEBUG_INFO("[didactyl] startup phase: existing self kind-1 note found; skipping startup kind-1 publish"); } for (int relay_index = 0; relay_index < g_cfg->relay_count; relay_index++) { publish_pending_startup_events_for_relay_index(relay_index, "startup_reconcile"); } if (g_startup_publish_tracking_enabled && g_startup_published) { int events_published = 0; int relays_used = 0; int missing_any_event = 0; for (int r = 0; r < g_cfg->relay_count; r++) { int used = 0; for (int i = 0; i < g_cfg->startup_event_count; i++) { size_t slot = (size_t)i * (size_t)g_cfg->relay_count + (size_t)r; if (g_startup_published[slot]) { used = 1; break; } } if (used) { relays_used++; } } for (int i = 0; i < g_cfg->startup_event_count; i++) { startup_event_t* se = &g_cfg->startup_events[i]; char relays_line[512]; relays_line[0] = '\0'; int relay_hits = 0; for (int r = 0; r < g_cfg->relay_count; r++) { size_t slot = (size_t)i * (size_t)g_cfg->relay_count + (size_t)r; if (!g_startup_published[slot]) { continue; } relay_hits++; if (relays_line[0] != '\0') { strncat(relays_line, ", ", sizeof(relays_line) - strlen(relays_line) - 1U); } strncat(relays_line, g_cfg->relays[r], sizeof(relays_line) - strlen(relays_line) - 1U); } if (relay_hits > 0) { events_published++; DEBUG_INFO("[didactyl] startup publish detail: kind=%d (%s) -> %s", se->kind, startup_kind_label(se->kind), relays_line[0] ? relays_line : ""); } else { missing_any_event = 1; DEBUG_ERROR("[didactyl] startup publish missing: kind=%d (%s) was not published to any relay", se->kind, startup_kind_label(se->kind)); } } DEBUG_INFO("[didactyl] startup publish summary: %d/%d event(s) published to at least one relay; relays used=%d/%d", events_published, g_cfg->startup_event_count, relays_used, g_cfg->relay_count); if (missing_any_event) { DEBUG_ERROR("[didactyl] startup reconcile failed: one or more startup events were not published to any relay"); return -1; } } return 0; } char* nostr_handler_get_self_events_by_kind_json(int kind) { cJSON* out = cJSON_CreateArray(); if (!out) { return NULL; } pthread_mutex_lock(&g_self_skill_mutex); if (g_self_skill_events && cJSON_IsArray(g_self_skill_events)) { int n = cJSON_GetArraySize(g_self_skill_events); for (int i = 0; i < n; i++) { cJSON* ev = cJSON_GetArrayItem(g_self_skill_events, i); cJSON* ev_kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL; if (!ev_kind || !cJSON_IsNumber(ev_kind) || (int)ev_kind->valuedouble != kind) { continue; } cJSON* dup = cJSON_Duplicate(ev, 1); if (dup) { cJSON_AddItemToArray(out, dup); } } } pthread_mutex_unlock(&g_self_skill_mutex); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); return json ? json : strdup("[]"); } char* nostr_handler_get_self_skill_events_json(void) { cJSON* out = cJSON_CreateArray(); if (!out) { return NULL; } pthread_mutex_lock(&g_self_skill_mutex); if (g_self_skill_events && cJSON_IsArray(g_self_skill_events)) { int n = cJSON_GetArraySize(g_self_skill_events); for (int i = 0; i < n; i++) { cJSON* ev = cJSON_GetArrayItem(g_self_skill_events, i); cJSON* ev_kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL; if (!ev_kind || !cJSON_IsNumber(ev_kind)) { continue; } int k = (int)ev_kind->valuedouble; if (k != 31123 && k != 31124) { continue; } cJSON* dup = cJSON_Duplicate(ev, 1); if (dup) { cJSON_AddItemToArray(out, dup); } } } pthread_mutex_unlock(&g_self_skill_mutex); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); return json ? json : strdup("[]"); } int nostr_handler_refresh_self_skill_cache_from_relays(int timeout_ms) { if (!g_cfg || !g_pool) { return -1; } int effective_timeout_ms = timeout_ms > 0 ? timeout_ms : 5000; cJSON* filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); cJSON* authors = cJSON_CreateArray(); if (!filter || !kinds || !authors) { cJSON_Delete(filter); cJSON_Delete(kinds); cJSON_Delete(authors); return -1; } cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31123)); cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31124)); cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10123)); cJSON_AddItemToObject(filter, "kinds", kinds); cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex)); if (g_cfg->admin.pubkey[0] != '\0' && strcmp(g_cfg->admin.pubkey, g_cfg->keys.public_key_hex) != 0) { cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->admin.pubkey)); } cJSON_AddItemToObject(filter, "authors", authors); cJSON_AddNumberToObject(filter, "limit", 500); char* events_json = nostr_handler_query_json(filter, effective_timeout_ms); cJSON_Delete(filter); if (!events_json) { return -1; } cJSON* events = cJSON_Parse(events_json); free(events_json); if (!events || !cJSON_IsArray(events)) { cJSON_Delete(events); return -1; } cJSON* adopted_authors = cJSON_CreateObject(); if (adopted_authors) { cJSON* latest_self_adoption = NULL; double latest_self_adoption_created = -1.0; int n0 = cJSON_GetArraySize(events); for (int i = 0; i < n0; i++) { cJSON* ev = cJSON_GetArrayItem(events, i); if (!ev || !cJSON_IsObject(ev)) { continue; } cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind"); cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(ev, "pubkey"); if (!kind || !cJSON_IsNumber(kind) || (int)kind->valuedouble != 10123 || !pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring || strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) != 0) { continue; } cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at"); double created = (created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0.0; if (!latest_self_adoption || created > latest_self_adoption_created) { latest_self_adoption = ev; latest_self_adoption_created = created; } } if (latest_self_adoption) { cJSON* tags = cJSON_GetObjectItemCaseSensitive(latest_self_adoption, "tags"); if (tags && cJSON_IsArray(tags)) { int tn = cJSON_GetArraySize(tags); for (int ti = 0; ti < tn; ti++) { cJSON* tag = cJSON_GetArrayItem(tags, ti); if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { continue; } cJSON* k = cJSON_GetArrayItem(tag, 0); cJSON* v = cJSON_GetArrayItem(tag, 1); if (!k || !v || !cJSON_IsString(k) || !k->valuestring || !cJSON_IsString(v) || !v->valuestring || strcmp(k->valuestring, "a") != 0) { continue; } int adopted_kind = 0; char adopted_pubkey[65] = {0}; char adopted_d_tag[65] = {0}; if (parse_skill_address_local(v->valuestring, &adopted_kind, adopted_pubkey, adopted_d_tag) != 0) { continue; } if (adopted_pubkey[0] == '\0') { continue; } if (!cJSON_GetObjectItemCaseSensitive(adopted_authors, adopted_pubkey)) { cJSON_AddBoolToObject(adopted_authors, adopted_pubkey, 1); } } } } int adopted_author_count = 0; cJSON* adopted_it = NULL; cJSON_ArrayForEach(adopted_it, adopted_authors) { adopted_author_count++; } if (adopted_author_count > 0) { cJSON* adopted_filter = cJSON_CreateObject(); cJSON* adopted_kinds = cJSON_CreateArray(); cJSON* adopted_author_arr = cJSON_CreateArray(); if (adopted_filter && adopted_kinds && adopted_author_arr) { cJSON_AddItemToArray(adopted_kinds, cJSON_CreateNumber(31123)); cJSON_AddItemToArray(adopted_kinds, cJSON_CreateNumber(31124)); cJSON_AddItemToObject(adopted_filter, "kinds", adopted_kinds); cJSON_ArrayForEach(adopted_it, adopted_authors) { if (adopted_it->string && adopted_it->string[0] != '\0') { cJSON_AddItemToArray(adopted_author_arr, cJSON_CreateString(adopted_it->string)); } } cJSON_AddItemToObject(adopted_filter, "authors", adopted_author_arr); cJSON_AddNumberToObject(adopted_filter, "limit", 1000); char* adopted_json = nostr_handler_query_json(adopted_filter, effective_timeout_ms); if (adopted_json) { cJSON* adopted_events = cJSON_Parse(adopted_json); free(adopted_json); if (adopted_events && cJSON_IsArray(adopted_events)) { int an = cJSON_GetArraySize(adopted_events); for (int ai = 0; ai < an; ai++) { cJSON* ev = cJSON_GetArrayItem(adopted_events, ai); cJSON* dup = ev ? cJSON_Duplicate(ev, 1) : NULL; if (dup) { cJSON_AddItemToArray(events, dup); } } } cJSON_Delete(adopted_events); } } else { cJSON_Delete(adopted_filter); cJSON_Delete(adopted_kinds); cJSON_Delete(adopted_author_arr); } cJSON_Delete(adopted_filter); } } cJSON_Delete(adopted_authors); int merged = 0; pthread_mutex_lock(&g_self_skill_mutex); int n = cJSON_GetArraySize(events); for (int i = 0; i < n; i++) { cJSON* ev = cJSON_GetArrayItem(events, i); if (!ev || !cJSON_IsObject(ev)) { continue; } cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind"); int kind_val = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : -1; if (kind_val == 31124) { (void)normalize_private_skill_event_local(ev); } self_skill_cache_upsert_event_locked(ev); merged++; } pthread_mutex_unlock(&g_self_skill_mutex); cJSON_Delete(events); agent_invalidate_adopted_skills_cache(); DEBUG_INFO("[didactyl] skill_refresh relay reload merged %d self-skill/adoption events", merged); return 0; } int nostr_handler_is_event_in_self_cache(const char* event_id_hex) { if (!event_id_hex || strlen(event_id_hex) != 64U) { return 0; } int found = 0; pthread_mutex_lock(&g_self_skill_mutex); if (g_self_skill_events && cJSON_IsArray(g_self_skill_events)) { int n = cJSON_GetArraySize(g_self_skill_events); for (int i = 0; i < n; i++) { cJSON* ev = cJSON_GetArrayItem(g_self_skill_events, i); cJSON* id = ev ? cJSON_GetObjectItemCaseSensitive(ev, "id") : NULL; if (!id || !cJSON_IsString(id) || !id->valuestring) { continue; } if (strcmp(id->valuestring, event_id_hex) == 0) { found = 1; break; } } } pthread_mutex_unlock(&g_self_skill_mutex); return found; } const char* nostr_handler_get_startup_display_name(void) { return g_startup_display_name; } int nostr_handler_connected_relay_count(void) { if (!g_cfg || !g_pool) { return 0; } int connected = 0; for (int i = 0; i < g_cfg->relay_count; i++) { if (nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]) == NOSTR_POOL_RELAY_CONNECTED) { connected++; } } return connected; } char* nostr_handler_relay_status_json(void) { if (!g_pool) { return NULL; } char** relay_urls = NULL; nostr_pool_relay_status_t* statuses = NULL; int relay_count = nostr_relay_pool_list_relays(g_pool, &relay_urls, &statuses); if (relay_count < 0) { return NULL; } cJSON* root = cJSON_CreateObject(); cJSON* relays = cJSON_CreateArray(); if (!root || !relays) { cJSON_Delete(root); cJSON_Delete(relays); if (relay_urls) { for (int i = 0; i < relay_count; i++) { free(relay_urls[i]); } free(relay_urls); } free(statuses); return NULL; } int connected_count = 0; for (int i = 0; i < relay_count; i++) { cJSON* relay = cJSON_CreateObject(); if (!relay) { continue; } const char* url = (relay_urls && relay_urls[i]) ? relay_urls[i] : ""; nostr_pool_relay_status_t st = statuses ? statuses[i] : NOSTR_POOL_RELAY_DISCONNECTED; if (st == NOSTR_POOL_RELAY_CONNECTED) { connected_count++; } const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(g_pool, url); cJSON_AddStringToObject(relay, "url", url); cJSON_AddStringToObject(relay, "status", relay_status_str(st)); if (stats) { cJSON_AddNumberToObject(relay, "events_received", stats->events_received); cJSON_AddNumberToObject(relay, "events_published", stats->events_published); cJSON_AddNumberToObject(relay, "events_published_ok", stats->events_published_ok); cJSON_AddNumberToObject(relay, "events_published_failed", stats->events_published_failed); cJSON_AddNumberToObject(relay, "ping_latency_current", stats->ping_latency_current); cJSON_AddNumberToObject(relay, "ping_latency_avg", stats->ping_latency_avg); cJSON_AddNumberToObject(relay, "query_latency_avg", stats->query_latency_avg); cJSON_AddNumberToObject(relay, "publish_latency_avg", stats->publish_latency_avg); cJSON_AddNumberToObject(relay, "connection_uptime_start", (double)stats->connection_uptime_start); cJSON_AddNumberToObject(relay, "last_event_time", (double)stats->last_event_time); } const char* last_pub_err = nostr_relay_pool_get_relay_last_publish_error(g_pool, url); const char* last_conn_err = nostr_relay_pool_get_relay_last_connection_error(g_pool, url); if (last_pub_err && last_pub_err[0] != '\0') { cJSON_AddStringToObject(relay, "last_publish_error", last_pub_err); } if (last_conn_err && last_conn_err[0] != '\0') { cJSON_AddStringToObject(relay, "last_connection_error", last_conn_err); } cJSON_AddItemToArray(relays, relay); } cJSON_AddNumberToObject(root, "relay_count", relay_count); cJSON_AddNumberToObject(root, "connected_count", connected_count); cJSON_AddItemToObject(root, "relays", relays); char* out = cJSON_PrintUnformatted(root); cJSON_Delete(root); if (relay_urls) { for (int i = 0; i < relay_count; i++) { free(relay_urls[i]); } free(relay_urls); } free(statuses); return out; } char* nostr_handler_relay_info_json(const char* relay_url) { if (!relay_url || relay_url[0] == '\0') { return NULL; } nostr_relay_info_t* info = NULL; if (nostr_nip11_fetch_relay_info(relay_url, &info, 10) != NOSTR_SUCCESS || !info) { return NULL; } cJSON* root = cJSON_CreateObject(); if (!root) { nostr_nip11_relay_info_free(info); return NULL; } cJSON* basic = cJSON_CreateObject(); if (!basic) { cJSON_Delete(root); nostr_nip11_relay_info_free(info); return NULL; } if (info->basic.name) cJSON_AddStringToObject(basic, "name", info->basic.name); if (info->basic.description) cJSON_AddStringToObject(basic, "description", info->basic.description); if (info->basic.pubkey) cJSON_AddStringToObject(basic, "pubkey", info->basic.pubkey); if (info->basic.contact) cJSON_AddStringToObject(basic, "contact", info->basic.contact); if (info->basic.software) cJSON_AddStringToObject(basic, "software", info->basic.software); if (info->basic.version) cJSON_AddStringToObject(basic, "version", info->basic.version); cJSON* supported_nips = cJSON_CreateArray(); if (!supported_nips) { cJSON_Delete(root); cJSON_Delete(basic); nostr_nip11_relay_info_free(info); return NULL; } for (size_t i = 0; i < info->basic.supported_nips_count; i++) { cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(info->basic.supported_nips[i])); } cJSON_AddItemToObject(basic, "supported_nips", supported_nips); cJSON_AddItemToObject(root, "basic", basic); if (info->has_limitations) { cJSON* limitations = cJSON_CreateObject(); if (!limitations) { cJSON_Delete(root); nostr_nip11_relay_info_free(info); return NULL; } cJSON_AddNumberToObject(limitations, "max_message_length", info->limitations.max_message_length); cJSON_AddNumberToObject(limitations, "max_subscriptions", info->limitations.max_subscriptions); cJSON_AddNumberToObject(limitations, "max_filters", info->limitations.max_filters); cJSON_AddNumberToObject(limitations, "max_limit", info->limitations.max_limit); cJSON_AddNumberToObject(limitations, "max_subid_length", info->limitations.max_subid_length); cJSON_AddNumberToObject(limitations, "min_prefix", info->limitations.min_prefix); cJSON_AddNumberToObject(limitations, "max_event_tags", info->limitations.max_event_tags); cJSON_AddNumberToObject(limitations, "max_content_length", info->limitations.max_content_length); cJSON_AddNumberToObject(limitations, "min_pow_difficulty", info->limitations.min_pow_difficulty); cJSON_AddNumberToObject(limitations, "auth_required", info->limitations.auth_required); cJSON_AddNumberToObject(limitations, "payment_required", info->limitations.payment_required); cJSON_AddNumberToObject(limitations, "restricted_writes", info->limitations.restricted_writes); cJSON_AddNumberToObject(limitations, "created_at_lower_limit", (double)info->limitations.created_at_lower_limit); cJSON_AddNumberToObject(limitations, "created_at_upper_limit", (double)info->limitations.created_at_upper_limit); cJSON_AddItemToObject(root, "limitations", limitations); } if (info->has_content_limitations) { cJSON* countries = cJSON_CreateArray(); if (!countries) { cJSON_Delete(root); nostr_nip11_relay_info_free(info); return NULL; } for (size_t i = 0; i < info->content_limitations.relay_countries_count; i++) { if (info->content_limitations.relay_countries[i]) { cJSON_AddItemToArray(countries, cJSON_CreateString(info->content_limitations.relay_countries[i])); } } cJSON_AddItemToObject(root, "relay_countries", countries); } if (info->has_community_preferences) { cJSON* prefs = cJSON_CreateObject(); if (!prefs) { cJSON_Delete(root); nostr_nip11_relay_info_free(info); return NULL; } cJSON* language_tags = cJSON_CreateArray(); cJSON* tags = cJSON_CreateArray(); if (!language_tags || !tags) { cJSON_Delete(root); cJSON_Delete(prefs); cJSON_Delete(language_tags); cJSON_Delete(tags); nostr_nip11_relay_info_free(info); return NULL; } for (size_t i = 0; i < info->community_preferences.language_tags_count; i++) { if (info->community_preferences.language_tags[i]) { cJSON_AddItemToArray(language_tags, cJSON_CreateString(info->community_preferences.language_tags[i])); } } for (size_t i = 0; i < info->community_preferences.tags_count; i++) { if (info->community_preferences.tags[i]) { cJSON_AddItemToArray(tags, cJSON_CreateString(info->community_preferences.tags[i])); } } cJSON_AddItemToObject(prefs, "language_tags", language_tags); cJSON_AddItemToObject(prefs, "tags", tags); if (info->community_preferences.posting_policy) { cJSON_AddStringToObject(prefs, "posting_policy", info->community_preferences.posting_policy); } cJSON_AddItemToObject(root, "community_preferences", prefs); } if (info->has_icon && info->icon.icon) { cJSON_AddStringToObject(root, "icon", info->icon.icon); } char* out = cJSON_PrintUnformatted(root); cJSON_Delete(root); nostr_nip11_relay_info_free(info); return out; } char* nostr_handler_subscription_status_json(void) { cJSON* root = cJSON_CreateObject(); cJSON* subs = cJSON_CreateArray(); if (!root || !subs) { cJSON_Delete(root); cJSON_Delete(subs); return NULL; } pthread_mutex_lock(&g_subscription_mutex); managed_subscriptions_init_defaults(); for (int i = 0; i < MANAGED_SUB_COUNT; i++) { managed_subscription_t* sub = &g_managed_subs[i]; cJSON* item = cJSON_CreateObject(); if (!item) { continue; } cJSON_AddStringToObject(item, "name", sub->name ? sub->name : ""); cJSON_AddBoolToObject(item, "enabled", sub->enabled ? 1 : 0); cJSON_AddBoolToObject(item, "active", sub->handle ? 1 : 0); cJSON_AddBoolToObject(item, "has_filter", sub->filter ? 1 : 0); cJSON_AddNumberToObject(item, "close_on_eose", sub->close_on_eose); cJSON_AddNumberToObject(item, "enable_deduplication", sub->enable_deduplication); cJSON_AddNumberToObject(item, "relay_timeout_seconds", sub->relay_timeout_seconds); cJSON_AddNumberToObject(item, "eose_timeout_seconds", sub->eose_timeout_seconds); if (sub->filter) { cJSON* filter_dup = cJSON_Duplicate(sub->filter, 1); if (filter_dup) { cJSON_AddItemToObject(item, "filter", filter_dup); } } cJSON_AddItemToArray(subs, item); } pthread_mutex_unlock(&g_subscription_mutex); cJSON_AddNumberToObject(root, "count", MANAGED_SUB_COUNT); cJSON_AddItemToObject(root, "subscriptions", subs); char* out = cJSON_PrintUnformatted(root); cJSON_Delete(root); return out; } int nostr_handler_subscription_set_json(const char* name, cJSON* filter, int enabled, int enabled_set) { if (!name || name[0] == '\0') { return -1; } pthread_mutex_lock(&g_subscription_mutex); managed_subscriptions_init_defaults(); managed_subscription_id_t id = MANAGED_SUB_COUNT; if (managed_subscription_id_from_name(name, &id) != 0) { pthread_mutex_unlock(&g_subscription_mutex); return -1; } managed_subscription_t* sub = &g_managed_subs[id]; cJSON* filter_to_use = sub->filter; if (filter) { filter_to_use = filter; } int enabled_to_use = enabled_set ? (enabled ? 1 : 0) : sub->enabled; int rc = managed_subscription_register_locked(id, filter_to_use, sub->on_event, sub->on_eose, sub->user_data, sub->close_on_eose, sub->enable_deduplication, sub->result_mode, sub->relay_timeout_seconds, sub->eose_timeout_seconds, enabled_to_use); pthread_mutex_unlock(&g_subscription_mutex); return rc; } int nostr_handler_send_dm_nip17_with_role(const char* recipient_pubkey_hex, const char* message, const char* subject, didactyl_dm_history_role_t role) { if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message || recipient_pubkey_hex[0] == '\0' || message[0] == '\0') { return -1; } const char* target_relays[NIP17_MAX_RELAYS] = {0}; int target_count = 0; for (int i = 0; i < g_cfg->relay_count && target_count < NIP17_MAX_RELAYS; i++) { if (nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]) == NOSTR_POOL_RELAY_CONNECTED) { target_relays[target_count++] = g_cfg->relays[i]; } } DEBUG_TRACE("[didactyl] NIP-17 send prep: recipient=%.16s... connected_targets=%d", recipient_pubkey_hex, target_count); if (target_count <= 0) { DEBUG_WARN("[didactyl] NIP-17 send aborted: no connected relays for recipient %.16s...", recipient_pubkey_hex); return -1; } const char* recipients[1] = { recipient_pubkey_hex }; cJSON* chat_event = nostr_nip17_create_chat_event( message, recipients, 1, (subject && subject[0] != '\0') ? subject : NULL, NULL, NULL, g_cfg->keys.public_key_hex); if (!chat_event) { return -1; } cJSON* gift_wraps[NIP17_MAX_GIFT_WRAPS] = {0}; int gift_count = nostr_nip17_send_dm(chat_event, recipients, 1, g_cfg->keys.private_key, gift_wraps, NIP17_MAX_GIFT_WRAPS, 0); cJSON_Delete(chat_event); if (gift_count <= 0) { DEBUG_WARN("[didactyl] NIP-17 send aborted: gift_wrap creation failed for %.16s...", recipient_pubkey_hex); return -1; } int any_sent = 0; int sent_count = 0; for (int i = 0; i < gift_count; i++) { if (!gift_wraps[i]) { continue; } int sent = nostr_relay_pool_publish_async(g_pool, target_relays, target_count, gift_wraps[i], NULL, NULL); if (sent > 0) { any_sent = 1; sent_count += sent; } for (int r = 0; r < target_count; r++) { DEBUG_INFO("[didactyl] kind 1059 event published to %s (async)", target_relays[r]); } cJSON_Delete(gift_wraps[i]); } DEBUG_TRACE("[didactyl] NIP-17 send complete: recipient=%.16s... sent_count=%d", recipient_pubkey_hex, sent_count); if (!any_sent) { DEBUG_WARN("[didactyl] NIP-17 send failed: no relay accepted publish for recipient %.16s...", recipient_pubkey_hex); return -1; } dm_history_remember(recipient_pubkey_hex, message, role, time(NULL)); return 0; } int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* message, const char* subject) { return nostr_handler_send_dm_nip17_with_role(recipient_pubkey_hex, message, subject, DIDACTYL_DM_HISTORY_ASSISTANT); } char* nostr_handler_get_dm_history_json(const char* peer_pubkey_hex, int limit) { if (!peer_pubkey_hex || strlen(peer_pubkey_hex) != 64U) { return strdup("[]"); } int max_take = limit > 0 ? limit : 64; if (max_take > DM_HISTORY_RING_SIZE) { max_take = DM_HISTORY_RING_SIZE; } cJSON* arr = cJSON_CreateArray(); if (!arr) { return NULL; } pthread_mutex_lock(&g_dm_history_mutex); int total = g_dm_history_count; int matches = 0; for (int i = 0; i < total; i++) { int idx = (g_dm_history_count < DM_HISTORY_RING_SIZE) ? i : (g_dm_history_next + i) % DM_HISTORY_RING_SIZE; dm_history_entry_t* entry = &g_dm_history_ring[idx]; if (entry->peer_pubkey_hex[0] == '\0' || !entry->content) { continue; } if (strcmp(entry->peer_pubkey_hex, peer_pubkey_hex) == 0) { matches++; } } int skip = matches > max_take ? (matches - max_take) : 0; int seen = 0; for (int i = 0; i < total; i++) { int idx = (g_dm_history_count < DM_HISTORY_RING_SIZE) ? i : (g_dm_history_next + i) % DM_HISTORY_RING_SIZE; dm_history_entry_t* entry = &g_dm_history_ring[idx]; if (entry->peer_pubkey_hex[0] == '\0' || !entry->content) { continue; } if (strcmp(entry->peer_pubkey_hex, peer_pubkey_hex) != 0) { continue; } if (seen++ < skip) { continue; } cJSON* item = cJSON_CreateObject(); if (!item) { continue; } const char* role_s = "assistant"; if (entry->role == DIDACTYL_DM_HISTORY_USER) { role_s = "user"; } else if (entry->role == DIDACTYL_DM_HISTORY_TOOL_REQUEST) { role_s = "tool_request"; } else if (entry->role == DIDACTYL_DM_HISTORY_TOOL_RESPONSE) { role_s = "tool_response"; } cJSON_AddStringToObject(item, "role", role_s); cJSON_AddStringToObject(item, "content", entry->content); cJSON_AddNumberToObject(item, "created_at", (double)entry->created_at); cJSON_AddItemToArray(arr, item); } pthread_mutex_unlock(&g_dm_history_mutex); char* out = cJSON_PrintUnformatted(arr); cJSON_Delete(arr); return out ? out : strdup("[]"); } char* nostr_handler_get_dm_history_markdown(const char* peer_pubkey_hex, int limit) { if (!peer_pubkey_hex || strlen(peer_pubkey_hex) != 64U) { return strdup(""); } int max_take = limit > 0 ? limit : 64; if (max_take > DM_HISTORY_RING_SIZE) { max_take = DM_HISTORY_RING_SIZE; } size_t cap = 1024; size_t used = 0; char* buf = (char*)malloc(cap); if (!buf) return NULL; buf[0] = '\0'; pthread_mutex_lock(&g_dm_history_mutex); int total = g_dm_history_count; int matches = 0; for (int i = 0; i < total; i++) { int idx = (g_dm_history_count < DM_HISTORY_RING_SIZE) ? i : (g_dm_history_next + i) % DM_HISTORY_RING_SIZE; dm_history_entry_t* entry = &g_dm_history_ring[idx]; if (entry->peer_pubkey_hex[0] == '\0' || !entry->content) { continue; } if (strcmp(entry->peer_pubkey_hex, peer_pubkey_hex) == 0) { matches++; } } int skip = matches > max_take ? (matches - max_take) : 0; int seen = 0; for (int i = 0; i < total; i++) { int idx = (g_dm_history_count < DM_HISTORY_RING_SIZE) ? i : (g_dm_history_next + i) % DM_HISTORY_RING_SIZE; dm_history_entry_t* entry = &g_dm_history_ring[idx]; if (entry->peer_pubkey_hex[0] == '\0' || !entry->content) { continue; } if (strcmp(entry->peer_pubkey_hex, peer_pubkey_hex) != 0) { continue; } if (seen++ < skip) { continue; } const char* role_label = "**You**"; if (entry->role == DIDACTYL_DM_HISTORY_USER) { role_label = "**Admin**"; } else if (entry->role == DIDACTYL_DM_HISTORY_TOOL_REQUEST) { role_label = "**Admin (Tool Request)**"; } else if (entry->role == DIDACTYL_DM_HISTORY_TOOL_RESPONSE) { role_label = "**You (Tool Response)**"; } size_t need = strlen("- ") + strlen(role_label) + strlen(": ") + strlen(entry->content) + 2U; if (used + need > cap) { size_t next = cap; while (used + need > next) next *= 2U; char* grown = (char*)realloc(buf, next); if (!grown) { pthread_mutex_unlock(&g_dm_history_mutex); free(buf); return NULL; } buf = grown; cap = next; } snprintf(buf + used, cap - used, "- %s: %s\n", role_label, entry->content); used += strlen(buf + used); } pthread_mutex_unlock(&g_dm_history_mutex); if (used > 0 && buf[used - 1] == '\n') { buf[used - 1] = '\0'; } return buf; } char* nostr_handler_get_admin_kind0_context(void) { if (!g_cfg || !g_cfg->admin_context.enabled || !g_cfg->admin_context.track_kind_0) { return NULL; } pthread_mutex_lock(&g_admin_ctx_mutex); char* out = g_admin_kind0_json ? strdup(g_admin_kind0_json) : NULL; pthread_mutex_unlock(&g_admin_ctx_mutex); return out; } char* nostr_handler_get_admin_kind3_context(void) { if (!g_cfg || !g_cfg->admin_context.enabled || !g_cfg->admin_context.track_kind_3) { return NULL; } pthread_mutex_lock(&g_admin_ctx_mutex); cJSON* out = cJSON_CreateArray(); if (!out) { pthread_mutex_unlock(&g_admin_ctx_mutex); return NULL; } for (int i = 0; i < g_admin_wot_contact_count; i++) { const char* pk = g_admin_wot_contacts[i]; if (!pk || pk[0] == '\0') continue; cJSON_AddItemToArray(out, cJSON_CreateString(pk)); } pthread_mutex_unlock(&g_admin_ctx_mutex); char* out_json = cJSON_PrintUnformatted(out); cJSON_Delete(out); return out_json; } char* nostr_handler_get_admin_kind10002_context(void) { if (!g_cfg || !g_cfg->admin_context.enabled || !g_cfg->admin_context.track_kind_10002) { return NULL; } pthread_mutex_lock(&g_admin_ctx_mutex); char* out = g_admin_kind10002_json ? strdup(g_admin_kind10002_json) : NULL; pthread_mutex_unlock(&g_admin_ctx_mutex); return out; } char* nostr_handler_get_admin_kind1_notes_context(void) { if (!g_cfg || !g_cfg->admin_context.enabled || !g_cfg->admin_context.track_kind_1) { return NULL; } pthread_mutex_lock(&g_admin_ctx_mutex); if (g_admin_kind1_note_count <= 0 || !g_admin_kind1_notes) { pthread_mutex_unlock(&g_admin_ctx_mutex); return NULL; } size_t total = strlen("Administrator recent public notes:\n") + 1U; for (int i = 0; i < g_admin_kind1_note_count; i++) { total += strlen("- ") + strlen(g_admin_kind1_notes[i].content ? g_admin_kind1_notes[i].content : "") + 1U; } char* out = (char*)malloc(total); if (!out) { pthread_mutex_unlock(&g_admin_ctx_mutex); return NULL; } out[0] = '\0'; strcat(out, "Administrator recent public notes:\n"); for (int i = 0; i < g_admin_kind1_note_count; i++) { strcat(out, "- "); strcat(out, g_admin_kind1_notes[i].content ? g_admin_kind1_notes[i].content : ""); strcat(out, "\n"); } pthread_mutex_unlock(&g_admin_ctx_mutex); return out; } static char* extract_latest_kind0_content_from_events_json(const char* events_json) { if (!events_json || events_json[0] == '\0') { return NULL; } cJSON* root = cJSON_Parse(events_json); if (!root || !cJSON_IsArray(root)) { cJSON_Delete(root); return NULL; } char* best_content = NULL; time_t best_created_at = 0; int n = cJSON_GetArraySize(root); for (int i = 0; i < n; i++) { cJSON* ev = cJSON_GetArrayItem(root, i); if (!ev || !cJSON_IsObject(ev)) { continue; } cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind"); cJSON* content = cJSON_GetObjectItemCaseSensitive(ev, "content"); cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at"); if (!kind || !cJSON_IsNumber(kind) || (int)kind->valuedouble != 0 || !content || !cJSON_IsString(content) || !content->valuestring || content->valuestring[0] == '\0') { continue; } time_t ev_created_at = 0; if (created_at && cJSON_IsNumber(created_at)) { ev_created_at = (time_t)created_at->valuedouble; } if (!best_content || ev_created_at >= best_created_at) { char* dup = strdup(content->valuestring); if (!dup) { continue; } free(best_content); best_content = dup; best_created_at = ev_created_at; } } cJSON_Delete(root); return best_content; } char* nostr_handler_get_agent_kind0_context(void) { if (!g_cfg) { return NULL; } pthread_mutex_lock(&g_admin_ctx_mutex); char* out = g_agent_kind0_json ? strdup(g_agent_kind0_json) : NULL; pthread_mutex_unlock(&g_admin_ctx_mutex); if (out && out[0] != '\0') { return out; } free(out); cJSON* filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); cJSON* authors = cJSON_CreateArray(); if (!filter || !kinds || !authors) { cJSON_Delete(filter); cJSON_Delete(kinds); cJSON_Delete(authors); return NULL; } cJSON_AddItemToArray(kinds, cJSON_CreateNumber(0)); cJSON_AddItemToObject(filter, "kinds", kinds); cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex)); cJSON_AddItemToObject(filter, "authors", authors); cJSON_AddNumberToObject(filter, "limit", 8); char* events_json = nostr_handler_query_json(filter, 3000); cJSON_Delete(filter); if (!events_json) { return NULL; } char* fetched_kind0 = extract_latest_kind0_content_from_events_json(events_json); free(events_json); if (!fetched_kind0 || fetched_kind0[0] == '\0') { free(fetched_kind0); return NULL; } pthread_mutex_lock(&g_admin_ctx_mutex); free(g_agent_kind0_json); g_agent_kind0_json = strdup(fetched_kind0); pthread_mutex_unlock(&g_admin_ctx_mutex); return fetched_kind0; } char* nostr_handler_get_agent_kind3_context(void) { if (!g_cfg) { return NULL; } pthread_mutex_lock(&g_admin_ctx_mutex); cJSON* out = cJSON_CreateArray(); if (!out) { pthread_mutex_unlock(&g_admin_ctx_mutex); return NULL; } for (int i = 0; i < g_agent_contact_count; i++) { const char* pk = g_agent_contacts[i]; if (!pk || pk[0] == '\0') continue; cJSON_AddItemToArray(out, cJSON_CreateString(pk)); } pthread_mutex_unlock(&g_admin_ctx_mutex); char* out_json = cJSON_PrintUnformatted(out); cJSON_Delete(out); return out_json; } char* nostr_handler_get_agent_kind10002_context(void) { if (!g_cfg) { return NULL; } pthread_mutex_lock(&g_admin_ctx_mutex); char* out = g_agent_kind10002_json ? strdup(g_agent_kind10002_json) : NULL; pthread_mutex_unlock(&g_admin_ctx_mutex); return out; } char* nostr_handler_get_agent_kind1_notes_context(void) { if (!g_cfg) { return NULL; } pthread_mutex_lock(&g_admin_ctx_mutex); if (g_agent_kind1_note_count <= 0 || !g_agent_kind1_notes) { pthread_mutex_unlock(&g_admin_ctx_mutex); return NULL; } size_t total = strlen("Agent recent public notes:\n") + 1U; for (int i = 0; i < g_agent_kind1_note_count; i++) { total += strlen("- ") + strlen(g_agent_kind1_notes[i].content ? g_agent_kind1_notes[i].content : "") + 1U; } char* out = (char*)malloc(total); if (!out) { pthread_mutex_unlock(&g_admin_ctx_mutex); return NULL; } out[0] = '\0'; strcat(out, "Agent recent public notes:\n"); for (int i = 0; i < g_agent_kind1_note_count; i++) { strcat(out, "- "); strcat(out, g_agent_kind1_notes[i].content ? g_agent_kind1_notes[i].content : ""); strcat(out, "\n"); } pthread_mutex_unlock(&g_admin_ctx_mutex); return out; } int nostr_handler_is_wot_contact(const char* pubkey_hex) { if (!pubkey_hex || strlen(pubkey_hex) != 64U) { return 0; } pthread_mutex_lock(&g_admin_ctx_mutex); int found = 0; for (int i = 0; i < g_admin_wot_contact_count; i++) { if (g_admin_wot_contacts[i] && strcmp(g_admin_wot_contacts[i], pubkey_hex) == 0) { found = 1; break; } } pthread_mutex_unlock(&g_admin_ctx_mutex); return found; } int nostr_handler_poll(int timeout_ms) { if (!g_pool) { return -1; } double start_ms = -1.0; struct timespec ts_start; if (clock_gettime(CLOCK_MONOTONIC, &ts_start) == 0) { start_ms = ts_start.tv_sec * 1000.0 + ts_start.tv_nsec / 1000000.0; } int rc = nostr_relay_pool_poll(g_pool, timeout_ms); g_poll_counter++; if (start_ms >= 0.0) { struct timespec ts_end; if (clock_gettime(CLOCK_MONOTONIC, &ts_end) == 0) { const double end_ms = ts_end.tv_sec * 1000.0 + ts_end.tv_nsec / 1000000.0; const double elapsed_ms = end_ms - start_ms; const double expected_ms = timeout_ms > 0 ? (double)timeout_ms : 0.0; if (elapsed_ms > expected_ms + 250.0) { DEBUG_WARN("[didactyl] poll latency spike: nostr_relay_pool_poll(timeout=%d) took %.1fms rc=%d count=%llu", timeout_ms, elapsed_ms, rc, (unsigned long long)g_poll_counter); } } } log_relay_state_changes(); return rc; } void nostr_handler_cleanup(void) { pthread_mutex_lock(&g_subscription_mutex); managed_subscriptions_init_defaults(); for (int i = 0; i < MANAGED_SUB_COUNT; i++) { (void)managed_subscription_close_locked((managed_subscription_id_t)i); cJSON_Delete(g_managed_subs[i].filter); g_managed_subs[i].filter = NULL; g_managed_subs[i].enabled = 0; g_managed_subs[i].on_event = NULL; g_managed_subs[i].on_eose = NULL; g_managed_subs[i].user_data = NULL; } external_subscription_wrapper_t* ext = g_external_sub_wrappers; while (ext) { external_subscription_wrapper_t* next = ext->next; free(ext); ext = next; } g_external_sub_wrappers = NULL; pthread_mutex_unlock(&g_subscription_mutex); if (g_pool) { nostr_relay_pool_destroy(g_pool); } nostr_core_clear_log_bridge_if_supported(); free(g_last_relay_statuses); g_last_relay_statuses = NULL; free(g_startup_published); g_startup_published = NULL; g_startup_publish_tracking_enabled = 0; g_startup_kind1_already_exists = 0; snprintf(g_startup_display_name, sizeof(g_startup_display_name), "%s", "Didactyl"); g_pool = NULL; g_cfg = NULL; g_dm_callback = NULL; g_dm_user_data = NULL; memset(g_seen_dm_ids, 0, sizeof(g_seen_dm_ids)); g_seen_dm_count = 0; g_seen_dm_next = 0; pthread_mutex_lock(&g_sender_protocol_mutex); memset(g_sender_protocol_cache, 0, sizeof(g_sender_protocol_cache)); pthread_mutex_unlock(&g_sender_protocol_mutex); pthread_mutex_lock(&g_dm_history_mutex); dm_history_clear_locked(); pthread_mutex_unlock(&g_dm_history_mutex); pthread_mutex_lock(&g_admin_ctx_mutex); free_admin_context_locked(); free_agent_context_locked(); pthread_mutex_unlock(&g_admin_ctx_mutex); pthread_mutex_lock(&g_self_skill_mutex); cJSON_Delete(g_self_skill_events); g_self_skill_events = NULL; pthread_mutex_unlock(&g_self_skill_mutex); }