Files
didactyl/src/nostr_handler.c
T
2026-02-28 16:27:33 -04:00

1147 lines
35 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"
static didactyl_config_t* g_cfg = NULL;
static nostr_relay_pool_t* g_pool = NULL;
static dm_callback_t g_dm_callback = NULL;
static void* g_dm_user_data = NULL;
static int g_poll_counter = 0;
static time_t g_start_time = 0;
static time_t g_last_status_log_time = 0;
static nostr_pool_relay_status_t* g_last_relay_statuses = NULL;
static char* g_system_context = NULL;
static unsigned char* g_startup_published = NULL;
static int g_startup_publish_tracking_enabled = 0;
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 pthread_mutex_t g_admin_ctx_mutex = PTHREAD_MUTEX_INITIALIZER;
#define DM_DEDUP_CACHE_SIZE 256
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;
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 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);
static void on_admin_context_event(cJSON* event, const char* relay_url, void* user_data);
static int parse_kind3_wot_contacts(cJSON* tags);
static void upsert_kind1_note(time_t created_at, const char* content);
static void log_relay_statuses(const char* reason) {
if (!g_pool || !g_cfg) {
return;
}
int info_level = (reason && strcmp(reason, "after init") == 0) ? 1 : 0;
if (info_level) {
DEBUG_INFO("[didactyl] relay status snapshot (%s)", reason ? reason : "periodic");
} else {
DEBUG_TRACE("[didactyl] relay status snapshot (%s)", reason ? reason : "periodic");
}
for (int i = 0; i < g_cfg->relay_count; i++) {
const char* relay = g_cfg->relays[i];
nostr_pool_relay_status_t status = nostr_relay_pool_get_relay_status(g_pool, relay);
const char* last_err = nostr_relay_pool_get_relay_last_connection_error(g_pool, relay);
double ping_ms = nostr_relay_pool_get_relay_ping_latency(g_pool, relay);
if (info_level) {
if (ping_ms > 0.0) {
DEBUG_INFO("[didactyl] - %s => %s (ping %.1f ms)",
relay,
relay_status_str(status),
ping_ms);
} else {
DEBUG_INFO("[didactyl] - %s => %s", relay, relay_status_str(status));
}
} else {
if (ping_ms > 0.0) {
DEBUG_TRACE("[didactyl] - %s => %s (ping %.1f ms)",
relay,
relay_status_str(status),
ping_ms);
} else {
DEBUG_TRACE("[didactyl] - %s => %s", relay, relay_status_str(status));
}
}
if (last_err && last_err[0] != '\0') {
DEBUG_WARN("[didactyl] - %s last_connection_error: %s", relay, last_err);
}
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(g_pool, relay);
if (stats) {
DEBUG_LOG("[didactyl] - %s stats attempts=%d failures=%d recv=%d pub_ok=%d pub_fail=%d",
relay,
stats->connection_attempts,
stats->connection_failures,
stats->events_received,
stats->events_published_ok,
stats->events_published_failed);
}
}
}
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) {
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");
}
}
}
}
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 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 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;
if (!event || !g_cfg || !g_dm_callback) {
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)) {
return;
}
const char* event_id_hex = (id && cJSON_IsString(id) && id->valuestring && strlen(id->valuestring) == 64U)
? id->valuestring
: NULL;
if ((int)kind->valuedouble != 4) {
return;
}
char recipient_pubkey_hex[65] = {0};
if (extract_first_p_tag(tags, recipient_pubkey_hex) != 0) {
return;
}
if (strcmp(recipient_pubkey_hex, g_cfg->keys.public_key_hex) != 0) {
return;
}
didactyl_sender_tier_t tier = DIDACTYL_SENDER_STRANGER;
if (strcmp(pubkey->valuestring, g_cfg->admin.pubkey) == 0) {
tier = DIDACTYL_SENDER_ADMIN;
} else if (g_cfg->security.wot.enabled && nostr_handler_is_wot_contact(pubkey->valuestring)) {
tier = DIDACTYL_SENDER_WOT;
}
if (tier == DIDACTYL_SENDER_STRANGER) {
if (!g_cfg->security.stranger.enabled) {
DEBUG_LOG("[didactyl] ignored DM from stranger %.16s... via %s",
pubkey->valuestring,
relay_url ? relay_url : "unknown relay");
return;
}
if (g_cfg->security.stranger_response[0] != '\0') {
(void)nostr_handler_send_dm(pubkey->valuestring, g_cfg->security.stranger_response);
}
return;
}
unsigned char sender_pubkey[32];
if (hex_to_pubkey(pubkey->valuestring, sender_pubkey) != 0) {
return;
}
char* decrypted = (char*)malloc(NOSTR_NIP04_MAX_PLAINTEXT_SIZE);
if (!decrypted) {
fprintf(stderr, "[didactyl] failed to allocate DM decrypt buffer\n");
return;
}
decrypted[0] = '\0';
trace_event_json("received encrypted DM event:", event);
if (nostr_nip04_decrypt(g_cfg->keys.private_key, sender_pubkey, content->valuestring, decrypted, NOSTR_NIP04_MAX_PLAINTEXT_SIZE) != NOSTR_SUCCESS) {
fprintf(stdout, "[didactyl] failed to decrypt incoming DM from %.16s...\n", pubkey->valuestring);
free(decrypted);
return;
}
trace_plaintext_dm("received decrypted DM content:", decrypted);
if (event_id_hex && dm_id_seen_or_remember(event_id_hex)) {
DEBUG_LOG("[didactyl] skipped duplicate DM event %.16s... from %.16s... via %s",
event_id_hex,
pubkey->valuestring,
relay_url ? relay_url : "unknown relay");
free(decrypted);
return;
}
DEBUG_INFO("[didactyl] received kind %d event %.16s... from %.16s... via %s tier=%d",
(int)kind->valuedouble,
event_id_hex ? event_id_hex : "<no-id>",
pubkey->valuestring,
relay_url ? relay_url : "unknown relay",
(int)tier);
g_dm_callback(pubkey->valuestring, decrypted, tier, g_dm_user_data);
free(decrypted);
}
static void on_eose(cJSON** events, int event_count, void* user_data) {
(void)events;
(void)event_count;
(void)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 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 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 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 && content && cJSON_IsString(content) && content->valuestring) {
free(g_admin_kind10002_json);
g_admin_kind10002_json = strdup(content->valuestring);
} 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);
}
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;
DEBUG_INFO("[didactyl] initializing relay pool with %d relays", g_cfg->relay_count);
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 = 10;
g_pool = nostr_relay_pool_create(&reconnect);
if (!g_pool) {
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]);
}
g_last_status_log_time = time(NULL);
log_relay_statuses("after init");
free(g_startup_published);
g_startup_published = NULL;
g_startup_publish_tracking_enabled = 0;
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));
if (cJSON_GetArraySize(profile_kinds) > 0) {
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);
nostr_pool_subscription_t* profile_sub = nostr_relay_pool_subscribe(
g_pool,
(const char**)g_cfg->relays,
g_cfg->relay_count,
profile_filter,
on_admin_context_event,
on_eose,
NULL,
0,
1,
NOSTR_POOL_EOSE_FULL_SET,
30,
120);
if (!profile_sub) {
rc = -1;
}
} else {
cJSON_Delete(profile_kinds);
cJSON_Delete(profile_authors);
}
cJSON_Delete(profile_filter);
if (g_cfg->admin_context.track_kind_1) {
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->admin.pubkey));
cJSON_AddItemToObject(notes_filter, "authors", notes_authors);
cJSON_AddNumberToObject(notes_filter, "limit", kind1_limit);
nostr_pool_subscription_t* notes_sub = nostr_relay_pool_subscribe(
g_pool,
(const char**)g_cfg->relays,
g_cfg->relay_count,
notes_filter,
on_admin_context_event,
on_eose,
NULL,
0,
1,
NOSTR_POOL_EOSE_FULL_SET,
30,
120);
cJSON_Delete(notes_filter);
if (!notes_sub) {
rc = -1;
}
}
if (rc == 0) {
DEBUG_INFO("[didactyl] admin context subscriptions active for admin %.16s...", 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;
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON* p_values = cJSON_CreateArray();
if (!filter || !kinds || !p_values) {
cJSON_Delete(filter);
cJSON_Delete(kinds);
cJSON_Delete(p_values);
return -1;
}
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(4));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToArray(p_values, cJSON_CreateString(g_cfg->keys.public_key_hex));
cJSON_AddItemToObject(filter, "#p", p_values);
cJSON_AddNumberToObject(filter, "since", (double)g_start_time);
cJSON_AddNumberToObject(filter, "limit", 100);
nostr_pool_subscription_t* sub = nostr_relay_pool_subscribe(
g_pool,
(const char**)g_cfg->relays,
g_cfg->relay_count,
filter,
on_event,
on_eose,
NULL,
0,
1,
NOSTR_POOL_EOSE_FULL_SET,
30,
120);
cJSON_Delete(filter);
if (!sub) {
fprintf(stderr, "[didactyl] DM subscription failed\n");
return -1;
}
DEBUG_INFO("[didactyl] DM subscription active for pubkey %.16s...", g_cfg->keys.public_key_hex);
return 0;
}
int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message) {
if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message) {
return -1;
}
unsigned char recipient_pubkey[32];
if (hex_to_pubkey(recipient_pubkey_hex, recipient_pubkey) != 0) {
return -1;
}
trace_plaintext_dm("sending plaintext DM content:", message);
char* encrypted = (char*)malloc(NOSTR_NIP04_MAX_ENCRYPTED_SIZE);
if (!encrypted) {
fprintf(stderr, "[didactyl] failed to allocate DM encrypt buffer\n");
return -1;
}
encrypted[0] = '\0';
if (nostr_nip04_encrypt(g_cfg->keys.private_key, recipient_pubkey, message, encrypted, NOSTR_NIP04_MAX_ENCRYPTED_SIZE) != NOSTR_SUCCESS) {
free(encrypted);
return -1;
}
cJSON* tags = create_dm_tags_for_recipient(recipient_pubkey_hex);
if (!tags) {
free(encrypted);
return -1;
}
cJSON* event = nostr_create_and_sign_event(4, encrypted, tags, g_cfg->keys.private_key, time(NULL));
cJSON_Delete(tags);
free(encrypted);
if (!event) {
return -1;
}
trace_event_json("sending encrypted DM event:", event);
log_publish_targets("publish DM");
const char** connected_relays = (const char**)calloc((size_t)g_cfg->relay_count, sizeof(char*));
if (!connected_relays) {
cJSON_Delete(event);
return -1;
}
int connected_count = 0;
for (int i = 0; i < g_cfg->relay_count; i++) {
if (nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]) == NOSTR_POOL_RELAY_CONNECTED) {
connected_relays[connected_count++] = g_cfg->relays[i];
}
}
int sent = 0;
if (connected_count > 0) {
sent = nostr_relay_pool_publish_async(
g_pool,
connected_relays,
connected_count,
event,
NULL,
NULL);
for (int i = 0; i < connected_count; i++) {
DEBUG_INFO("[didactyl] kind 4 event published to %s (async)", connected_relays[i]);
}
} else {
DEBUG_WARN("[didactyl] kind 4 event not queued: no connected relays");
}
cJSON* event_id = cJSON_GetObjectItemCaseSensitive(event, "id");
const char* out_event_id_hex = (event_id && cJSON_IsString(event_id) && event_id->valuestring) ? event_id->valuestring : "<no-id>";
DEBUG_INFO("[didactyl] sent DM %.16s... to %.16s... via %d connected relay(s)",
out_event_id_hex,
recipient_pubkey_hex,
sent);
free(connected_relays);
cJSON_Delete(event);
return sent > 0 ? 0 : -1;
}
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) {
if (!g_cfg || !g_pool || !content || !relay_urls || relay_count <= 0) {
return -1;
}
cJSON* tags_copy = NULL;
if (tags) {
tags_copy = cJSON_Duplicate(tags, 1);
if (!tags_copy) {
return -1;
}
}
cJSON* event = nostr_create_and_sign_event(kind, content, tags_copy, g_cfg->keys.private_key, time(NULL));
if (tags_copy) {
cJSON_Delete(tags_copy);
}
if (!event) {
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_Delete(event);
return sent > 0 ? 0 : -1;
}
int nostr_handler_publish_kind_event(int kind, const char* content, cJSON* tags) {
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");
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;
}
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 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;
}
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;
}
}
const char* one_relay[1] = { relay_url };
if (publish_kind_event_to_relays(se->kind, se->content, tags, one_relay, 1, reason) == 0) {
g_startup_published[slot] = 1;
} else {
DEBUG_WARN("[didactyl] startup event publish failed for kind=%d relay=%s", se->kind, relay_url);
}
if (se->kind == 31120 && !g_system_context) {
g_system_context = strdup(se->content);
}
cJSON_Delete(tags);
}
}
int nostr_handler_reconcile_startup_events(void) {
if (!g_cfg || !g_pool) {
return -1;
}
free(g_system_context);
g_system_context = NULL;
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;
}
for (int i = 0; i < g_cfg->startup_event_count; i++) {
startup_event_t* se = &g_cfg->startup_events[i];
if (se->kind == 31120 && se->content && !g_system_context) {
g_system_context = strdup(se->content);
break;
}
}
if (!g_system_context) {
g_system_context = strdup("You are Didactyl, a sovereign AI agent living on Nostr.");
}
for (int relay_index = 0; relay_index < g_cfg->relay_count; relay_index++) {
publish_pending_startup_events_for_relay_index(relay_index, "startup_reconcile");
}
return 0;
}
const char* nostr_handler_get_system_context(void) {
return g_system_context;
}
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_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;
}
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;
}
int rc = nostr_relay_pool_poll(g_pool, timeout_ms);
g_poll_counter++;
log_relay_state_changes();
time_t now = time(NULL);
if (g_last_status_log_time == 0 || difftime(now, g_last_status_log_time) >= 10.0) {
log_relay_statuses("periodic");
g_last_status_log_time = now;
}
return rc;
}
void nostr_handler_cleanup(void) {
if (g_pool) {
nostr_relay_pool_destroy(g_pool);
}
free(g_last_relay_statuses);
g_last_relay_statuses = NULL;
g_last_status_log_time = 0;
free(g_startup_published);
g_startup_published = NULL;
g_startup_publish_tracking_enabled = 0;
g_pool = NULL;
g_cfg = NULL;
g_dm_callback = NULL;
g_dm_user_data = NULL;
free(g_system_context);
g_system_context = 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_admin_ctx_mutex);
free_admin_context_locked();
pthread_mutex_unlock(&g_admin_ctx_mutex);
}