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
@@ -51,11 +51,11 @@ Agents learn capabilities through skills — Nostr events that any agent can di
Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers.
## Current Status — v0.0.18
## Current Status — v0.0.19
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.0.18Harden tool argument JSON parsing and set fixed README publish image tag
> Last release update: v0.0.19Add nostr_delete/nostr_react/nostr_profile_get/nostr_relay_status tools with relay status backend
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected
+247
View File
@@ -0,0 +1,247 @@
# Implementation Plan: 4 New Nostr Tools
## Overview
Add four new tools to Didactyl's tool system. Each tool follows the same pattern established by existing tools in `src/tools.c`:
1. Register OpenAI function schema in `tools_build_openai_schema_json()`
2. Implement `execute_*()` function
3. Wire dispatch in `tools_execute()`
All four tools reuse existing infrastructure — no new `nostr_handler` APIs needed for the first three, and only a thin new wrapper for the fourth.
---
## Tool 1: `nostr_delete` — Event Deletion Request (NIP-09)
### Purpose
Let the agent retract or request deletion of events it previously published. Essential for self-correction.
### OpenAI Schema
```json
{
"name": "nostr_delete",
"description": "Request deletion of one or more previously published events (NIP-09 kind 5)",
"parameters": {
"type": "object",
"properties": {
"event_ids": {
"type": "array",
"items": { "type": "string" },
"description": "Array of event ID hex strings to request deletion for"
},
"kinds": {
"type": "array",
"items": { "type": "integer" },
"description": "Array of kind numbers corresponding to the events being deleted"
},
"reason": {
"type": "string",
"description": "Optional reason for the deletion request"
}
},
"required": ["event_ids"]
}
}
```
### Implementation: `execute_nostr_delete()`
```
1. Parse args_json (with same hardened parsing as nostr_post)
2. Extract "event_ids" array (required) — validate each is a 64-char hex string
3. Extract "kinds" array (optional) — validate each is an integer
4. Extract "reason" string (optional) — use as content, default to empty string
5. Build tags array:
- For each event_id: add ["e", event_id]
- For each kind: add ["k", kind_as_string]
6. Call nostr_handler_publish_kind_event(5, reason, tags, &publish_result)
7. Return standard publish result JSON
```
### Files Modified
- `src/tools.c`: Add schema block in `tools_build_openai_schema_json()`, add `execute_nostr_delete()`, add dispatch case in `tools_execute()`
---
## Tool 2: `nostr_react` — Reactions (NIP-25)
### Purpose
Let the agent react to events — like, dislike, or emoji react. Gives the agent social presence.
### OpenAI Schema
```json
{
"name": "nostr_react",
"description": "React to a Nostr event with a like, dislike, or emoji (NIP-25 kind 7)",
"parameters": {
"type": "object",
"properties": {
"event_id": {
"type": "string",
"description": "Hex event ID of the event to react to"
},
"event_pubkey": {
"type": "string",
"description": "Hex pubkey of the event author"
},
"event_kind": {
"type": "integer",
"description": "Kind number of the event being reacted to"
},
"reaction": {
"type": "string",
"description": "Reaction content: + for like, - for dislike, or an emoji. Default: +"
}
},
"required": ["event_id", "event_pubkey"]
}
}
```
### Implementation: `execute_nostr_react()`
```
1. Parse args_json
2. Extract "event_id" (required, 64-char hex)
3. Extract "event_pubkey" (required, 64-char hex)
4. Extract "event_kind" (optional integer)
5. Extract "reaction" (optional string, default "+")
6. Build tags array:
- ["e", event_id]
- ["p", event_pubkey]
- If event_kind provided: ["k", kind_as_string]
7. Call nostr_handler_publish_kind_event(7, reaction, tags, &publish_result)
8. Return standard publish result JSON
```
### Files Modified
- `src/tools.c`: Same three insertion points as above
---
## Tool 3: `nostr_profile_get` — Profile Lookup (kind 0)
### Purpose
Let the agent look up any user's profile metadata. Useful for WoT reasoning, greeting users by name, checking identity.
### OpenAI Schema
```json
{
"name": "nostr_profile_get",
"description": "Look up a Nostr user profile (kind 0 metadata) by pubkey",
"parameters": {
"type": "object",
"properties": {
"pubkey": {
"type": "string",
"description": "Hex public key of the user to look up"
}
},
"required": ["pubkey"]
}
}
```
### Implementation: `execute_nostr_profile_get()`
```
1. Parse args_json
2. Extract "pubkey" (required, 64-char hex)
3. Build filter:
{
"kinds": [0],
"authors": [pubkey],
"limit": 1
}
4. Call nostr_handler_query_json(filter, 8000)
5. Parse the returned events JSON
6. If events found:
- Extract the first event's content (which is a JSON string of profile fields)
- Parse that content JSON
- Return { success: true, pubkey: ..., profile: { name, display_name, about, picture, nip05, ... } }
7. If no events: return { success: true, pubkey: ..., profile: null }
```
### Files Modified
- `src/tools.c`: Same three insertion points
---
## Tool 4: `nostr_relay_status` — Relay Health Dashboard
### Purpose
Let the agent introspect its own relay connectivity. Useful for self-diagnostics and admin status reports.
### OpenAI Schema
```json
{
"name": "nostr_relay_status",
"description": "Get connection status and statistics for all connected relays",
"parameters": {
"type": "object",
"properties": {}
}
}
```
### Implementation
This tool needs access to the relay pool, which is currently a `static` global in `nostr_handler.c`. Two options:
**Option A (preferred):** Add a new function to `nostr_handler.h`:
```c
char* nostr_handler_relay_status_json(void);
```
This function iterates the pool's relays using `nostr_relay_pool_list_relays()` and `nostr_relay_pool_get_relay_stats()`, builds a JSON array, and returns it.
**Option B:** Expose the pool pointer — not recommended, breaks encapsulation.
### `nostr_handler_relay_status_json()` in `nostr_handler.c`:
```
1. Call nostr_relay_pool_list_relays(g_pool, &urls, &statuses)
2. For each relay:
a. Get stats via nostr_relay_pool_get_relay_stats(g_pool, url)
b. Build JSON object with:
- url
- status (disconnected/connecting/connected/error)
- ping_latency_ms
- events_received
- events_published
- events_published_ok
- events_published_failed
- connection_uptime_start
3. Return JSON array as string
```
### `execute_nostr_relay_status()` in `tools.c`:
```
1. Parse args_json (accept empty/no args)
2. Call nostr_handler_relay_status_json()
3. Wrap in { success: true, relays: [...] }
4. Return
```
### Files Modified
- `src/nostr_handler.h`: Add `nostr_handler_relay_status_json()` declaration
- `src/nostr_handler.c`: Implement `nostr_handler_relay_status_json()`
- `src/tools.c`: Schema + execute + dispatch
---
## Implementation Order
1. **`nostr_delete`** — simplest, pure tag construction + existing publish
2. **`nostr_react`** — same pattern, slightly different tags
3. **`nostr_profile_get`** — uses existing query path, adds profile content parsing
4. **`nostr_relay_status`** — requires one new `nostr_handler` function
All four schemas should be added to `tools_build_openai_schema_json()` in a single pass, and all four dispatch cases added to `tools_execute()` in a single pass, to minimize diff churn.
## Build & Validate
After all four tools are implemented:
1. Run `./build_static.sh` to verify compilation
2. Run `./increment_and_push.sh "Add nostr_delete, nostr_react, nostr_profile_get, nostr_relay_status tools"`
+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);
}