2362 lines
77 KiB
C
2362 lines
77 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"
|
|
|
|
#define NIP17_MAX_RELAYS 32
|
|
#define NIP17_MAX_GIFT_WRAPS 8
|
|
|
|
static didactyl_config_t* g_cfg = NULL;
|
|
static nostr_relay_pool_t* g_pool = NULL;
|
|
static dm_callback_t g_dm_callback = NULL;
|
|
static void* g_dm_user_data = NULL;
|
|
static int g_poll_counter = 0;
|
|
static time_t g_start_time = 0;
|
|
static nostr_pool_relay_status_t* g_last_relay_statuses = NULL;
|
|
static char* g_system_context = 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 pthread_mutex_t g_admin_ctx_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
static cJSON* g_self_skill_events = NULL;
|
|
static pthread_mutex_t g_self_skill_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;
|
|
|
|
#define SENDER_PROTOCOL_CACHE_SIZE 128
|
|
|
|
typedef struct {
|
|
char pubkey_hex[65];
|
|
dm_protocol_t protocol;
|
|
time_t seen_at;
|
|
} sender_protocol_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 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 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 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 on_self_skill_event(cJSON* event, const char* relay_url, void* user_data);
|
|
|
|
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]);
|
|
}
|
|
}
|
|
|
|
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 void self_skill_cache_upsert_event_locked(cJSON* event) {
|
|
if (!event || !cJSON_IsObject(event)) {
|
|
return;
|
|
}
|
|
|
|
cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind");
|
|
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
|
|
cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
|
|
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at");
|
|
|
|
if (!kind || !cJSON_IsNumber(kind) ||
|
|
!pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring ||
|
|
!tags || !cJSON_IsArray(tags)) {
|
|
return;
|
|
}
|
|
|
|
int kind_val = (int)kind->valuedouble;
|
|
if (kind_val != 31123 && kind_val != 31124 && kind_val != 10123) {
|
|
return;
|
|
}
|
|
|
|
if (!g_cfg || strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) != 0) {
|
|
return;
|
|
}
|
|
|
|
const char* d_tag = find_tag_value_local(tags, "d");
|
|
if ((kind_val == 31123 || kind_val == 31124) && (!d_tag || d_tag[0] == '\0')) {
|
|
return;
|
|
}
|
|
|
|
if (!g_self_skill_events || !cJSON_IsArray(g_self_skill_events)) {
|
|
cJSON_Delete(g_self_skill_events);
|
|
g_self_skill_events = cJSON_CreateArray();
|
|
if (!g_self_skill_events) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
double created = (created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0.0;
|
|
|
|
int n = cJSON_GetArraySize(g_self_skill_events);
|
|
for (int i = 0; i < n; i++) {
|
|
cJSON* existing = cJSON_GetArrayItem(g_self_skill_events, i);
|
|
if (!existing || !cJSON_IsObject(existing)) {
|
|
continue;
|
|
}
|
|
|
|
cJSON* ex_kind = cJSON_GetObjectItemCaseSensitive(existing, "kind");
|
|
cJSON* ex_pubkey = cJSON_GetObjectItemCaseSensitive(existing, "pubkey");
|
|
cJSON* ex_tags = cJSON_GetObjectItemCaseSensitive(existing, "tags");
|
|
if (!ex_kind || !cJSON_IsNumber(ex_kind) ||
|
|
!ex_pubkey || !cJSON_IsString(ex_pubkey) || !ex_pubkey->valuestring ||
|
|
!ex_tags || !cJSON_IsArray(ex_tags)) {
|
|
continue;
|
|
}
|
|
|
|
int ex_kind_val = (int)ex_kind->valuedouble;
|
|
if (ex_kind_val != kind_val || strcmp(ex_pubkey->valuestring, pubkey->valuestring) != 0) {
|
|
continue;
|
|
}
|
|
|
|
if (kind_val == 31123 || kind_val == 31124) {
|
|
const char* ex_d = find_tag_value_local(ex_tags, "d");
|
|
if (!ex_d || !d_tag || strcmp(ex_d, d_tag) != 0) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
cJSON* ex_created_at = cJSON_GetObjectItemCaseSensitive(existing, "created_at");
|
|
double ex_created = (ex_created_at && cJSON_IsNumber(ex_created_at)) ? ex_created_at->valuedouble : 0.0;
|
|
if (created < ex_created) {
|
|
return;
|
|
}
|
|
|
|
cJSON* dup = cJSON_Duplicate(event, 1);
|
|
if (!dup) {
|
|
return;
|
|
}
|
|
|
|
cJSON_ReplaceItemInArray(g_self_skill_events, i, dup);
|
|
return;
|
|
}
|
|
|
|
cJSON* dup = cJSON_Duplicate(event, 1);
|
|
if (!dup) {
|
|
return;
|
|
}
|
|
cJSON_AddItemToArray(g_self_skill_events, dup);
|
|
}
|
|
|
|
static int hex_to_pubkey(const char* hex, unsigned char out_pubkey[32]) {
|
|
if (!hex || !out_pubkey || strlen(hex) != 64U) {
|
|
return -1;
|
|
}
|
|
return nostr_hex_to_bytes(hex, out_pubkey, 32) == 0 ? 0 : -1;
|
|
}
|
|
|
|
static int duplicate_relay_list(const char** relay_urls, int relay_count, char*** out_relays) {
|
|
if (!out_relays) {
|
|
return -1;
|
|
}
|
|
*out_relays = NULL;
|
|
if (!relay_urls || relay_count <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
char** relays = (char**)calloc((size_t)relay_count, sizeof(char*));
|
|
if (!relays) {
|
|
return -1;
|
|
}
|
|
|
|
for (int i = 0; i < relay_count; i++) {
|
|
relays[i] = strdup(relay_urls[i] ? relay_urls[i] : "");
|
|
if (!relays[i]) {
|
|
for (int j = 0; j < i; j++) {
|
|
free(relays[j]);
|
|
}
|
|
free(relays);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
*out_relays = relays;
|
|
return 0;
|
|
}
|
|
|
|
void nostr_handler_publish_result_free(nostr_publish_result_t* result) {
|
|
if (!result) {
|
|
return;
|
|
}
|
|
|
|
if (result->relays) {
|
|
for (int i = 0; i < result->relay_count; i++) {
|
|
free(result->relays[i]);
|
|
}
|
|
free(result->relays);
|
|
}
|
|
|
|
memset(result, 0, sizeof(*result));
|
|
}
|
|
|
|
static void extract_d_tag(cJSON* tags, char* out_d_tag, size_t out_size) {
|
|
if (!out_d_tag || out_size == 0) {
|
|
return;
|
|
}
|
|
out_d_tag[0] = '\0';
|
|
|
|
if (!tags || !cJSON_IsArray(tags)) {
|
|
return;
|
|
}
|
|
|
|
int tag_count = cJSON_GetArraySize(tags);
|
|
for (int i = 0; i < tag_count; i++) {
|
|
cJSON* tag = cJSON_GetArrayItem(tags, i);
|
|
if (!tag || !cJSON_IsArray(tag)) {
|
|
continue;
|
|
}
|
|
|
|
cJSON* key = cJSON_GetArrayItem(tag, 0);
|
|
cJSON* value = cJSON_GetArrayItem(tag, 1);
|
|
if (!key || !value || !cJSON_IsString(key) || !cJSON_IsString(value) ||
|
|
!key->valuestring || !value->valuestring) {
|
|
continue;
|
|
}
|
|
|
|
if (strcmp(key->valuestring, "d") == 0) {
|
|
snprintf(out_d_tag, out_size, "%s", value->valuestring);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void fill_publish_result(nostr_publish_result_t* out_result,
|
|
int kind,
|
|
cJSON* tags,
|
|
const char* event_id_hex,
|
|
const char** relay_urls,
|
|
int relay_count,
|
|
int accepted_by_pool_count) {
|
|
if (!out_result) {
|
|
return;
|
|
}
|
|
|
|
nostr_handler_publish_result_free(out_result);
|
|
out_result->kind = kind;
|
|
out_result->relay_count = relay_count;
|
|
out_result->accepted_by_pool_count = accepted_by_pool_count;
|
|
out_result->success = accepted_by_pool_count > 0 ? 1 : 0;
|
|
|
|
if (event_id_hex) {
|
|
snprintf(out_result->event_id, sizeof(out_result->event_id), "%s", event_id_hex);
|
|
|
|
unsigned char event_id_bytes[32];
|
|
char note_bech32[128];
|
|
if (strlen(event_id_hex) == 64U &&
|
|
nostr_hex_to_bytes(event_id_hex, event_id_bytes, sizeof(event_id_bytes)) == 0 &&
|
|
nostr_key_to_bech32(event_id_bytes, "note", note_bech32) == 0) {
|
|
snprintf(out_result->note_uri, sizeof(out_result->note_uri), "nostr:%s", note_bech32);
|
|
}
|
|
}
|
|
|
|
extract_d_tag(tags, out_result->d_tag, sizeof(out_result->d_tag));
|
|
|
|
(void)kind;
|
|
(void)relay_urls;
|
|
|
|
(void)duplicate_relay_list(relay_urls, relay_count, &out_result->relays);
|
|
}
|
|
|
|
static cJSON* create_dm_tags_for_recipient(const char* recipient_pubkey_hex) {
|
|
cJSON* tags = cJSON_CreateArray();
|
|
if (!tags) {
|
|
return NULL;
|
|
}
|
|
|
|
cJSON* p_tag = cJSON_CreateArray();
|
|
if (!p_tag) {
|
|
cJSON_Delete(tags);
|
|
return NULL;
|
|
}
|
|
|
|
cJSON_AddItemToArray(p_tag, cJSON_CreateString("p"));
|
|
cJSON_AddItemToArray(p_tag, cJSON_CreateString(recipient_pubkey_hex));
|
|
cJSON_AddItemToArray(tags, p_tag);
|
|
return tags;
|
|
}
|
|
|
|
static int extract_first_p_tag(cJSON* tags, char out_pubkey_hex[65]) {
|
|
if (!tags || !cJSON_IsArray(tags)) {
|
|
return -1;
|
|
}
|
|
|
|
int n = cJSON_GetArraySize(tags);
|
|
for (int i = 0; i < n; i++) {
|
|
cJSON* tag = cJSON_GetArrayItem(tags, i);
|
|
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
|
continue;
|
|
}
|
|
|
|
cJSON* key = cJSON_GetArrayItem(tag, 0);
|
|
cJSON* val = cJSON_GetArrayItem(tag, 1);
|
|
if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val)) {
|
|
continue;
|
|
}
|
|
|
|
if (strcmp(key->valuestring, "p") == 0 && strlen(val->valuestring) == 64U) {
|
|
memcpy(out_pubkey_hex, val->valuestring, 65U);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
static void trace_event_json(const char* prefix, cJSON* event) {
|
|
if (g_debug_level < DEBUG_LEVEL_TRACE || !event) {
|
|
return;
|
|
}
|
|
|
|
char* event_json = cJSON_PrintUnformatted(event);
|
|
if (!event_json) {
|
|
DEBUG_TRACE("[didactyl] %s <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;
|
|
}
|
|
|
|
decrypted = (char*)malloc(NOSTR_NIP04_MAX_PLAINTEXT_SIZE);
|
|
if (!decrypted) {
|
|
fprintf(stderr, "[didactyl] failed to allocate DM decrypt buffer\n");
|
|
return;
|
|
}
|
|
decrypted[0] = '\0';
|
|
|
|
trace_event_json("received encrypted DM event:", event);
|
|
|
|
if (nostr_nip04_decrypt(g_cfg->keys.private_key, sender_pubkey, content->valuestring, decrypted, NOSTR_NIP04_MAX_PLAINTEXT_SIZE) != NOSTR_SUCCESS) {
|
|
fprintf(stdout, "[didactyl] failed to decrypt incoming DM from %.16s...\n", sender_pubkey_hex);
|
|
free(decrypted);
|
|
return;
|
|
}
|
|
|
|
trace_plaintext_dm("received decrypted DM content:", decrypted);
|
|
received_protocol = DM_PROTOCOL_NIP04;
|
|
} else if (kind_val == 1059) {
|
|
if (g_cfg->dm_protocol == DM_PROTOCOL_NIP04) {
|
|
DEBUG_TRACE("[didactyl] DEBUG on_event: ignoring kind1059 in dm_protocol=nip04 mode");
|
|
return;
|
|
}
|
|
|
|
cJSON* rumor = nostr_nip17_receive_dm(event, g_cfg->keys.private_key);
|
|
if (!rumor) {
|
|
DEBUG_TRACE("[didactyl] DEBUG on_event: failed to unwrap/decrypt NIP-17 gift wrap %.16s...",
|
|
event_id_hex ? event_id_hex : "<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 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 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 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_self_skill_event(cJSON* event, const char* relay_url, void* user_data) {
|
|
(void)relay_url;
|
|
(void)user_data;
|
|
|
|
if (!event || !g_cfg) {
|
|
return;
|
|
}
|
|
|
|
if (g_cfg->security.verify_signatures && nostr_verify_event_signature(event) != 0) {
|
|
return;
|
|
}
|
|
|
|
pthread_mutex_lock(&g_self_skill_mutex);
|
|
self_skill_cache_upsert_event_locked(event);
|
|
pthread_mutex_unlock(&g_self_skill_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;
|
|
}
|
|
|
|
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));
|
|
|
|
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_self_skills(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(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);
|
|
|
|
nostr_pool_subscription_t* skill_sub = nostr_relay_pool_subscribe(
|
|
g_pool,
|
|
(const char**)g_cfg->relays,
|
|
g_cfg->relay_count,
|
|
filter,
|
|
on_self_skill_event,
|
|
on_eose,
|
|
NULL,
|
|
0,
|
|
0,
|
|
NOSTR_POOL_EOSE_FULL_SET,
|
|
30,
|
|
120);
|
|
|
|
cJSON_Delete(filter);
|
|
if (!skill_sub) {
|
|
return -1;
|
|
}
|
|
|
|
DEBUG_INFO("[didactyl] self-event subscriptions active for pubkey %.16s...", g_cfg->keys.public_key_hex);
|
|
return 0;
|
|
}
|
|
|
|
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;
|
|
|
|
if (!need_kind4 && !need_kind1059) {
|
|
DEBUG_WARN("[didactyl] DM subscription skipped: no protocol selected");
|
|
return -1;
|
|
}
|
|
|
|
int subscribed_any = 0;
|
|
|
|
if (need_kind4) {
|
|
cJSON* filter4 = cJSON_CreateObject();
|
|
cJSON* kinds4 = cJSON_CreateArray();
|
|
cJSON* 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);
|
|
|
|
{
|
|
char* filter_str = cJSON_PrintUnformatted(filter4);
|
|
DEBUG_TRACE("[didactyl] DEBUG DM subscription filter kind4: %s", filter_str ? filter_str : "<null>");
|
|
free(filter_str);
|
|
}
|
|
|
|
nostr_pool_subscription_t* sub4 = nostr_relay_pool_subscribe(
|
|
g_pool,
|
|
(const char**)g_cfg->relays,
|
|
g_cfg->relay_count,
|
|
filter4,
|
|
on_event,
|
|
on_eose,
|
|
NULL,
|
|
0,
|
|
0,
|
|
NOSTR_POOL_EOSE_FULL_SET,
|
|
30,
|
|
120);
|
|
|
|
cJSON_Delete(filter4);
|
|
if (!sub4) {
|
|
fprintf(stderr, "[didactyl] kind4 DM subscription failed\n");
|
|
return -1;
|
|
}
|
|
|
|
subscribed_any = 1;
|
|
DEBUG_TRACE("[didactyl] DEBUG kind4 DM subscription sub=%p", (void*)sub4);
|
|
}
|
|
|
|
if (need_kind1059) {
|
|
time_t since_1059 = g_start_time;
|
|
|
|
cJSON* filter1059 = cJSON_CreateObject();
|
|
cJSON* kinds1059 = cJSON_CreateArray();
|
|
cJSON* p_values1059 = cJSON_CreateArray();
|
|
if (!filter1059 || !kinds1059 || !p_values1059) {
|
|
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, "since", (double)since_1059);
|
|
cJSON_AddNumberToObject(filter1059, "limit", 400);
|
|
|
|
{
|
|
char* filter_str = cJSON_PrintUnformatted(filter1059);
|
|
DEBUG_TRACE("[didactyl] DEBUG DM subscription filter kind1059: %s", filter_str ? filter_str : "<null>");
|
|
DEBUG_TRACE("[didactyl] DEBUG kind1059 since=%ld start=%ld", (long)since_1059, (long)g_start_time);
|
|
free(filter_str);
|
|
}
|
|
|
|
nostr_pool_subscription_t* sub1059 = nostr_relay_pool_subscribe(
|
|
g_pool,
|
|
(const char**)g_cfg->relays,
|
|
g_cfg->relay_count,
|
|
filter1059,
|
|
on_event,
|
|
on_eose,
|
|
NULL,
|
|
0,
|
|
0,
|
|
NOSTR_POOL_EOSE_FULL_SET,
|
|
30,
|
|
120);
|
|
|
|
cJSON_Delete(filter1059);
|
|
if (!sub1059) {
|
|
fprintf(stderr, "[didactyl] kind1059 DM subscription failed\n");
|
|
return -1;
|
|
}
|
|
|
|
subscribed_any = 1;
|
|
DEBUG_TRACE("[didactyl] DEBUG kind1059 DM subscription sub=%p", (void*)sub1059);
|
|
}
|
|
|
|
if (!subscribed_any) {
|
|
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_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;
|
|
}
|
|
|
|
int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* message) {
|
|
if (!recipient_pubkey_hex || !message) {
|
|
return -1;
|
|
}
|
|
|
|
if (!g_cfg) {
|
|
return nostr_handler_send_dm(recipient_pubkey_hex, message);
|
|
}
|
|
|
|
if (g_cfg->dm_protocol == DM_PROTOCOL_NIP04) {
|
|
return nostr_handler_send_dm(recipient_pubkey_hex, message);
|
|
}
|
|
|
|
if (g_cfg->dm_protocol == DM_PROTOCOL_NIP17) {
|
|
return nostr_handler_send_dm_nip17(recipient_pubkey_hex, message, NULL);
|
|
}
|
|
|
|
dm_protocol_t target = sender_protocol_lookup(recipient_pubkey_hex);
|
|
if (target == DM_PROTOCOL_NIP17) {
|
|
int rc17 = nostr_handler_send_dm_nip17(recipient_pubkey_hex, message, NULL);
|
|
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(recipient_pubkey_hex, message);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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* 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) {
|
|
pthread_mutex_lock(&g_self_skill_mutex);
|
|
self_skill_cache_upsert_event_locked(event);
|
|
pthread_mutex_unlock(&g_self_skill_mutex);
|
|
}
|
|
|
|
cJSON_Delete(event);
|
|
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;
|
|
}
|
|
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;
|
|
}
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
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_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("[]");
|
|
}
|
|
|
|
const char* nostr_handler_get_startup_display_name(void) {
|
|
return g_startup_display_name;
|
|
}
|
|
|
|
int nostr_handler_connected_relay_count(void) {
|
|
if (!g_cfg || !g_pool) {
|
|
return 0;
|
|
}
|
|
|
|
int connected = 0;
|
|
for (int i = 0; i < g_cfg->relay_count; i++) {
|
|
if (nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]) == NOSTR_POOL_RELAY_CONNECTED) {
|
|
connected++;
|
|
}
|
|
}
|
|
|
|
return connected;
|
|
}
|
|
|
|
char* nostr_handler_relay_status_json(void) {
|
|
if (!g_pool) {
|
|
return NULL;
|
|
}
|
|
|
|
char** relay_urls = NULL;
|
|
nostr_pool_relay_status_t* statuses = NULL;
|
|
int relay_count = nostr_relay_pool_list_relays(g_pool, &relay_urls, &statuses);
|
|
if (relay_count < 0) {
|
|
return NULL;
|
|
}
|
|
|
|
cJSON* root = cJSON_CreateObject();
|
|
cJSON* relays = cJSON_CreateArray();
|
|
if (!root || !relays) {
|
|
cJSON_Delete(root);
|
|
cJSON_Delete(relays);
|
|
if (relay_urls) {
|
|
for (int i = 0; i < relay_count; i++) {
|
|
free(relay_urls[i]);
|
|
}
|
|
free(relay_urls);
|
|
}
|
|
free(statuses);
|
|
return NULL;
|
|
}
|
|
|
|
int connected_count = 0;
|
|
for (int i = 0; i < relay_count; i++) {
|
|
cJSON* relay = cJSON_CreateObject();
|
|
if (!relay) {
|
|
continue;
|
|
}
|
|
|
|
const char* url = (relay_urls && relay_urls[i]) ? relay_urls[i] : "";
|
|
nostr_pool_relay_status_t st = statuses ? statuses[i] : NOSTR_POOL_RELAY_DISCONNECTED;
|
|
if (st == NOSTR_POOL_RELAY_CONNECTED) {
|
|
connected_count++;
|
|
}
|
|
|
|
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(g_pool, url);
|
|
|
|
cJSON_AddStringToObject(relay, "url", url);
|
|
cJSON_AddStringToObject(relay, "status", relay_status_str(st));
|
|
|
|
if (stats) {
|
|
cJSON_AddNumberToObject(relay, "events_received", stats->events_received);
|
|
cJSON_AddNumberToObject(relay, "events_published", stats->events_published);
|
|
cJSON_AddNumberToObject(relay, "events_published_ok", stats->events_published_ok);
|
|
cJSON_AddNumberToObject(relay, "events_published_failed", stats->events_published_failed);
|
|
cJSON_AddNumberToObject(relay, "ping_latency_current", stats->ping_latency_current);
|
|
cJSON_AddNumberToObject(relay, "ping_latency_avg", stats->ping_latency_avg);
|
|
cJSON_AddNumberToObject(relay, "query_latency_avg", stats->query_latency_avg);
|
|
cJSON_AddNumberToObject(relay, "publish_latency_avg", stats->publish_latency_avg);
|
|
cJSON_AddNumberToObject(relay, "connection_uptime_start", (double)stats->connection_uptime_start);
|
|
cJSON_AddNumberToObject(relay, "last_event_time", (double)stats->last_event_time);
|
|
}
|
|
|
|
const char* last_pub_err = nostr_relay_pool_get_relay_last_publish_error(g_pool, url);
|
|
const char* last_conn_err = nostr_relay_pool_get_relay_last_connection_error(g_pool, url);
|
|
if (last_pub_err && last_pub_err[0] != '\0') {
|
|
cJSON_AddStringToObject(relay, "last_publish_error", last_pub_err);
|
|
}
|
|
if (last_conn_err && last_conn_err[0] != '\0') {
|
|
cJSON_AddStringToObject(relay, "last_connection_error", last_conn_err);
|
|
}
|
|
|
|
cJSON_AddItemToArray(relays, relay);
|
|
}
|
|
|
|
cJSON_AddNumberToObject(root, "relay_count", relay_count);
|
|
cJSON_AddNumberToObject(root, "connected_count", connected_count);
|
|
cJSON_AddItemToObject(root, "relays", relays);
|
|
|
|
char* out = cJSON_PrintUnformatted(root);
|
|
cJSON_Delete(root);
|
|
|
|
if (relay_urls) {
|
|
for (int i = 0; i < relay_count; i++) {
|
|
free(relay_urls[i]);
|
|
}
|
|
free(relay_urls);
|
|
}
|
|
free(statuses);
|
|
|
|
return out;
|
|
}
|
|
|
|
char* nostr_handler_relay_info_json(const char* relay_url) {
|
|
if (!relay_url || relay_url[0] == '\0') {
|
|
return NULL;
|
|
}
|
|
|
|
nostr_relay_info_t* info = NULL;
|
|
if (nostr_nip11_fetch_relay_info(relay_url, &info, 10) != NOSTR_SUCCESS || !info) {
|
|
return NULL;
|
|
}
|
|
|
|
cJSON* root = cJSON_CreateObject();
|
|
if (!root) {
|
|
nostr_nip11_relay_info_free(info);
|
|
return NULL;
|
|
}
|
|
|
|
cJSON* basic = cJSON_CreateObject();
|
|
if (!basic) {
|
|
cJSON_Delete(root);
|
|
nostr_nip11_relay_info_free(info);
|
|
return NULL;
|
|
}
|
|
|
|
if (info->basic.name) cJSON_AddStringToObject(basic, "name", info->basic.name);
|
|
if (info->basic.description) cJSON_AddStringToObject(basic, "description", info->basic.description);
|
|
if (info->basic.pubkey) cJSON_AddStringToObject(basic, "pubkey", info->basic.pubkey);
|
|
if (info->basic.contact) cJSON_AddStringToObject(basic, "contact", info->basic.contact);
|
|
if (info->basic.software) cJSON_AddStringToObject(basic, "software", info->basic.software);
|
|
if (info->basic.version) cJSON_AddStringToObject(basic, "version", info->basic.version);
|
|
|
|
cJSON* supported_nips = cJSON_CreateArray();
|
|
if (!supported_nips) {
|
|
cJSON_Delete(root);
|
|
cJSON_Delete(basic);
|
|
nostr_nip11_relay_info_free(info);
|
|
return NULL;
|
|
}
|
|
for (size_t i = 0; i < info->basic.supported_nips_count; i++) {
|
|
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(info->basic.supported_nips[i]));
|
|
}
|
|
cJSON_AddItemToObject(basic, "supported_nips", supported_nips);
|
|
cJSON_AddItemToObject(root, "basic", basic);
|
|
|
|
if (info->has_limitations) {
|
|
cJSON* limitations = cJSON_CreateObject();
|
|
if (!limitations) {
|
|
cJSON_Delete(root);
|
|
nostr_nip11_relay_info_free(info);
|
|
return NULL;
|
|
}
|
|
|
|
cJSON_AddNumberToObject(limitations, "max_message_length", info->limitations.max_message_length);
|
|
cJSON_AddNumberToObject(limitations, "max_subscriptions", info->limitations.max_subscriptions);
|
|
cJSON_AddNumberToObject(limitations, "max_filters", info->limitations.max_filters);
|
|
cJSON_AddNumberToObject(limitations, "max_limit", info->limitations.max_limit);
|
|
cJSON_AddNumberToObject(limitations, "max_subid_length", info->limitations.max_subid_length);
|
|
cJSON_AddNumberToObject(limitations, "min_prefix", info->limitations.min_prefix);
|
|
cJSON_AddNumberToObject(limitations, "max_event_tags", info->limitations.max_event_tags);
|
|
cJSON_AddNumberToObject(limitations, "max_content_length", info->limitations.max_content_length);
|
|
cJSON_AddNumberToObject(limitations, "min_pow_difficulty", info->limitations.min_pow_difficulty);
|
|
cJSON_AddNumberToObject(limitations, "auth_required", info->limitations.auth_required);
|
|
cJSON_AddNumberToObject(limitations, "payment_required", info->limitations.payment_required);
|
|
cJSON_AddNumberToObject(limitations, "restricted_writes", info->limitations.restricted_writes);
|
|
cJSON_AddNumberToObject(limitations, "created_at_lower_limit", (double)info->limitations.created_at_lower_limit);
|
|
cJSON_AddNumberToObject(limitations, "created_at_upper_limit", (double)info->limitations.created_at_upper_limit);
|
|
cJSON_AddItemToObject(root, "limitations", limitations);
|
|
}
|
|
|
|
if (info->has_content_limitations) {
|
|
cJSON* countries = cJSON_CreateArray();
|
|
if (!countries) {
|
|
cJSON_Delete(root);
|
|
nostr_nip11_relay_info_free(info);
|
|
return NULL;
|
|
}
|
|
for (size_t i = 0; i < info->content_limitations.relay_countries_count; i++) {
|
|
if (info->content_limitations.relay_countries[i]) {
|
|
cJSON_AddItemToArray(countries, cJSON_CreateString(info->content_limitations.relay_countries[i]));
|
|
}
|
|
}
|
|
cJSON_AddItemToObject(root, "relay_countries", countries);
|
|
}
|
|
|
|
if (info->has_community_preferences) {
|
|
cJSON* prefs = cJSON_CreateObject();
|
|
if (!prefs) {
|
|
cJSON_Delete(root);
|
|
nostr_nip11_relay_info_free(info);
|
|
return NULL;
|
|
}
|
|
|
|
cJSON* language_tags = cJSON_CreateArray();
|
|
cJSON* tags = cJSON_CreateArray();
|
|
if (!language_tags || !tags) {
|
|
cJSON_Delete(root);
|
|
cJSON_Delete(prefs);
|
|
cJSON_Delete(language_tags);
|
|
cJSON_Delete(tags);
|
|
nostr_nip11_relay_info_free(info);
|
|
return NULL;
|
|
}
|
|
|
|
for (size_t i = 0; i < info->community_preferences.language_tags_count; i++) {
|
|
if (info->community_preferences.language_tags[i]) {
|
|
cJSON_AddItemToArray(language_tags, cJSON_CreateString(info->community_preferences.language_tags[i]));
|
|
}
|
|
}
|
|
for (size_t i = 0; i < info->community_preferences.tags_count; i++) {
|
|
if (info->community_preferences.tags[i]) {
|
|
cJSON_AddItemToArray(tags, cJSON_CreateString(info->community_preferences.tags[i]));
|
|
}
|
|
}
|
|
|
|
cJSON_AddItemToObject(prefs, "language_tags", language_tags);
|
|
cJSON_AddItemToObject(prefs, "tags", tags);
|
|
if (info->community_preferences.posting_policy) {
|
|
cJSON_AddStringToObject(prefs, "posting_policy", info->community_preferences.posting_policy);
|
|
}
|
|
cJSON_AddItemToObject(root, "community_preferences", prefs);
|
|
}
|
|
|
|
if (info->has_icon && info->icon.icon) {
|
|
cJSON_AddStringToObject(root, "icon", info->icon.icon);
|
|
}
|
|
|
|
char* out = cJSON_PrintUnformatted(root);
|
|
cJSON_Delete(root);
|
|
nostr_nip11_relay_info_free(info);
|
|
return out;
|
|
}
|
|
|
|
int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* message, const char* subject) {
|
|
if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message || recipient_pubkey_hex[0] == '\0' || message[0] == '\0') {
|
|
return -1;
|
|
}
|
|
|
|
const char* target_relays[NIP17_MAX_RELAYS] = {0};
|
|
int target_count = 0;
|
|
|
|
for (int i = 0; i < g_cfg->relay_count && target_count < NIP17_MAX_RELAYS; i++) {
|
|
if (nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]) == NOSTR_POOL_RELAY_CONNECTED) {
|
|
target_relays[target_count++] = g_cfg->relays[i];
|
|
}
|
|
}
|
|
|
|
DEBUG_TRACE("[didactyl] NIP-17 send prep: recipient=%.16s... connected_targets=%d",
|
|
recipient_pubkey_hex,
|
|
target_count);
|
|
|
|
if (target_count <= 0) {
|
|
DEBUG_WARN("[didactyl] NIP-17 send aborted: no connected relays for recipient %.16s...",
|
|
recipient_pubkey_hex);
|
|
return -1;
|
|
}
|
|
|
|
const char* recipients[1] = { recipient_pubkey_hex };
|
|
cJSON* chat_event = nostr_nip17_create_chat_event(
|
|
message,
|
|
recipients,
|
|
1,
|
|
(subject && subject[0] != '\0') ? subject : NULL,
|
|
NULL,
|
|
NULL,
|
|
g_cfg->keys.public_key_hex);
|
|
if (!chat_event) {
|
|
return -1;
|
|
}
|
|
|
|
cJSON* gift_wraps[NIP17_MAX_GIFT_WRAPS] = {0};
|
|
int gift_count = nostr_nip17_send_dm(chat_event,
|
|
recipients,
|
|
1,
|
|
g_cfg->keys.private_key,
|
|
gift_wraps,
|
|
NIP17_MAX_GIFT_WRAPS,
|
|
0);
|
|
cJSON_Delete(chat_event);
|
|
if (gift_count <= 0) {
|
|
DEBUG_WARN("[didactyl] NIP-17 send aborted: gift_wrap creation failed for %.16s...",
|
|
recipient_pubkey_hex);
|
|
return -1;
|
|
}
|
|
|
|
int any_sent = 0;
|
|
int sent_count = 0;
|
|
|
|
for (int i = 0; i < gift_count; i++) {
|
|
if (!gift_wraps[i]) {
|
|
continue;
|
|
}
|
|
|
|
int sent = nostr_relay_pool_publish_async(g_pool,
|
|
target_relays,
|
|
target_count,
|
|
gift_wraps[i],
|
|
NULL,
|
|
NULL);
|
|
if (sent > 0) {
|
|
any_sent = 1;
|
|
sent_count += sent;
|
|
}
|
|
|
|
for (int r = 0; r < target_count; r++) {
|
|
DEBUG_INFO("[didactyl] kind 1059 event published to %s (async)", target_relays[r]);
|
|
}
|
|
|
|
cJSON_Delete(gift_wraps[i]);
|
|
}
|
|
|
|
DEBUG_TRACE("[didactyl] NIP-17 send complete: recipient=%.16s... sent_count=%d",
|
|
recipient_pubkey_hex,
|
|
sent_count);
|
|
|
|
if (!any_sent) {
|
|
DEBUG_WARN("[didactyl] NIP-17 send failed: no relay accepted publish for recipient %.16s...",
|
|
recipient_pubkey_hex);
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
} else if ((g_poll_counter % 200ULL) == 0ULL) {
|
|
DEBUG_TRACE("[didactyl] poll heartbeat: timeout=%d elapsed=%.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) {
|
|
if (g_pool) {
|
|
nostr_relay_pool_destroy(g_pool);
|
|
}
|
|
|
|
free(g_last_relay_statuses);
|
|
g_last_relay_statuses = NULL;
|
|
free(g_startup_published);
|
|
g_startup_published = NULL;
|
|
g_startup_publish_tracking_enabled = 0;
|
|
g_startup_kind1_already_exists = 0;
|
|
snprintf(g_startup_display_name, sizeof(g_startup_display_name), "%s", "Didactyl");
|
|
|
|
g_pool = NULL;
|
|
g_cfg = NULL;
|
|
g_dm_callback = NULL;
|
|
g_dm_user_data = NULL;
|
|
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_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_admin_ctx_mutex);
|
|
free_admin_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);
|
|
}
|