v0.0.19 - Add nostr_delete/nostr_react/nostr_profile_get/nostr_relay_status tools with relay status backend
This commit is contained in:
@@ -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.18 — Harden tool argument JSON parsing and set fixed README publish image tag
|
||||
> Last release update: v0.0.19 — Add 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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user