v0.0.19 - Add nostr_delete/nostr_react/nostr_profile_get/nostr_relay_status tools with relay status backend

This commit is contained in:
Your Name
2026-03-01 17:45:57 -04:00
parent 66b4ebee79
commit a798f2c345
6 changed files with 809 additions and 4 deletions
+2 -2
View File
@@ -12,8 +12,8 @@
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
#define DIDACTYL_VERSION_MAJOR 0
#define DIDACTYL_VERSION_MINOR 0
#define DIDACTYL_VERSION_PATCH 18
#define DIDACTYL_VERSION "v0.0.18"
#define DIDACTYL_VERSION_PATCH 19
#define DIDACTYL_VERSION "v0.0.19"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"
+88
View File
@@ -1295,6 +1295,94 @@ int nostr_handler_connected_relay_count(void) {
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_get_admin_kind0_context(void) {
if (!g_cfg || !g_cfg->admin_context.enabled || !g_cfg->admin_context.track_kind_0) {
return NULL;
+1
View File
@@ -43,6 +43,7 @@ char* nostr_handler_get_admin_kind0_context(void);
char* nostr_handler_get_admin_kind10002_context(void);
char* nostr_handler_get_admin_kind1_notes_context(void);
int nostr_handler_is_wot_contact(const char* pubkey_hex);
char* nostr_handler_relay_status_json(void);
void nostr_handler_cleanup(void);
#endif
+469
View File
@@ -508,6 +508,51 @@ void tools_cleanup(tools_context_t* ctx) {
memset(ctx, 0, sizeof(*ctx));
}
static int is_hex_string_len(const char* s, size_t expected_len) {
if (!s || strlen(s) != expected_len) return 0;
for (size_t i = 0; i < expected_len; i++) {
unsigned char c = (unsigned char)s[i];
if (!isxdigit(c)) return 0;
}
return 1;
}
static cJSON* parse_tool_args_json(const char* args_json) {
const char* raw_args_json = args_json ? args_json : "{}";
raw_args_json = skip_ws(raw_args_json);
if (!raw_args_json || raw_args_json[0] == '\0') {
raw_args_json = "{}";
}
cJSON* args = cJSON_Parse(raw_args_json);
char* repaired_args_json = NULL;
if (args && cJSON_IsString(args) && args->valuestring) {
cJSON* nested = cJSON_Parse(args->valuestring);
if (nested) {
cJSON_Delete(args);
args = nested;
}
}
if (!args) {
repaired_args_json = sanitize_json_string_controls(raw_args_json);
if (repaired_args_json) {
args = cJSON_Parse(repaired_args_json);
if (args && cJSON_IsString(args) && args->valuestring) {
cJSON* nested = cJSON_Parse(args->valuestring);
if (nested) {
cJSON_Delete(args);
args = nested;
}
}
}
}
free(repaired_args_json);
return args;
}
char* tools_build_openai_schema_json(const tools_context_t* ctx) {
(void)ctx;
@@ -668,6 +713,117 @@ char* tools_build_openai_schema_json(const tools_context_t* ctx) {
cJSON_AddItemToObject(t6, "function", t6_fn);
cJSON_AddItemToArray(tools, t6);
cJSON* t7 = cJSON_CreateObject();
cJSON* t7_fn = cJSON_CreateObject();
cJSON* t7_params = cJSON_CreateObject();
cJSON* t7_props = cJSON_CreateObject();
cJSON* t7_required = cJSON_CreateArray();
cJSON_AddStringToObject(t7, "type", "function");
cJSON_AddStringToObject(t7_fn, "name", "nostr_delete");
cJSON_AddStringToObject(t7_fn, "description", "Request deletion of one or more previously published events (NIP-09 kind 5)");
cJSON_AddStringToObject(t7_params, "type", "object");
cJSON_AddItemToObject(t7_params, "properties", t7_props);
cJSON_AddItemToObject(t7_params, "required", t7_required);
cJSON* p_del_ids = cJSON_CreateObject();
cJSON_AddStringToObject(p_del_ids, "type", "array");
cJSON* p_del_ids_items = cJSON_CreateObject();
cJSON_AddStringToObject(p_del_ids_items, "type", "string");
cJSON_AddItemToObject(p_del_ids, "items", p_del_ids_items);
cJSON_AddItemToObject(t7_props, "event_ids", p_del_ids);
cJSON* p_del_kinds = cJSON_CreateObject();
cJSON_AddStringToObject(p_del_kinds, "type", "array");
cJSON* p_del_kinds_items = cJSON_CreateObject();
cJSON_AddStringToObject(p_del_kinds_items, "type", "integer");
cJSON_AddItemToObject(p_del_kinds, "items", p_del_kinds_items);
cJSON_AddItemToObject(t7_props, "kinds", p_del_kinds);
cJSON* p_del_reason = cJSON_CreateObject();
cJSON_AddStringToObject(p_del_reason, "type", "string");
cJSON_AddItemToObject(t7_props, "reason", p_del_reason);
cJSON_AddItemToArray(t7_required, cJSON_CreateString("event_ids"));
cJSON_AddItemToObject(t7_fn, "parameters", t7_params);
cJSON_AddItemToObject(t7, "function", t7_fn);
cJSON_AddItemToArray(tools, t7);
cJSON* t8 = cJSON_CreateObject();
cJSON* t8_fn = cJSON_CreateObject();
cJSON* t8_params = cJSON_CreateObject();
cJSON* t8_props = cJSON_CreateObject();
cJSON* t8_required = cJSON_CreateArray();
cJSON_AddStringToObject(t8, "type", "function");
cJSON_AddStringToObject(t8_fn, "name", "nostr_react");
cJSON_AddStringToObject(t8_fn, "description", "React to a Nostr event with like/dislike/emoji (NIP-25 kind 7)");
cJSON_AddStringToObject(t8_params, "type", "object");
cJSON_AddItemToObject(t8_params, "properties", t8_props);
cJSON_AddItemToObject(t8_params, "required", t8_required);
cJSON* p_react_event_id = cJSON_CreateObject();
cJSON_AddStringToObject(p_react_event_id, "type", "string");
cJSON_AddItemToObject(t8_props, "event_id", p_react_event_id);
cJSON* p_react_event_pubkey = cJSON_CreateObject();
cJSON_AddStringToObject(p_react_event_pubkey, "type", "string");
cJSON_AddItemToObject(t8_props, "event_pubkey", p_react_event_pubkey);
cJSON* p_react_event_kind = cJSON_CreateObject();
cJSON_AddStringToObject(p_react_event_kind, "type", "integer");
cJSON_AddItemToObject(t8_props, "event_kind", p_react_event_kind);
cJSON* p_react_reaction = cJSON_CreateObject();
cJSON_AddStringToObject(p_react_reaction, "type", "string");
cJSON_AddItemToObject(t8_props, "reaction", p_react_reaction);
cJSON_AddItemToArray(t8_required, cJSON_CreateString("event_id"));
cJSON_AddItemToArray(t8_required, cJSON_CreateString("event_pubkey"));
cJSON_AddItemToObject(t8_fn, "parameters", t8_params);
cJSON_AddItemToObject(t8, "function", t8_fn);
cJSON_AddItemToArray(tools, t8);
cJSON* t9 = cJSON_CreateObject();
cJSON* t9_fn = cJSON_CreateObject();
cJSON* t9_params = cJSON_CreateObject();
cJSON* t9_props = cJSON_CreateObject();
cJSON* t9_required = cJSON_CreateArray();
cJSON_AddStringToObject(t9, "type", "function");
cJSON_AddStringToObject(t9_fn, "name", "nostr_profile_get");
cJSON_AddStringToObject(t9_fn, "description", "Look up a Nostr profile (kind 0 metadata) by pubkey");
cJSON_AddStringToObject(t9_params, "type", "object");
cJSON_AddItemToObject(t9_params, "properties", t9_props);
cJSON_AddItemToObject(t9_params, "required", t9_required);
cJSON* p_profile_pubkey = cJSON_CreateObject();
cJSON_AddStringToObject(p_profile_pubkey, "type", "string");
cJSON_AddItemToObject(t9_props, "pubkey", p_profile_pubkey);
cJSON_AddItemToArray(t9_required, cJSON_CreateString("pubkey"));
cJSON_AddItemToObject(t9_fn, "parameters", t9_params);
cJSON_AddItemToObject(t9, "function", t9_fn);
cJSON_AddItemToArray(tools, t9);
cJSON* t10 = cJSON_CreateObject();
cJSON* t10_fn = cJSON_CreateObject();
cJSON* t10_params = cJSON_CreateObject();
cJSON* t10_props = cJSON_CreateObject();
cJSON_AddStringToObject(t10, "type", "function");
cJSON_AddStringToObject(t10_fn, "name", "nostr_relay_status");
cJSON_AddStringToObject(t10_fn, "description", "Get connection status and statistics for all relays");
cJSON_AddStringToObject(t10_params, "type", "object");
cJSON_AddItemToObject(t10_params, "properties", t10_props);
cJSON_AddItemToObject(t10_fn, "parameters", t10_params);
cJSON_AddItemToObject(t10, "function", t10_fn);
cJSON_AddItemToArray(tools, t10);
char* out = cJSON_PrintUnformatted(tools);
cJSON_Delete(tools);
return out;
@@ -955,6 +1111,307 @@ static char* execute_nostr_post_readme(tools_context_t* ctx, const char* args_js
return json;
}
static char* execute_nostr_delete(const char* args_json) {
cJSON* args = parse_tool_args_json(args_json);
if (!args) return json_error("invalid arguments JSON");
cJSON* event_ids = cJSON_GetObjectItemCaseSensitive(args, "event_ids");
cJSON* kinds = cJSON_GetObjectItemCaseSensitive(args, "kinds");
cJSON* reason = cJSON_GetObjectItemCaseSensitive(args, "reason");
if (!event_ids || !cJSON_IsArray(event_ids) || cJSON_GetArraySize(event_ids) <= 0) {
cJSON_Delete(args);
return json_error("nostr_delete requires non-empty event_ids array");
}
if (kinds && !cJSON_IsArray(kinds)) {
cJSON_Delete(args);
return json_error("nostr_delete kinds must be an array when provided");
}
if (reason && !cJSON_IsString(reason)) {
cJSON_Delete(args);
return json_error("nostr_delete reason must be a string when provided");
}
cJSON* tags = cJSON_CreateArray();
if (!tags) {
cJSON_Delete(args);
return json_error("failed to create tags");
}
int event_id_count = cJSON_GetArraySize(event_ids);
for (int i = 0; i < event_id_count; i++) {
cJSON* id = cJSON_GetArrayItem(event_ids, i);
if (!id || !cJSON_IsString(id) || !id->valuestring || !is_hex_string_len(id->valuestring, 64U)) {
cJSON_Delete(tags);
cJSON_Delete(args);
return json_error("nostr_delete event_ids must contain 64-char hex strings");
}
if (add_string_tag(tags, "e", id->valuestring) != 0) {
cJSON_Delete(tags);
cJSON_Delete(args);
return json_error("failed to add e tag");
}
}
if (kinds) {
int kind_count = cJSON_GetArraySize(kinds);
for (int i = 0; i < kind_count; i++) {
cJSON* k = cJSON_GetArrayItem(kinds, i);
if (!k || !cJSON_IsNumber(k)) {
cJSON_Delete(tags);
cJSON_Delete(args);
return json_error("nostr_delete kinds must contain integers");
}
char kind_buf[32];
snprintf(kind_buf, sizeof(kind_buf), "%d", (int)k->valuedouble);
if (add_string_tag(tags, "k", kind_buf) != 0) {
cJSON_Delete(tags);
cJSON_Delete(args);
return json_error("failed to add k tag");
}
}
}
nostr_publish_result_t publish_result;
memset(&publish_result, 0, sizeof(publish_result));
const char* reason_content = (reason && reason->valuestring) ? reason->valuestring : "";
int rc = nostr_handler_publish_kind_event(5, reason_content, tags, &publish_result);
cJSON_Delete(tags);
cJSON_Delete(args);
if (rc != 0) {
nostr_handler_publish_result_free(&publish_result);
return json_error("nostr_delete failed");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
nostr_handler_publish_result_free(&publish_result);
return NULL;
}
cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0);
cJSON_AddStringToObject(out, "message", "nostr_delete published");
cJSON_AddNumberToObject(out, "kind", publish_result.kind);
cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
cJSON_AddNumberToObject(out, "requested_event_count", event_id_count);
cJSON_AddNumberToObject(out, "relay_count", publish_result.relay_count);
cJSON_AddNumberToObject(out, "accepted_by_pool_count", publish_result.accepted_by_pool_count);
cJSON* relays = cJSON_CreateArray();
if (!relays) {
nostr_handler_publish_result_free(&publish_result);
cJSON_Delete(out);
return NULL;
}
for (int i = 0; i < publish_result.relay_count; i++) {
cJSON_AddItemToArray(relays, cJSON_CreateString(publish_result.relays[i] ? publish_result.relays[i] : ""));
}
cJSON_AddItemToObject(out, "relays", relays);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
nostr_handler_publish_result_free(&publish_result);
return json;
}
static char* execute_nostr_react(const char* args_json) {
cJSON* args = parse_tool_args_json(args_json);
if (!args) return json_error("invalid arguments JSON");
cJSON* event_id = cJSON_GetObjectItemCaseSensitive(args, "event_id");
cJSON* event_pubkey = cJSON_GetObjectItemCaseSensitive(args, "event_pubkey");
cJSON* event_kind = cJSON_GetObjectItemCaseSensitive(args, "event_kind");
cJSON* reaction = cJSON_GetObjectItemCaseSensitive(args, "reaction");
if (!event_id || !cJSON_IsString(event_id) || !event_id->valuestring || !is_hex_string_len(event_id->valuestring, 64U) ||
!event_pubkey || !cJSON_IsString(event_pubkey) || !event_pubkey->valuestring || !is_hex_string_len(event_pubkey->valuestring, 64U)) {
cJSON_Delete(args);
return json_error("nostr_react requires event_id and event_pubkey as 64-char hex strings");
}
if (event_kind && !cJSON_IsNumber(event_kind)) {
cJSON_Delete(args);
return json_error("nostr_react event_kind must be an integer when provided");
}
if (reaction && !cJSON_IsString(reaction)) {
cJSON_Delete(args);
return json_error("nostr_react reaction must be a string when provided");
}
cJSON* tags = cJSON_CreateArray();
if (!tags) {
cJSON_Delete(args);
return json_error("failed to create tags");
}
if (add_string_tag(tags, "e", event_id->valuestring) != 0 ||
add_string_tag(tags, "p", event_pubkey->valuestring) != 0) {
cJSON_Delete(tags);
cJSON_Delete(args);
return json_error("failed to add required reaction tags");
}
if (event_kind) {
char kind_buf[32];
snprintf(kind_buf, sizeof(kind_buf), "%d", (int)event_kind->valuedouble);
if (add_string_tag(tags, "k", kind_buf) != 0) {
cJSON_Delete(tags);
cJSON_Delete(args);
return json_error("failed to add k tag");
}
}
const char* reaction_content = (reaction && reaction->valuestring && reaction->valuestring[0] != '\0') ? reaction->valuestring : "+";
nostr_publish_result_t publish_result;
memset(&publish_result, 0, sizeof(publish_result));
int rc = nostr_handler_publish_kind_event(7, reaction_content, tags, &publish_result);
cJSON_Delete(tags);
cJSON_Delete(args);
if (rc != 0) {
nostr_handler_publish_result_free(&publish_result);
return json_error("nostr_react failed");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
nostr_handler_publish_result_free(&publish_result);
return NULL;
}
cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0);
cJSON_AddStringToObject(out, "message", "nostr_react published");
cJSON_AddStringToObject(out, "reaction", reaction_content);
cJSON_AddNumberToObject(out, "kind", publish_result.kind);
cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
cJSON_AddNumberToObject(out, "relay_count", publish_result.relay_count);
cJSON_AddNumberToObject(out, "accepted_by_pool_count", publish_result.accepted_by_pool_count);
cJSON* relays = cJSON_CreateArray();
if (!relays) {
nostr_handler_publish_result_free(&publish_result);
cJSON_Delete(out);
return NULL;
}
for (int i = 0; i < publish_result.relay_count; i++) {
cJSON_AddItemToArray(relays, cJSON_CreateString(publish_result.relays[i] ? publish_result.relays[i] : ""));
}
cJSON_AddItemToObject(out, "relays", relays);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
nostr_handler_publish_result_free(&publish_result);
return json;
}
static char* execute_nostr_profile_get(const char* args_json) {
cJSON* args = parse_tool_args_json(args_json);
if (!args) return json_error("invalid arguments JSON");
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey");
if (!pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring || !is_hex_string_len(pubkey->valuestring, 64U)) {
cJSON_Delete(args);
return json_error("nostr_profile_get requires pubkey as 64-char hex string");
}
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);
cJSON_Delete(args);
return json_error("failed to create profile query filter");
}
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(0));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey->valuestring));
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddNumberToObject(filter, "limit", 1);
char* events_json = nostr_handler_query_json(filter, 8000);
cJSON_Delete(filter);
cJSON_Delete(args);
if (!events_json) {
return json_error("nostr_profile_get query failed");
}
cJSON* events = cJSON_Parse(events_json);
free(events_json);
if (!events || !cJSON_IsArray(events)) {
cJSON_Delete(events);
return json_error("nostr_profile_get returned invalid events JSON");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
cJSON_Delete(events);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
int found = cJSON_GetArraySize(events) > 0;
cJSON_AddBoolToObject(out, "found", found ? 1 : 0);
if (found) {
cJSON* ev = cJSON_GetArrayItem(events, 0);
cJSON* ev_dup = ev ? cJSON_Duplicate(ev, 1) : NULL;
if (ev_dup) {
cJSON_AddItemToObject(out, "event", ev_dup);
}
cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL;
if (content && cJSON_IsString(content) && content->valuestring) {
cJSON* profile = cJSON_Parse(content->valuestring);
if (profile && cJSON_IsObject(profile)) {
cJSON_AddItemToObject(out, "profile", profile);
} else {
cJSON_Delete(profile);
cJSON_AddStringToObject(out, "profile_raw", content->valuestring);
}
}
}
cJSON_Delete(events);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
static char* execute_nostr_relay_status(const char* args_json) {
cJSON* args = parse_tool_args_json(args_json);
if (!args) return json_error("invalid arguments JSON");
cJSON_Delete(args);
char* status_json = nostr_handler_relay_status_json();
if (!status_json) {
return json_error("nostr_relay_status failed");
}
cJSON* status = cJSON_Parse(status_json);
free(status_json);
if (!status) {
return json_error("nostr_relay_status returned invalid JSON");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
cJSON_Delete(status);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddItemToObject(out, "status", status);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
static char* execute_nostr_query(const char* args_json) {
cJSON* args = cJSON_Parse(args_json ? args_json : "{}");
if (!args) return json_error("invalid arguments JSON");
@@ -1167,6 +1624,18 @@ char* tools_execute(tools_context_t* ctx, const char* tool_name, const char* arg
if (strcmp(tool_name, "nostr_post") == 0) {
return execute_nostr_post(args_json);
}
if (strcmp(tool_name, "nostr_delete") == 0) {
return execute_nostr_delete(args_json);
}
if (strcmp(tool_name, "nostr_react") == 0) {
return execute_nostr_react(args_json);
}
if (strcmp(tool_name, "nostr_profile_get") == 0) {
return execute_nostr_profile_get(args_json);
}
if (strcmp(tool_name, "nostr_relay_status") == 0) {
return execute_nostr_relay_status(args_json);
}
if (strcmp(tool_name, "nostr_query") == 0) {
return execute_nostr_query(args_json);
}