Files
didactyl/src/nostr_handler.c
T

5094 lines
174 KiB
C

#define _POSIX_C_SOURCE 200809L
#include "nostr_handler.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
#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"
#include "signer_health.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;
/* Optional process-lifetime signer for relay pool AUTH. Owned by main.c. */
static nostr_signer_t* g_auth_signer = 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 <large %s frame suppressed, bytes=%zu>",
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 : "";
/* Phase 3 item 13: route self-encrypt through the process signer when
* available so remote signer modes keep the nsec out of the agent. The
* signer verb returns a heap string via out-param. Falls back to the
* legacy raw-key path when no signer is set (local mode / pre-Phase-3). */
if (g_auth_signer) {
char* signer_out = NULL;
int rc = nostr_signer_nip44_encrypt(g_auth_signer,
g_cfg->keys.public_key_hex,
plain,
&signer_out);
if (rc != NOSTR_SUCCESS || !signer_out) {
signer_health_record_failure(rc, "nip44_encrypt(self)");
return -1;
}
signer_health_record_ok();
*out_ciphertext = signer_out;
return 0;
}
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;
}
/* Phase 3 item 13: route self-decrypt through the process signer when
* available. Falls back to the legacy raw-key path when no signer is set. */
if (g_auth_signer) {
char* signer_out = NULL;
int rc = nostr_signer_nip44_decrypt(g_auth_signer,
g_cfg->keys.public_key_hex,
ciphertext,
&signer_out);
if (rc != NOSTR_SUCCESS || !signer_out) {
signer_health_record_failure(rc, "nip44_decrypt(self)");
return -1;
}
signer_health_record_ok();
*out_plaintext = signer_out;
return 0;
}
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)) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: event missing or not object");
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");
cJSON* ev_id = cJSON_GetObjectItemCaseSensitive(event, "id");
int kind_val = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : -1;
double created = (created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0.0;
DEBUG_INFO("[didactyl] self-skill upsert candidate: kind=%d author=%.16s... id=%.16s... created_at=%.0f",
kind_val,
(pubkey && cJSON_IsString(pubkey) && pubkey->valuestring) ? pubkey->valuestring : "?",
(ev_id && cJSON_IsString(ev_id) && ev_id->valuestring) ? ev_id->valuestring : "?",
created);
if (!kind || !cJSON_IsNumber(kind) ||
!pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring ||
!tags || !cJSON_IsArray(tags)) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: missing required kind/pubkey/tags");
return;
}
if (kind_val != 31123 && kind_val != 31124 && kind_val != 10123) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: kind not in {31123,31124,10123}");
return;
}
if (!g_cfg) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: config unavailable");
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) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: kind=10123 author is not self");
return;
}
}
if (is_admin_author && kind_val == 31124) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: admin cannot own private skill (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')) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: parameterized kind missing d-tag");
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) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: failed to create cache array");
return;
}
}
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) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: stale (existing created_at=%.0f >= new created_at=%.0f)",
ex_created,
created);
return;
}
cJSON* dup = cJSON_Duplicate(event, 1);
if (!dup) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: failed to duplicate event");
return;
}
cJSON_ReplaceItemInArray(g_self_skill_events, i, dup);
DEBUG_INFO("[didactyl] self-skill upsert accepted: replaced existing (old created_at=%.0f)", ex_created);
if (kind_val == 10123) {
int a_count = 0;
cJSON* t;
cJSON_ArrayForEach(t, tags) {
if (cJSON_IsArray(t) && cJSON_GetArraySize(t) >= 2) {
cJSON* t0 = cJSON_GetArrayItem(t, 0);
if (t0 && cJSON_IsString(t0) && t0->valuestring && strcmp(t0->valuestring, "a") == 0) {
a_count++;
}
}
}
DEBUG_INFO("[didactyl] kind 10123 accepted with %d 'a' tags", a_count);
}
return;
}
cJSON* dup = cJSON_Duplicate(event, 1);
if (!dup) {
DEBUG_INFO("[didactyl] self-skill upsert rejected: failed to duplicate event");
return;
}
cJSON_AddItemToArray(g_self_skill_events, dup);
DEBUG_INFO("[didactyl] self-skill upsert accepted: new entry added");
if (kind_val == 10123) {
int a_count = 0;
cJSON* t;
cJSON_ArrayForEach(t, tags) {
if (cJSON_IsArray(t) && cJSON_GetArraySize(t) >= 2) {
cJSON* t0 = cJSON_GetArrayItem(t, 0);
if (t0 && cJSON_IsString(t0) && t0->valuestring && strcmp(t0->valuestring, "a") == 0) {
a_count++;
}
}
}
DEBUG_INFO("[didactyl] kind 10123 accepted with %d 'a' tags", a_count);
}
}
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 <failed to serialize event>", 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 : "<no-id>",
pubkey->valuestring ? pubkey->valuestring : "<no-pk>",
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 : "<no-id>");
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;
}
trace_event_json("received encrypted DM event:", event);
/* Phase 3 item 13: route NIP-04 decrypt through the process signer
* when available so remote signer modes keep the nsec out of the
* agent. The signer verb returns a heap string via out-param. Falls
* back to the legacy raw-key path when no signer is set. */
if (g_auth_signer) {
char* signer_out = NULL;
int dec_rc = nostr_signer_nip04_decrypt(g_auth_signer,
sender_pubkey_hex,
content->valuestring,
&signer_out);
if (dec_rc != NOSTR_SUCCESS || !signer_out) {
signer_health_record_failure(dec_rc, "nip04_decrypt(incoming DM)");
fprintf(stdout, "[didactyl] failed to decrypt incoming DM from %.16s...\n", sender_pubkey_hex);
return;
}
signer_health_record_ok();
decrypted = signer_out;
} else {
decrypted = (char*)malloc(NOSTR_NIP04_MAX_PLAINTEXT_SIZE);
if (!decrypted) {
fprintf(stderr, "[didactyl] failed to allocate DM decrypt buffer\n");
return;
}
decrypted[0] = '\0';
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;
}
/* Phase 3 item 14: prefer signer-aware NIP-17 unwrap when a process
* signer is available; fall back to the legacy raw-key path in local
* mode / pre-Phase-3. Gift-wrap event ownership is unchanged: the
* returned rumor cJSON is owned by us and freed below. */
cJSON* rumor = NULL;
if (g_auth_signer) {
rumor = nostr_nip17_receive_dm_with_signer(event, g_auth_signer);
} else {
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 : "<no-id>");
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 : "<no-id>",
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 : "<no-id>",
(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();
}
void nostr_handler_set_signer(nostr_signer_t* signer) {
g_auth_signer = signer;
}
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;
}
/* Relay pool NIP-42 AUTH. When a process-lifetime signer is available
* (set via nostr_handler_set_signer), route AUTH signing through it so
* remote signer modes can keep the nsec out of the agent process. In
* local mode the signer wraps the same raw key, so behavior is identical.
* Falls back to the legacy raw-key path when no signer is set. */
if (g_auth_signer) {
if (nostr_relay_pool_set_auth_with_signer(g_pool, g_auth_signer, 1) != NOSTR_SUCCESS) {
fprintf(stderr, "[didactyl] failed to enable relay pool auth (signer path)\n");
return -1;
}
} else {
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);
char* filter_str = cJSON_PrintUnformatted(filter);
DEBUG_INFO("[didactyl] self-skill subscription filter: %s", filter_str ? filter_str : "(null)");
free(filter_str);
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 : "<no-id>",
(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);
/* Phase 3 item 13: route NIP-04 encrypt through the process signer when
* available so remote signer modes keep the nsec out of the agent. The
* signer verb returns a heap string via out-param. Falls back to the
* legacy raw-key path when no signer is set. */
char* encrypted = NULL;
if (g_auth_signer) {
int enc_rc = nostr_signer_nip04_encrypt(g_auth_signer,
recipient_pubkey_hex,
message,
&encrypted);
if (enc_rc != NOSTR_SUCCESS || !encrypted) {
signer_health_record_failure(enc_rc, "nip04_encrypt(outgoing DM)");
return -1;
}
signer_health_record_ok();
} else {
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;
}
/* Phase 3 item 12: route kind-4 DM event signing through the process
* signer handle when available so remote signer modes can keep the nsec
* out of the agent process. Falls back to the legacy raw-key path when
* no signer is set (local mode / pre-Phase-2 binaries) so behavior is
* unchanged. */
cJSON* event = NULL;
if (g_auth_signer) {
event = nostr_create_and_sign_event_with_signer(4, encrypted, tags, g_auth_signer, time(NULL));
} else {
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 : "<no-id>";
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;
}
}
/* Phase 3 item 12: route generic kind event signing through the process
* signer handle when available (covers kind 30078 config publish, kind
* 31124 private skill events, startup adoption events, etc.). Falls back
* to the legacy raw-key path when no signer is set so local mode is
* unchanged. */
cJSON* event = NULL;
if (g_auth_signer) {
event = nostr_create_and_sign_event_with_signer(kind, content_to_publish, tags_copy, g_auth_signer, time(NULL));
} else {
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 : "<none>");
} 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));
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_state_summary(void) {
if (!g_cfg || !g_pool || g_cfg->relay_count <= 0 || !g_cfg->relays) {
return NULL;
}
size_t total = 1;
for (int i = 0; i < g_cfg->relay_count; i++) {
const char* relay = (g_cfg->relays[i] && g_cfg->relays[i][0] != '\0') ? g_cfg->relays[i] : "<unknown>";
const char* status = relay_status_str(nostr_relay_pool_get_relay_status(g_pool, relay));
total += strlen(relay) + 1 + strlen(status) + 2;
}
char* out = (char*)malloc(total);
if (!out) {
return NULL;
}
out[0] = '\0';
size_t used = 0;
for (int i = 0; i < g_cfg->relay_count; i++) {
const char* relay = (g_cfg->relays[i] && g_cfg->relays[i][0] != '\0') ? g_cfg->relays[i] : "<unknown>";
const char* status = relay_status_str(nostr_relay_pool_get_relay_status(g_pool, relay));
int n = snprintf(out + used,
total - used,
"%s%s=%s",
(i == 0) ? "" : "; ",
relay,
status);
if (n < 0) {
free(out);
return NULL;
}
size_t written = (size_t)n;
if (written >= (total - used)) {
used = total - 1;
break;
}
used += written;
}
return out;
}
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;
}
/* Phase 3 item 14: prefer signer-aware NIP-17 gift-wrap creation when a
* process signer is available; fall back to the legacy raw-key path in
* local mode / pre-Phase-3. Gift-wrap ownership is unchanged: each
* non-NULL entry in gift_wraps[] is freed by cJSON_Delete below. */
cJSON* gift_wraps[NIP17_MAX_GIFT_WRAPS] = {0};
int gift_count = 0;
if (g_auth_signer) {
gift_count = nostr_nip17_send_dm_with_signer(chat_event,
recipients,
1,
g_auth_signer,
gift_wraps,
NIP17_MAX_GIFT_WRAPS,
0);
} else {
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_auth_signer = 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);
}