v0.0.20 - Add Tier2/Tier3 Nostr tools: nip05 lookup, encode/decode, dm send, relay info, nip44 encrypt/decrypt, nip17 dm, list manage

This commit is contained in:
Your Name
2026-03-01 18:18:46 -04:00
parent a798f2c345
commit fea0fdf5c9
6 changed files with 1697 additions and 188 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.19
## Current Status — v0.0.20
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.0.19 — Add nostr_delete/nostr_react/nostr_profile_get/nostr_relay_status tools with relay status backend
> Last release update: v0.0.20 — Add Tier2/Tier3 Nostr tools: nip05 lookup, encode/decode, dm send, relay info, nip44 encrypt/decrypt, nip17 dm, list manage
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected
+533 -184
View File
@@ -1,247 +1,596 @@
# Implementation Plan: 4 New Nostr Tools
# Implementation Plan: Nostr Tools
## Overview
## Current Tool Inventory (v0.0.19)
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 | Kind/NIP | Status |
|---|------|----------|--------|
| 1 | `nostr_post` | Any kind | ✅ Shipped |
| 2 | `nostr_post_readme` | 30023 / NIP-23 | ✅ Shipped |
| 3 | `nostr_query` | Filter-based | ✅ Shipped |
| 4 | `nostr_delete` | 5 / NIP-09 | ✅ Shipped |
| 5 | `nostr_react` | 7 / NIP-25 | ✅ Shipped |
| 6 | `nostr_profile_get` | 0 query | ✅ Shipped |
| 7 | `nostr_relay_status` | Pool stats | ✅ Shipped |
| 8 | `shell_exec` | OS | ✅ Shipped |
| 9 | `file_read` | OS | ✅ Shipped |
| 10 | `file_write` | OS | ✅ Shipped |
---
## Tool 1: `nostr_delete` — Event Deletion Request (NIP-09)
## Pattern for All New Tools
### Purpose
Let the agent retract or request deletion of events it previously published. Essential for self-correction.
Every tool follows the same three-step pattern in [`src/tools.c`](../src/tools.c):
### 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"]
}
}
```
1. **Schema** — Add OpenAI function schema block in [`tools_build_openai_schema_json()`](../src/tools.c:511)
2. **Executor** — Implement `static char* execute_<name>()` function
3. **Dispatch** — Add `strcmp` case in [`tools_execute()`](../src/tools.c:1620)
### 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()`
Use the shared [`parse_tool_args_json()`](../src/tools.c:527) helper for hardened argument parsing.
---
## Tool 2: `nostr_react` — Reactions (NIP-25)
## Tier 2 Tools — Medium Value, Moderate Implementation
### Purpose
Let the agent react to events — like, dislike, or emoji react. Gives the agent social presence.
### Tool 11: `nostr_nip05_lookup` — DNS Identity Verification (NIP-05)
### OpenAI Schema
#### Purpose
Verify `user@domain.com` NIP-05 identifiers. Useful for trust decisions and profile enrichment.
#### OpenAI Schema
```json
{
"name": "nostr_react",
"description": "React to a Nostr event with a like, dislike, or emoji (NIP-25 kind 7)",
"name": "nostr_nip05_lookup",
"description": "Verify or look up a NIP-05 DNS identifier and return the associated pubkey and relays",
"parameters": {
"type": "object",
"properties": {
"event_id": {
"identifier": {
"type": "string",
"description": "Hex event ID of the event to react to"
"description": "NIP-05 identifier, e.g. user@domain.com"
},
"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"
"description": "Optional 64-char hex pubkey to verify against the identifier"
}
},
"required": ["pubkey"]
"required": ["identifier"]
}
}
```
### Implementation: `execute_nostr_profile_get()`
#### Implementation: `execute_nostr_nip05_lookup()`
```
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 }
2. Extract "identifier" (required string, must contain @)
3. Extract "pubkey" (optional 64-char hex)
4. If pubkey provided:
- Call nostr_nip05_verify(identifier, pubkey, &relays, &relay_count, 10)
- Return { success, verified: true/false, pubkey, relays }
5. If no pubkey:
- Call nostr_nip05_lookup(identifier, pubkey_out, &relays, &relay_count, 10)
- Return { success, pubkey: pubkey_out, relays }
6. Free relay array after serializing
```
### Files Modified
- `src/tools.c`: Same three insertion points
#### Library API Used
- [`nostr_nip05_lookup()`](../nostr_core_lib/nostr_core/nip005.h:13)
- [`nostr_nip05_verify()`](../nostr_core_lib/nostr_core/nip005.h:15)
#### Files Modified
- `src/tools.c`: Schema + executor + dispatch
#### Notes
- Involves HTTP network I/O (fetches `/.well-known/nostr.json` from the domain)
- May block for up to `timeout_seconds` — use 10s default
- Must free the `relays` array returned by the library
---
## Tool 4: `nostr_relay_status` — Relay Health Dashboard
### Tool 12: `nostr_encode` — Bech32 Entity Encoding (NIP-19/NIP-21)
### Purpose
Let the agent introspect its own relay connectivity. Useful for self-diagnostics and admin status reports.
#### Purpose
Encode hex pubkeys, event IDs, or addressable coordinates into shareable `npub`, `note`, `nprofile`, `nevent`, `naddr` URIs.
### OpenAI Schema
#### OpenAI Schema
```json
{
"name": "nostr_relay_status",
"description": "Get connection status and statistics for all connected relays",
"name": "nostr_encode",
"description": "Encode a Nostr entity into a bech32 URI: npub, note, nprofile, nevent, or naddr",
"parameters": {
"type": "object",
"properties": {}
"properties": {
"type": {
"type": "string",
"description": "Entity type: npub, note, nprofile, nevent, or naddr"
},
"hex": {
"type": "string",
"description": "64-char hex value: pubkey for npub/nprofile, event_id for note/nevent"
},
"relays": {
"type": "array",
"items": { "type": "string" },
"description": "Optional relay hints for nprofile, nevent, naddr"
},
"kind": {
"type": "integer",
"description": "Kind number, required for naddr"
},
"identifier": {
"type": "string",
"description": "d-tag identifier, required for naddr"
}
},
"required": ["type", "hex"]
}
}
```
### 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`:
#### Implementation: `execute_nostr_encode()`
```
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
1. Parse args_json
2. Extract "type" (required: npub|note|nprofile|nevent|naddr)
3. Extract "hex" (required, 64-char hex)
4. Convert hex to 32-byte array via nostr_hex_to_bytes()
5. Switch on type:
- "npub": nostr_build_uri_npub(bytes, output, sizeof output)
- "note": nostr_build_uri_note(bytes, output, sizeof output)
- "nprofile": nostr_build_uri_nprofile(bytes, relays, relay_count, output, sizeof output)
- "nevent": nostr_build_uri_nevent(bytes, relays, relay_count, NULL, -1, 0, output, sizeof output)
- "naddr": nostr_build_uri_naddr(identifier, bytes, kind, relays, relay_count, output, sizeof output)
6. Return { success, type, uri: output }
```
### `execute_nostr_relay_status()` in `tools.c`:
#### Library API Used
- [`nostr_build_uri_npub()`](../nostr_core_lib/nostr_core/nip021.h:66)
- [`nostr_build_uri_note()`](../nostr_core_lib/nostr_core/nip021.h:68)
- [`nostr_build_uri_nprofile()`](../nostr_core_lib/nostr_core/nip021.h:69)
- [`nostr_build_uri_nevent()`](../nostr_core_lib/nostr_core/nip021.h:71)
- [`nostr_build_uri_naddr()`](../nostr_core_lib/nostr_core/nip021.h:74)
```
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/tools.c`: Schema + executor + dispatch
---
### Tool 13: `nostr_decode` — Bech32 Entity Decoding (NIP-19/NIP-21)
#### Purpose
Decode `npub`, `note`, `nprofile`, `nevent`, `naddr` URIs back into hex values and metadata.
#### OpenAI Schema
```json
{
"name": "nostr_decode",
"description": "Decode a bech32 Nostr URI into its components: type, hex, relays, kind, identifier",
"parameters": {
"type": "object",
"properties": {
"uri": {
"type": "string",
"description": "Bech32 URI to decode, e.g. npub1..., note1..., nostr:npub1..."
}
},
"required": ["uri"]
}
}
```
### 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: `execute_nostr_decode()`
```
1. Parse args_json
2. Extract "uri" (required string)
3. Call nostr_parse_uri(uri, &result)
4. Switch on result.type:
- NPUB: convert result.data.pubkey to hex, return { type: "npub", pubkey: hex }
- NOTE: convert result.data.event_id to hex, return { type: "note", event_id: hex }
- NPROFILE: pubkey hex + relays array
- NEVENT: event_id hex + relays + optional author/kind
- NADDR: identifier + pubkey hex + kind + relays
5. Call nostr_uri_result_free(&result)
6. Return JSON
```
#### Library API Used
- [`nostr_parse_uri()`](../nostr_core_lib/nostr_core/nip021.h:63)
- [`nostr_uri_result_free()`](../nostr_core_lib/nostr_core/nip021.h:78)
#### Files Modified
- `src/tools.c`: Schema + executor + dispatch
---
### Tool 14: `nostr_dm_send` — Send DM as Tool Call
#### Purpose
Expose DM sending as a tool so the agent can proactively message people (notify admin, reach out to contacts).
#### OpenAI Schema
```json
{
"name": "nostr_dm_send",
"description": "Send an encrypted direct message to a Nostr user (NIP-04)",
"parameters": {
"type": "object",
"properties": {
"recipient_pubkey": {
"type": "string",
"description": "64-char hex pubkey of the recipient"
},
"message": {
"type": "string",
"description": "Plaintext message to send"
}
},
"required": ["recipient_pubkey", "message"]
}
}
```
#### Implementation: `execute_nostr_dm_send()`
```
1. Parse args_json
2. Extract "recipient_pubkey" (required, 64-char hex)
3. Extract "message" (required, non-empty string)
4. Call nostr_handler_send_dm(recipient_pubkey, message)
5. Return { success: true/false, recipient_pubkey, message_length }
```
#### Library API Used
- [`nostr_handler_send_dm()`](../src/nostr_handler.h:33) — already exists
#### Security Note
- Should be admin-tier only in practice. The tool itself doesn't enforce tier, but the agent's system prompt and skill definitions should restrict usage.
#### Files Modified
- `src/tools.c`: Schema + executor + dispatch
---
### Tool 15: `nostr_relay_info` — Relay Information Document (NIP-11)
#### Purpose
Inspect relay capabilities, supported NIPs, limitations, PoW requirements before publishing.
#### OpenAI Schema
```json
{
"name": "nostr_relay_info",
"description": "Fetch the NIP-11 relay information document for a relay URL",
"parameters": {
"type": "object",
"properties": {
"relay_url": {
"type": "string",
"description": "WebSocket relay URL, e.g. wss://relay.example.com"
}
},
"required": ["relay_url"]
}
}
```
#### Implementation
This requires a new helper in [`nostr_handler.c`](../src/nostr_handler.c) since NIP-11 fetching needs an HTTP GET request with `Accept: application/nostr+json` header.
##### New function: `nostr_handler_relay_info_json()`
```
1. Convert ws:// or wss:// URL to http:// or https://
2. Use libcurl to HTTP GET with header "Accept: application/nostr+json"
3. Return the raw JSON response string
```
##### `execute_nostr_relay_info()` in tools.c:
```
1. Parse args_json
2. Extract "relay_url" (required string, must start with ws:// or wss://)
3. Call nostr_handler_relay_info_json(relay_url)
4. Parse returned JSON
5. Return { success, relay_url, info: { name, description, pubkey, supported_nips, limitations, ... } }
```
#### Files Modified
- `src/nostr_handler.h`: Add `nostr_handler_relay_info_json()` declaration
- `src/nostr_handler.c`: Implement using libcurl (already linked)
- `src/tools.c`: Schema + executor + dispatch
---
## Tier 3 Tools — High Value, More Complex
### Tool 16: `nostr_dm_send_nip17` — Private DMs via Gift Wrap (NIP-17/NIP-59)
#### Purpose
Send modern, metadata-private DMs using the NIP-17 gift wrap protocol instead of legacy NIP-04.
#### OpenAI Schema
```json
{
"name": "nostr_dm_send_nip17",
"description": "Send a private direct message using NIP-17 gift wrap protocol for metadata privacy",
"parameters": {
"type": "object",
"properties": {
"recipient_pubkey": {
"type": "string",
"description": "64-char hex pubkey of the recipient"
},
"message": {
"type": "string",
"description": "Plaintext message to send"
},
"subject": {
"type": "string",
"description": "Optional conversation subject"
}
},
"required": ["recipient_pubkey", "message"]
}
}
```
#### Implementation
This is a multi-step crypto pipeline. The library handles the heavy lifting but we need to orchestrate:
##### New function: `nostr_handler_send_dm_nip17()` in nostr_handler.c
```
1. Query recipient's kind 10050 relay list (DM relays)
- Build filter: { kinds: [10050], authors: [recipient_pubkey], limit: 1 }
- Call nostr_relay_pool_query_sync()
- Extract relay URLs via nostr_nip17_extract_dm_relays()
- Fall back to our own relays if no 10050 found
2. Create kind 14 chat event:
- nostr_nip17_create_chat_event(message, &recipient_pubkey, 1, subject, NULL, NULL, our_pubkey)
3. Gift wrap and send:
- nostr_nip17_send_dm(chat_event, &recipient_pubkey, 1, our_private_key, gift_wraps, max_wraps, 0)
4. Publish each gift wrap to recipient's DM relays
5. Return success/failure + event metadata
```
##### `execute_nostr_dm_send_nip17()` in tools.c:
```
1. Parse args_json
2. Extract recipient_pubkey, message, optional subject
3. Call nostr_handler_send_dm_nip17(recipient_pubkey, message, subject)
4. Return { success, recipient_pubkey, protocol: "nip17" }
```
#### Library API Used
- [`nostr_nip17_create_chat_event()`](../nostr_core_lib/nostr_core/nip017.h:28)
- [`nostr_nip17_send_dm()`](../nostr_core_lib/nostr_core/nip017.h:100)
- [`nostr_nip17_extract_dm_relays()`](../nostr_core_lib/nostr_core/nip017.h)
- [`nostr_nip59_create_gift_wrap()`](../nostr_core_lib/nostr_core/nip059.h:50)
#### Files Modified
- `src/nostr_handler.h`: Add `nostr_handler_send_dm_nip17()` declaration
- `src/nostr_handler.c`: Implement the full NIP-17 send pipeline
- `src/tools.c`: Schema + executor + dispatch
---
### Tool 17: `nostr_encrypt` — NIP-44 Encryption
#### Purpose
Encrypt arbitrary content for a specific recipient using modern NIP-44 encryption.
#### OpenAI Schema
```json
{
"name": "nostr_encrypt",
"description": "Encrypt plaintext for a recipient using NIP-44 ChaCha20+HMAC encryption",
"parameters": {
"type": "object",
"properties": {
"recipient_pubkey": {
"type": "string",
"description": "64-char hex pubkey of the recipient"
},
"plaintext": {
"type": "string",
"description": "Text to encrypt"
}
},
"required": ["recipient_pubkey", "plaintext"]
}
}
```
#### Implementation: `execute_nostr_encrypt()`
```
1. Parse args_json
2. Extract recipient_pubkey (64-char hex), plaintext (non-empty string)
3. Convert recipient_pubkey hex to 32-byte array
4. Allocate output buffer (plaintext length * 2 + 256 for base64 overhead)
5. Call nostr_nip44_encrypt(our_private_key, recipient_pubkey_bytes, plaintext, output, output_size)
6. Return { success, recipient_pubkey, ciphertext: output }
```
#### Library API Used
- [`nostr_nip44_encrypt()`](../nostr_core_lib/nostr_core/nip044.h:28)
#### Security Note
- Admin-tier only. Uses the agent's private key for ECDH shared secret.
#### Files Modified
- `src/tools.c`: Schema + executor + dispatch
---
### Tool 18: `nostr_decrypt` — NIP-44 Decryption
#### Purpose
Decrypt NIP-44 encrypted content from a known sender.
#### OpenAI Schema
```json
{
"name": "nostr_decrypt",
"description": "Decrypt NIP-44 encrypted content from a sender",
"parameters": {
"type": "object",
"properties": {
"sender_pubkey": {
"type": "string",
"description": "64-char hex pubkey of the sender"
},
"ciphertext": {
"type": "string",
"description": "Base64-encoded NIP-44 encrypted payload"
}
},
"required": ["sender_pubkey", "ciphertext"]
}
}
```
#### Implementation: `execute_nostr_decrypt()`
```
1. Parse args_json
2. Extract sender_pubkey (64-char hex), ciphertext (non-empty string)
3. Convert sender_pubkey hex to 32-byte array
4. Allocate output buffer
5. Call nostr_nip44_decrypt(our_private_key, sender_pubkey_bytes, ciphertext, output, output_size)
6. Return { success, sender_pubkey, plaintext: output }
```
#### Library API Used
- [`nostr_nip44_decrypt()`](../nostr_core_lib/nostr_core/nip044.h:62)
#### Security Note
- Admin-tier only.
#### Files Modified
- `src/tools.c`: Schema + executor + dispatch
---
### Tool 19: `nostr_list_manage` — List Management (NIP-51)
#### Purpose
Manage mute lists (kind 10000), bookmarks (kind 10003), follow sets, relay sets. Lets the agent curate its social graph.
#### OpenAI Schema
```json
{
"name": "nostr_list_manage",
"description": "Add or remove items from a Nostr list: mute list, bookmarks, relay sets, etc. (NIP-51)",
"parameters": {
"type": "object",
"properties": {
"list_kind": {
"type": "integer",
"description": "Kind number of the list: 10000=mute, 10001=pins, 10003=bookmarks, 3=follows, 10002=relays"
},
"action": {
"type": "string",
"description": "Action: add or remove"
},
"items": {
"type": "array",
"items": {
"type": "array",
"items": { "type": "string" }
},
"description": "Array of tag tuples to add/remove, e.g. [[p, pubkey], [e, event_id]]"
}
},
"required": ["list_kind", "action", "items"]
}
}
```
#### Implementation
This is a read-modify-write pattern for replaceable events:
##### `execute_nostr_list_manage()`:
```
1. Parse args_json
2. Extract list_kind (integer), action ("add"|"remove"), items (array of tag arrays)
3. Query existing list:
- Build filter: { kinds: [list_kind], authors: [our_pubkey], limit: 1 }
- Call nostr_handler_query_json(filter, 8000)
4. Parse existing event tags (or start with empty array if none found)
5. If action == "add":
- For each item in items: append to tags if not already present
6. If action == "remove":
- For each item in items: remove matching tag from tags
7. Publish updated event:
- Call nostr_handler_publish_kind_event(list_kind, existing_content_or_empty, updated_tags, &result)
8. Return { success, list_kind, action, items_affected, event_id }
```
#### Notes
- Replaceable events (kind 10000-19999) automatically replace previous versions
- Kind 3 (follows) is also replaceable
- Content field may contain NIP-44 encrypted private items — preserve it unchanged
- Must deduplicate tags when adding
#### Files Modified
- `src/tools.c`: Schema + executor + 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
```mermaid
graph TD
A[nostr_nip05_lookup] --> B[nostr_encode]
B --> C[nostr_decode]
C --> D[nostr_dm_send]
D --> E[nostr_relay_info]
E --> F[nostr_encrypt]
F --> G[nostr_decrypt]
G --> H[nostr_dm_send_nip17]
H --> I[nostr_list_manage]
style A fill:#FFFFE0
style B fill:#FFFFE0
style C fill:#FFFFE0
style D fill:#FFFFE0
style E fill:#FFFFE0
style F fill:#FFD700
style G fill:#FFD700
style H fill:#FFD700
style I fill:#FFD700
```
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.
Yellow = Tier 2 (moderate), Gold = Tier 3 (complex).
### Recommended batching:
**Batch A — Pure computation, no new handler functions:**
1. `nostr_encode` — pure NIP-21 encoding
2. `nostr_decode` — pure NIP-21 decoding
3. `nostr_dm_send` — thin wrapper around existing `nostr_handler_send_dm()`
**Batch B — Network I/O tools:**
4. `nostr_nip05_lookup` — HTTP fetch via library
5. `nostr_relay_info` — HTTP fetch via new handler function + libcurl
**Batch C — Crypto tools:**
6. `nostr_encrypt` — NIP-44 encrypt
7. `nostr_decrypt` — NIP-44 decrypt
**Batch D — Complex orchestration:**
8. `nostr_dm_send_nip17` — multi-step NIP-17/59 pipeline, new handler function
9. `nostr_list_manage` — read-modify-write replaceable events
## Build & Validate
After all four tools are implemented:
After each batch:
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. Run `./increment_and_push.sh` with descriptive message
+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 19
#define DIDACTYL_VERSION "v0.0.19"
#define DIDACTYL_VERSION_PATCH 20
#define DIDACTYL_VERSION "v0.0.20"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"
+265
View File
@@ -12,6 +12,9 @@
#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;
@@ -1383,6 +1386,268 @@ char* nostr_handler_relay_status_json(void) {
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;
}
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(10050));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToArray(authors, cJSON_CreateString(recipient_pubkey_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,
5000);
cJSON_Delete(filter);
char* extracted_dm_relays[NIP17_MAX_RELAYS] = {0};
int extracted_count = 0;
if (events && event_count > 0 && events[0]) {
int extracted = nostr_nip17_extract_dm_relays(events[0], extracted_dm_relays, NIP17_MAX_RELAYS);
if (extracted > 0) {
extracted_count = extracted;
}
}
if (events) {
for (int i = 0; i < event_count; i++) {
cJSON_Delete(events[i]);
}
free(events);
}
const char* target_relays[NIP17_MAX_RELAYS] = {0};
int target_count = 0;
if (extracted_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) {
continue;
}
for (int j = 0; j < extracted_count; j++) {
if (extracted_dm_relays[j] && strcmp(g_cfg->relays[i], extracted_dm_relays[j]) == 0) {
target_relays[target_count++] = g_cfg->relays[i];
break;
}
}
}
}
if (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];
}
}
}
for (int i = 0; i < extracted_count; i++) {
free(extracted_dm_relays[i]);
}
if (target_count <= 0) {
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) {
return -1;
}
int any_sent = 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;
}
cJSON_Delete(gift_wraps[i]);
}
for (int i = 0; i < 5; i++) {
(void)nostr_relay_pool_poll(g_pool, 100);
}
return any_sent ? 0 : -1;
}
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;
+2
View File
@@ -44,6 +44,8 @@ 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);
char* nostr_handler_relay_info_json(const char* relay_url);
int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* message, const char* subject);
void nostr_handler_cleanup(void);
#endif
+893
View File
@@ -5,12 +5,14 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <limits.h>
#include <ctype.h>
#include <time.h>
#include "cjson/cJSON.h"
#include "nostr_handler.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
static char* json_error(const char* msg) {
cJSON* root = cJSON_CreateObject();
@@ -553,6 +555,56 @@ static cJSON* parse_tool_args_json(const char* args_json) {
return args;
}
static void free_string_array_heap(char** arr, int count) {
if (!arr) return;
for (int i = 0; i < count; i++) {
free(arr[i]);
}
free(arr);
}
static int tag_tuple_equal(cJSON* a, cJSON* b) {
if (!a || !b || !cJSON_IsArray(a) || !cJSON_IsArray(b)) return 0;
int an = cJSON_GetArraySize(a);
int bn = cJSON_GetArraySize(b);
if (an != bn) return 0;
for (int i = 0; i < an; i++) {
cJSON* ai = cJSON_GetArrayItem(a, i);
cJSON* bi = cJSON_GetArrayItem(b, i);
if (!ai || !bi || !cJSON_IsString(ai) || !cJSON_IsString(bi) ||
!ai->valuestring || !bi->valuestring || strcmp(ai->valuestring, bi->valuestring) != 0) {
return 0;
}
}
return 1;
}
static int tags_contains_tuple(cJSON* tags, cJSON* tuple) {
if (!tags || !tuple || !cJSON_IsArray(tags) || !cJSON_IsArray(tuple)) return 0;
int n = cJSON_GetArraySize(tags);
for (int i = 0; i < n; i++) {
cJSON* cur = cJSON_GetArrayItem(tags, i);
if (tag_tuple_equal(cur, tuple)) {
return 1;
}
}
return 0;
}
static int remove_matching_tag_tuples(cJSON* tags, cJSON* tuple) {
if (!tags || !tuple || !cJSON_IsArray(tags) || !cJSON_IsArray(tuple)) return 0;
int removed = 0;
for (int i = cJSON_GetArraySize(tags) - 1; i >= 0; i--) {
cJSON* cur = cJSON_GetArrayItem(tags, i);
if (tag_tuple_equal(cur, tuple)) {
cJSON_DeleteItemFromArray(tags, i);
removed++;
}
}
return removed;
}
char* tools_build_openai_schema_json(const tools_context_t* ctx) {
(void)ctx;
@@ -824,6 +876,262 @@ char* tools_build_openai_schema_json(const tools_context_t* ctx) {
cJSON_AddItemToObject(t10, "function", t10_fn);
cJSON_AddItemToArray(tools, t10);
cJSON* t11 = cJSON_CreateObject();
cJSON* t11_fn = cJSON_CreateObject();
cJSON* t11_params = cJSON_CreateObject();
cJSON* t11_props = cJSON_CreateObject();
cJSON* t11_required = cJSON_CreateArray();
cJSON_AddStringToObject(t11, "type", "function");
cJSON_AddStringToObject(t11_fn, "name", "nostr_nip05_lookup");
cJSON_AddStringToObject(t11_fn, "description", "Look up or verify a NIP-05 identifier (user@domain)");
cJSON_AddStringToObject(t11_params, "type", "object");
cJSON_AddItemToObject(t11_params, "properties", t11_props);
cJSON_AddItemToObject(t11_params, "required", t11_required);
cJSON* p_nip05_identifier = cJSON_CreateObject();
cJSON_AddStringToObject(p_nip05_identifier, "type", "string");
cJSON_AddItemToObject(t11_props, "identifier", p_nip05_identifier);
cJSON* p_nip05_pubkey = cJSON_CreateObject();
cJSON_AddStringToObject(p_nip05_pubkey, "type", "string");
cJSON_AddItemToObject(t11_props, "pubkey", p_nip05_pubkey);
cJSON_AddItemToArray(t11_required, cJSON_CreateString("identifier"));
cJSON_AddItemToObject(t11_fn, "parameters", t11_params);
cJSON_AddItemToObject(t11, "function", t11_fn);
cJSON_AddItemToArray(tools, t11);
cJSON* t12 = cJSON_CreateObject();
cJSON* t12_fn = cJSON_CreateObject();
cJSON* t12_params = cJSON_CreateObject();
cJSON* t12_props = cJSON_CreateObject();
cJSON* t12_required = cJSON_CreateArray();
cJSON_AddStringToObject(t12, "type", "function");
cJSON_AddStringToObject(t12_fn, "name", "nostr_encode");
cJSON_AddStringToObject(t12_fn, "description", "Encode a Nostr entity into nostr: URI (npub, note, nprofile, nevent, naddr)");
cJSON_AddStringToObject(t12_params, "type", "object");
cJSON_AddItemToObject(t12_params, "properties", t12_props);
cJSON_AddItemToObject(t12_params, "required", t12_required);
cJSON* p_encode_type = cJSON_CreateObject();
cJSON_AddStringToObject(p_encode_type, "type", "string");
cJSON_AddItemToObject(t12_props, "type", p_encode_type);
cJSON* p_encode_hex = cJSON_CreateObject();
cJSON_AddStringToObject(p_encode_hex, "type", "string");
cJSON_AddItemToObject(t12_props, "hex", p_encode_hex);
cJSON* p_encode_relays = cJSON_CreateObject();
cJSON_AddStringToObject(p_encode_relays, "type", "array");
cJSON* p_encode_relays_item = cJSON_CreateObject();
cJSON_AddStringToObject(p_encode_relays_item, "type", "string");
cJSON_AddItemToObject(p_encode_relays, "items", p_encode_relays_item);
cJSON_AddItemToObject(t12_props, "relays", p_encode_relays);
cJSON* p_encode_kind = cJSON_CreateObject();
cJSON_AddStringToObject(p_encode_kind, "type", "integer");
cJSON_AddItemToObject(t12_props, "kind", p_encode_kind);
cJSON* p_encode_identifier = cJSON_CreateObject();
cJSON_AddStringToObject(p_encode_identifier, "type", "string");
cJSON_AddItemToObject(t12_props, "identifier", p_encode_identifier);
cJSON_AddItemToArray(t12_required, cJSON_CreateString("type"));
cJSON_AddItemToArray(t12_required, cJSON_CreateString("hex"));
cJSON_AddItemToObject(t12_fn, "parameters", t12_params);
cJSON_AddItemToObject(t12, "function", t12_fn);
cJSON_AddItemToArray(tools, t12);
cJSON* t13 = cJSON_CreateObject();
cJSON* t13_fn = cJSON_CreateObject();
cJSON* t13_params = cJSON_CreateObject();
cJSON* t13_props = cJSON_CreateObject();
cJSON* t13_required = cJSON_CreateArray();
cJSON_AddStringToObject(t13, "type", "function");
cJSON_AddStringToObject(t13_fn, "name", "nostr_decode");
cJSON_AddStringToObject(t13_fn, "description", "Decode a Nostr bech32/nostr: URI into components");
cJSON_AddStringToObject(t13_params, "type", "object");
cJSON_AddItemToObject(t13_params, "properties", t13_props);
cJSON_AddItemToObject(t13_params, "required", t13_required);
cJSON* p_decode_uri = cJSON_CreateObject();
cJSON_AddStringToObject(p_decode_uri, "type", "string");
cJSON_AddItemToObject(t13_props, "uri", p_decode_uri);
cJSON_AddItemToArray(t13_required, cJSON_CreateString("uri"));
cJSON_AddItemToObject(t13_fn, "parameters", t13_params);
cJSON_AddItemToObject(t13, "function", t13_fn);
cJSON_AddItemToArray(tools, t13);
cJSON* t14 = cJSON_CreateObject();
cJSON* t14_fn = cJSON_CreateObject();
cJSON* t14_params = cJSON_CreateObject();
cJSON* t14_props = cJSON_CreateObject();
cJSON* t14_required = cJSON_CreateArray();
cJSON_AddStringToObject(t14, "type", "function");
cJSON_AddStringToObject(t14_fn, "name", "nostr_dm_send");
cJSON_AddStringToObject(t14_fn, "description", "Send a NIP-04 encrypted DM");
cJSON_AddStringToObject(t14_params, "type", "object");
cJSON_AddItemToObject(t14_params, "properties", t14_props);
cJSON_AddItemToObject(t14_params, "required", t14_required);
cJSON* p_dm_recipient = cJSON_CreateObject();
cJSON_AddStringToObject(p_dm_recipient, "type", "string");
cJSON_AddItemToObject(t14_props, "recipient_pubkey", p_dm_recipient);
cJSON* p_dm_message = cJSON_CreateObject();
cJSON_AddStringToObject(p_dm_message, "type", "string");
cJSON_AddItemToObject(t14_props, "message", p_dm_message);
cJSON_AddItemToArray(t14_required, cJSON_CreateString("recipient_pubkey"));
cJSON_AddItemToArray(t14_required, cJSON_CreateString("message"));
cJSON_AddItemToObject(t14_fn, "parameters", t14_params);
cJSON_AddItemToObject(t14, "function", t14_fn);
cJSON_AddItemToArray(tools, t14);
cJSON* t15 = cJSON_CreateObject();
cJSON* t15_fn = cJSON_CreateObject();
cJSON* t15_params = cJSON_CreateObject();
cJSON* t15_props = cJSON_CreateObject();
cJSON* t15_required = cJSON_CreateArray();
cJSON_AddStringToObject(t15, "type", "function");
cJSON_AddStringToObject(t15_fn, "name", "nostr_relay_info");
cJSON_AddStringToObject(t15_fn, "description", "Fetch NIP-11 relay information document");
cJSON_AddStringToObject(t15_params, "type", "object");
cJSON_AddItemToObject(t15_params, "properties", t15_props);
cJSON_AddItemToObject(t15_params, "required", t15_required);
cJSON* p_relay_info_url = cJSON_CreateObject();
cJSON_AddStringToObject(p_relay_info_url, "type", "string");
cJSON_AddItemToObject(t15_props, "relay_url", p_relay_info_url);
cJSON_AddItemToArray(t15_required, cJSON_CreateString("relay_url"));
cJSON_AddItemToObject(t15_fn, "parameters", t15_params);
cJSON_AddItemToObject(t15, "function", t15_fn);
cJSON_AddItemToArray(tools, t15);
cJSON* t16 = cJSON_CreateObject();
cJSON* t16_fn = cJSON_CreateObject();
cJSON* t16_params = cJSON_CreateObject();
cJSON* t16_props = cJSON_CreateObject();
cJSON* t16_required = cJSON_CreateArray();
cJSON_AddStringToObject(t16, "type", "function");
cJSON_AddStringToObject(t16_fn, "name", "nostr_encrypt");
cJSON_AddStringToObject(t16_fn, "description", "Encrypt plaintext using NIP-44 for a recipient");
cJSON_AddStringToObject(t16_params, "type", "object");
cJSON_AddItemToObject(t16_params, "properties", t16_props);
cJSON_AddItemToObject(t16_params, "required", t16_required);
cJSON* p_encrypt_recipient = cJSON_CreateObject();
cJSON_AddStringToObject(p_encrypt_recipient, "type", "string");
cJSON_AddItemToObject(t16_props, "recipient_pubkey", p_encrypt_recipient);
cJSON* p_encrypt_plaintext = cJSON_CreateObject();
cJSON_AddStringToObject(p_encrypt_plaintext, "type", "string");
cJSON_AddItemToObject(t16_props, "plaintext", p_encrypt_plaintext);
cJSON_AddItemToArray(t16_required, cJSON_CreateString("recipient_pubkey"));
cJSON_AddItemToArray(t16_required, cJSON_CreateString("plaintext"));
cJSON_AddItemToObject(t16_fn, "parameters", t16_params);
cJSON_AddItemToObject(t16, "function", t16_fn);
cJSON_AddItemToArray(tools, t16);
cJSON* t17 = cJSON_CreateObject();
cJSON* t17_fn = cJSON_CreateObject();
cJSON* t17_params = cJSON_CreateObject();
cJSON* t17_props = cJSON_CreateObject();
cJSON* t17_required = cJSON_CreateArray();
cJSON_AddStringToObject(t17, "type", "function");
cJSON_AddStringToObject(t17_fn, "name", "nostr_decrypt");
cJSON_AddStringToObject(t17_fn, "description", "Decrypt NIP-44 ciphertext from a sender");
cJSON_AddStringToObject(t17_params, "type", "object");
cJSON_AddItemToObject(t17_params, "properties", t17_props);
cJSON_AddItemToObject(t17_params, "required", t17_required);
cJSON* p_decrypt_sender = cJSON_CreateObject();
cJSON_AddStringToObject(p_decrypt_sender, "type", "string");
cJSON_AddItemToObject(t17_props, "sender_pubkey", p_decrypt_sender);
cJSON* p_decrypt_ciphertext = cJSON_CreateObject();
cJSON_AddStringToObject(p_decrypt_ciphertext, "type", "string");
cJSON_AddItemToObject(t17_props, "ciphertext", p_decrypt_ciphertext);
cJSON_AddItemToArray(t17_required, cJSON_CreateString("sender_pubkey"));
cJSON_AddItemToArray(t17_required, cJSON_CreateString("ciphertext"));
cJSON_AddItemToObject(t17_fn, "parameters", t17_params);
cJSON_AddItemToObject(t17, "function", t17_fn);
cJSON_AddItemToArray(tools, t17);
cJSON* t18 = cJSON_CreateObject();
cJSON* t18_fn = cJSON_CreateObject();
cJSON* t18_params = cJSON_CreateObject();
cJSON* t18_props = cJSON_CreateObject();
cJSON* t18_required = cJSON_CreateArray();
cJSON_AddStringToObject(t18, "type", "function");
cJSON_AddStringToObject(t18_fn, "name", "nostr_dm_send_nip17");
cJSON_AddStringToObject(t18_fn, "description", "Send a private DM using NIP-17 gift wrap protocol");
cJSON_AddStringToObject(t18_params, "type", "object");
cJSON_AddItemToObject(t18_params, "properties", t18_props);
cJSON_AddItemToObject(t18_params, "required", t18_required);
cJSON* p_dm17_recipient = cJSON_CreateObject();
cJSON_AddStringToObject(p_dm17_recipient, "type", "string");
cJSON_AddItemToObject(t18_props, "recipient_pubkey", p_dm17_recipient);
cJSON* p_dm17_message = cJSON_CreateObject();
cJSON_AddStringToObject(p_dm17_message, "type", "string");
cJSON_AddItemToObject(t18_props, "message", p_dm17_message);
cJSON* p_dm17_subject = cJSON_CreateObject();
cJSON_AddStringToObject(p_dm17_subject, "type", "string");
cJSON_AddItemToObject(t18_props, "subject", p_dm17_subject);
cJSON_AddItemToArray(t18_required, cJSON_CreateString("recipient_pubkey"));
cJSON_AddItemToArray(t18_required, cJSON_CreateString("message"));
cJSON_AddItemToObject(t18_fn, "parameters", t18_params);
cJSON_AddItemToObject(t18, "function", t18_fn);
cJSON_AddItemToArray(tools, t18);
cJSON* t19 = cJSON_CreateObject();
cJSON* t19_fn = cJSON_CreateObject();
cJSON* t19_params = cJSON_CreateObject();
cJSON* t19_props = cJSON_CreateObject();
cJSON* t19_required = cJSON_CreateArray();
cJSON_AddStringToObject(t19, "type", "function");
cJSON_AddStringToObject(t19_fn, "name", "nostr_list_manage");
cJSON_AddStringToObject(t19_fn, "description", "Add/remove tag tuples in replaceable list events (NIP-51 style)");
cJSON_AddStringToObject(t19_params, "type", "object");
cJSON_AddItemToObject(t19_params, "properties", t19_props);
cJSON_AddItemToObject(t19_params, "required", t19_required);
cJSON* p_list_kind = cJSON_CreateObject();
cJSON_AddStringToObject(p_list_kind, "type", "integer");
cJSON_AddItemToObject(t19_props, "list_kind", p_list_kind);
cJSON* p_list_action = cJSON_CreateObject();
cJSON_AddStringToObject(p_list_action, "type", "string");
cJSON_AddItemToObject(t19_props, "action", p_list_action);
cJSON* p_list_items = cJSON_CreateObject();
cJSON_AddStringToObject(p_list_items, "type", "array");
cJSON* p_list_items_item = cJSON_CreateObject();
cJSON_AddStringToObject(p_list_items_item, "type", "array");
cJSON* p_list_items_item_item = cJSON_CreateObject();
cJSON_AddStringToObject(p_list_items_item_item, "type", "string");
cJSON_AddItemToObject(p_list_items_item, "items", p_list_items_item_item);
cJSON_AddItemToObject(p_list_items, "items", p_list_items_item);
cJSON_AddItemToObject(t19_props, "items", p_list_items);
cJSON_AddItemToArray(t19_required, cJSON_CreateString("list_kind"));
cJSON_AddItemToArray(t19_required, cJSON_CreateString("action"));
cJSON_AddItemToArray(t19_required, cJSON_CreateString("items"));
cJSON_AddItemToObject(t19_fn, "parameters", t19_params);
cJSON_AddItemToObject(t19, "function", t19_fn);
cJSON_AddItemToArray(tools, t19);
char* out = cJSON_PrintUnformatted(tools);
cJSON_Delete(tools);
return out;
@@ -1412,6 +1720,564 @@ static char* execute_nostr_relay_status(const char* args_json) {
return json;
}
static char* execute_nostr_nip05_lookup(const char* args_json) {
cJSON* args = parse_tool_args_json(args_json);
if (!args) return json_error("invalid arguments JSON");
cJSON* identifier = cJSON_GetObjectItemCaseSensitive(args, "identifier");
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey");
if (!identifier || !cJSON_IsString(identifier) || !identifier->valuestring || strchr(identifier->valuestring, '@') == NULL) {
cJSON_Delete(args);
return json_error("nostr_nip05_lookup requires identifier in user@domain format");
}
if (pubkey && (!cJSON_IsString(pubkey) || !pubkey->valuestring || !is_hex_string_len(pubkey->valuestring, 64U))) {
cJSON_Delete(args);
return json_error("nostr_nip05_lookup pubkey must be a 64-char hex string when provided");
}
char found_pubkey[65] = {0};
char** relays = NULL;
int relay_count = 0;
int rc = 0;
if (pubkey) {
rc = nostr_nip05_verify(identifier->valuestring,
pubkey->valuestring,
&relays,
&relay_count,
10);
} else {
rc = nostr_nip05_lookup(identifier->valuestring,
found_pubkey,
&relays,
&relay_count,
10);
}
cJSON* out = cJSON_CreateObject();
if (!out) {
free_string_array_heap(relays, relay_count);
cJSON_Delete(args);
return NULL;
}
cJSON_AddBoolToObject(out, "success", rc == NOSTR_SUCCESS ? 1 : 0);
cJSON_AddStringToObject(out, "identifier", identifier->valuestring);
if (pubkey) {
cJSON_AddBoolToObject(out, "verified", rc == NOSTR_SUCCESS ? 1 : 0);
cJSON_AddStringToObject(out, "pubkey", pubkey->valuestring);
} else if (rc == NOSTR_SUCCESS) {
cJSON_AddStringToObject(out, "pubkey", found_pubkey);
}
cJSON* relay_arr = cJSON_CreateArray();
if (!relay_arr) {
cJSON_Delete(out);
free_string_array_heap(relays, relay_count);
cJSON_Delete(args);
return NULL;
}
for (int i = 0; i < relay_count; i++) {
cJSON_AddItemToArray(relay_arr, cJSON_CreateString(relays[i] ? relays[i] : ""));
}
cJSON_AddItemToObject(out, "relays", relay_arr);
if (rc != NOSTR_SUCCESS) {
cJSON_AddNumberToObject(out, "error_code", rc);
}
free_string_array_heap(relays, relay_count);
cJSON_Delete(args);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
static char* execute_nostr_encode(const char* args_json) {
cJSON* args = parse_tool_args_json(args_json);
if (!args) return json_error("invalid arguments JSON");
cJSON* type = cJSON_GetObjectItemCaseSensitive(args, "type");
cJSON* hex = cJSON_GetObjectItemCaseSensitive(args, "hex");
if (!type || !cJSON_IsString(type) || !type->valuestring ||
!hex || !cJSON_IsString(hex) || !hex->valuestring || !is_hex_string_len(hex->valuestring, 64U)) {
cJSON_Delete(args);
return json_error("nostr_encode requires type string and hex 64-char string");
}
char type_name[32];
snprintf(type_name, sizeof(type_name), "%s", type->valuestring);
unsigned char key_bytes[32];
if (nostr_hex_to_bytes(hex->valuestring, key_bytes, sizeof(key_bytes)) != 0) {
cJSON_Delete(args);
return json_error("nostr_encode invalid hex");
}
char bech32[256] = {0};
int rc = NOSTR_ERROR_INVALID_INPUT;
if (strcmp(type_name, "npub") == 0) {
rc = nostr_key_to_bech32(key_bytes, "npub", bech32);
} else if (strcmp(type_name, "nsec") == 0) {
rc = nostr_key_to_bech32(key_bytes, "nsec", bech32);
} else if (strcmp(type_name, "note") == 0) {
rc = nostr_key_to_bech32(key_bytes, "note", bech32);
} else {
cJSON_Delete(args);
return json_error("nostr_encode currently supports npub, nsec, note");
}
cJSON_Delete(args);
if (rc != NOSTR_SUCCESS) {
return json_error("nostr_encode failed");
}
char uri[320] = {0};
snprintf(uri, sizeof(uri), "nostr:%s", bech32);
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "type", type_name);
cJSON_AddStringToObject(out, "uri", uri);
cJSON_AddBoolToObject(out, "limited_support", 1);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
static char* execute_nostr_decode(const char* args_json) {
cJSON* args = parse_tool_args_json(args_json);
if (!args) return json_error("invalid arguments JSON");
cJSON* uri_in = cJSON_GetObjectItemCaseSensitive(args, "uri");
if (!uri_in || !cJSON_IsString(uri_in) || !uri_in->valuestring || uri_in->valuestring[0] == '\0') {
cJSON_Delete(args);
return json_error("nostr_decode requires uri string");
}
const char* in = uri_in->valuestring;
if (strncmp(in, "nostr:", 6) == 0) {
in += 6;
}
unsigned char key[32];
char key_hex[65] = {0};
const char* type = NULL;
int rc = NOSTR_ERROR_INVALID_INPUT;
if (strncmp(in, "npub1", 5) == 0) {
type = "npub";
rc = nostr_decode_npub(in, key);
} else if (strncmp(in, "nsec1", 5) == 0) {
type = "nsec";
rc = nostr_decode_nsec(in, key);
} else {
cJSON_Delete(args);
return json_error("nostr_decode currently supports npub and nsec");
}
cJSON_Delete(args);
if (rc != NOSTR_SUCCESS) {
return json_error("nostr_decode failed");
}
nostr_bytes_to_hex(key, 32, key_hex);
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "type", type);
if (strcmp(type, "npub") == 0) {
cJSON_AddStringToObject(out, "pubkey", key_hex);
} else {
cJSON_AddStringToObject(out, "private_key", key_hex);
}
cJSON_AddBoolToObject(out, "limited_support", 1);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
static char* execute_nostr_dm_send(const char* args_json) {
cJSON* args = parse_tool_args_json(args_json);
if (!args) return json_error("invalid arguments JSON");
cJSON* recipient = cJSON_GetObjectItemCaseSensitive(args, "recipient_pubkey");
cJSON* message = cJSON_GetObjectItemCaseSensitive(args, "message");
if (!recipient || !cJSON_IsString(recipient) || !recipient->valuestring || !is_hex_string_len(recipient->valuestring, 64U) ||
!message || !cJSON_IsString(message) || !message->valuestring || message->valuestring[0] == '\0') {
cJSON_Delete(args);
return json_error("nostr_dm_send requires recipient_pubkey hex and non-empty message");
}
int rc = nostr_handler_send_dm(recipient->valuestring, message->valuestring);
cJSON* out = cJSON_CreateObject();
if (!out) {
cJSON_Delete(args);
return NULL;
}
cJSON_AddBoolToObject(out, "success", rc == 0 ? 1 : 0);
cJSON_AddStringToObject(out, "recipient_pubkey", recipient->valuestring);
cJSON_AddNumberToObject(out, "message_length", (double)strlen(message->valuestring));
cJSON_Delete(args);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
static char* execute_nostr_relay_info(const char* args_json) {
cJSON* args = parse_tool_args_json(args_json);
if (!args) return json_error("invalid arguments JSON");
cJSON* relay_url = cJSON_GetObjectItemCaseSensitive(args, "relay_url");
if (!relay_url || !cJSON_IsString(relay_url) || !relay_url->valuestring || relay_url->valuestring[0] == '\0') {
cJSON_Delete(args);
return json_error("nostr_relay_info requires relay_url");
}
char* info_json = nostr_handler_relay_info_json(relay_url->valuestring);
cJSON_Delete(args);
if (!info_json) {
return json_error("nostr_relay_info failed");
}
cJSON* info = cJSON_Parse(info_json);
free(info_json);
if (!info || !cJSON_IsObject(info)) {
cJSON_Delete(info);
return json_error("nostr_relay_info returned invalid JSON");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
cJSON_Delete(info);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddItemToObject(out, "info", info);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
static char* execute_nostr_encrypt(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error("tool context unavailable");
cJSON* args = parse_tool_args_json(args_json);
if (!args) return json_error("invalid arguments JSON");
cJSON* recipient = cJSON_GetObjectItemCaseSensitive(args, "recipient_pubkey");
cJSON* plaintext = cJSON_GetObjectItemCaseSensitive(args, "plaintext");
if (!recipient || !cJSON_IsString(recipient) || !recipient->valuestring || !is_hex_string_len(recipient->valuestring, 64U) ||
!plaintext || !cJSON_IsString(plaintext) || !plaintext->valuestring || plaintext->valuestring[0] == '\0') {
cJSON_Delete(args);
return json_error("nostr_encrypt requires recipient_pubkey hex and plaintext");
}
unsigned char recipient_pubkey[32];
if (nostr_hex_to_bytes(recipient->valuestring, recipient_pubkey, sizeof(recipient_pubkey)) != 0) {
cJSON_Delete(args);
return json_error("nostr_encrypt invalid recipient_pubkey");
}
size_t out_cap = (strlen(plaintext->valuestring) * 4U) + 1024U;
char* ciphertext = (char*)malloc(out_cap);
if (!ciphertext) {
cJSON_Delete(args);
return json_error("allocation failure");
}
int rc = nostr_nip44_encrypt(ctx->cfg->keys.private_key,
recipient_pubkey,
plaintext->valuestring,
ciphertext,
out_cap);
if (rc != NOSTR_SUCCESS) {
free(ciphertext);
cJSON_Delete(args);
return json_error("nostr_encrypt failed");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
free(ciphertext);
cJSON_Delete(args);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "recipient_pubkey", recipient->valuestring);
cJSON_AddStringToObject(out, "ciphertext", ciphertext);
free(ciphertext);
cJSON_Delete(args);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
static char* execute_nostr_decrypt(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error("tool context unavailable");
cJSON* args = parse_tool_args_json(args_json);
if (!args) return json_error("invalid arguments JSON");
cJSON* sender = cJSON_GetObjectItemCaseSensitive(args, "sender_pubkey");
cJSON* ciphertext = cJSON_GetObjectItemCaseSensitive(args, "ciphertext");
if (!sender || !cJSON_IsString(sender) || !sender->valuestring || !is_hex_string_len(sender->valuestring, 64U) ||
!ciphertext || !cJSON_IsString(ciphertext) || !ciphertext->valuestring || ciphertext->valuestring[0] == '\0') {
cJSON_Delete(args);
return json_error("nostr_decrypt requires sender_pubkey hex and ciphertext");
}
unsigned char sender_pubkey[32];
if (nostr_hex_to_bytes(sender->valuestring, sender_pubkey, sizeof(sender_pubkey)) != 0) {
cJSON_Delete(args);
return json_error("nostr_decrypt invalid sender_pubkey");
}
size_t out_cap = strlen(ciphertext->valuestring) + 1024U;
char* plaintext = (char*)malloc(out_cap);
if (!plaintext) {
cJSON_Delete(args);
return json_error("allocation failure");
}
int rc = nostr_nip44_decrypt(ctx->cfg->keys.private_key,
sender_pubkey,
ciphertext->valuestring,
plaintext,
out_cap);
if (rc != NOSTR_SUCCESS) {
free(plaintext);
cJSON_Delete(args);
return json_error("nostr_decrypt failed");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
free(plaintext);
cJSON_Delete(args);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "sender_pubkey", sender->valuestring);
cJSON_AddStringToObject(out, "plaintext", plaintext);
free(plaintext);
cJSON_Delete(args);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
static char* execute_nostr_dm_send_nip17(const char* args_json) {
cJSON* args = parse_tool_args_json(args_json);
if (!args) return json_error("invalid arguments JSON");
cJSON* recipient = cJSON_GetObjectItemCaseSensitive(args, "recipient_pubkey");
cJSON* message = cJSON_GetObjectItemCaseSensitive(args, "message");
cJSON* subject = cJSON_GetObjectItemCaseSensitive(args, "subject");
if (!recipient || !cJSON_IsString(recipient) || !recipient->valuestring || !is_hex_string_len(recipient->valuestring, 64U) ||
!message || !cJSON_IsString(message) || !message->valuestring || message->valuestring[0] == '\0') {
cJSON_Delete(args);
return json_error("nostr_dm_send_nip17 requires recipient_pubkey hex and non-empty message");
}
if (subject && !cJSON_IsString(subject)) {
cJSON_Delete(args);
return json_error("nostr_dm_send_nip17 subject must be a string when provided");
}
int rc = nostr_handler_send_dm_nip17(recipient->valuestring,
message->valuestring,
(subject && subject->valuestring) ? subject->valuestring : NULL);
cJSON* out = cJSON_CreateObject();
if (!out) {
cJSON_Delete(args);
return NULL;
}
cJSON_AddBoolToObject(out, "success", rc == 0 ? 1 : 0);
cJSON_AddStringToObject(out, "recipient_pubkey", recipient->valuestring);
cJSON_AddStringToObject(out, "protocol", "nip17");
cJSON_AddNumberToObject(out, "message_length", (double)strlen(message->valuestring));
cJSON_Delete(args);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
static char* execute_nostr_list_manage(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error("tool context unavailable");
cJSON* args = parse_tool_args_json(args_json);
if (!args) return json_error("invalid arguments JSON");
cJSON* list_kind = cJSON_GetObjectItemCaseSensitive(args, "list_kind");
cJSON* action = cJSON_GetObjectItemCaseSensitive(args, "action");
cJSON* items = cJSON_GetObjectItemCaseSensitive(args, "items");
if (!list_kind || !cJSON_IsNumber(list_kind) ||
!action || !cJSON_IsString(action) || !action->valuestring ||
!items || !cJSON_IsArray(items)) {
cJSON_Delete(args);
return json_error("nostr_list_manage requires list_kind, action, and items");
}
if (strcmp(action->valuestring, "add") != 0 && strcmp(action->valuestring, "remove") != 0) {
cJSON_Delete(args);
return json_error("nostr_list_manage action must be add or remove");
}
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 list query filter");
}
cJSON_AddItemToArray(kinds, cJSON_CreateNumber((int)list_kind->valuedouble));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToArray(authors, cJSON_CreateString(ctx->cfg->keys.public_key_hex));
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddNumberToObject(filter, "limit", 1);
char* events_json = nostr_handler_query_json(filter, 8000);
cJSON_Delete(filter);
cJSON* events = events_json ? cJSON_Parse(events_json) : NULL;
free(events_json);
char* list_content = strdup("");
if (!list_content) {
cJSON_Delete(events);
cJSON_Delete(args);
return json_error("allocation failure");
}
cJSON* updated_tags = cJSON_CreateArray();
if (!updated_tags) {
free(list_content);
cJSON_Delete(events);
cJSON_Delete(args);
return json_error("failed to create list tags");
}
if (events && cJSON_IsArray(events) && cJSON_GetArraySize(events) > 0) {
cJSON* ev0 = cJSON_GetArrayItem(events, 0);
if (ev0 && cJSON_IsObject(ev0)) {
cJSON* content = cJSON_GetObjectItemCaseSensitive(ev0, "content");
if (content && cJSON_IsString(content) && content->valuestring) {
free(list_content);
list_content = strdup(content->valuestring);
if (!list_content) {
cJSON_Delete(updated_tags);
cJSON_Delete(events);
cJSON_Delete(args);
return json_error("allocation failure");
}
}
cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev0, "tags");
if (tags && cJSON_IsArray(tags)) {
cJSON_Delete(updated_tags);
updated_tags = cJSON_Duplicate(tags, 1);
if (!updated_tags) {
free(list_content);
cJSON_Delete(events);
cJSON_Delete(args);
return json_error("failed to duplicate existing list tags");
}
}
}
}
int items_affected = 0;
int item_count = cJSON_GetArraySize(items);
for (int i = 0; i < item_count; i++) {
cJSON* tuple = cJSON_GetArrayItem(items, i);
if (!tuple || !cJSON_IsArray(tuple) || cJSON_GetArraySize(tuple) <= 0) {
continue;
}
int tuple_ok = 1;
int tuple_size = cJSON_GetArraySize(tuple);
for (int j = 0; j < tuple_size; j++) {
cJSON* part = cJSON_GetArrayItem(tuple, j);
if (!part || !cJSON_IsString(part) || !part->valuestring) {
tuple_ok = 0;
break;
}
}
if (!tuple_ok) {
continue;
}
if (strcmp(action->valuestring, "add") == 0) {
if (!tags_contains_tuple(updated_tags, tuple)) {
cJSON* dup = cJSON_Duplicate(tuple, 1);
if (!dup) {
free(list_content);
cJSON_Delete(updated_tags);
cJSON_Delete(events);
cJSON_Delete(args);
return json_error("failed to duplicate list item");
}
cJSON_AddItemToArray(updated_tags, dup);
items_affected++;
}
} else {
items_affected += remove_matching_tag_tuples(updated_tags, tuple);
}
}
nostr_publish_result_t publish_result;
memset(&publish_result, 0, sizeof(publish_result));
int rc = nostr_handler_publish_kind_event((int)list_kind->valuedouble,
list_content,
updated_tags,
&publish_result);
free(list_content);
cJSON_Delete(updated_tags);
cJSON_Delete(events);
cJSON_Delete(args);
if (rc != 0) {
nostr_handler_publish_result_free(&publish_result);
return json_error("nostr_list_manage 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_AddNumberToObject(out, "list_kind", publish_result.kind);
cJSON_AddStringToObject(out, "action", action->valuestring);
cJSON_AddNumberToObject(out, "items_affected", items_affected);
cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
nostr_handler_publish_result_free(&publish_result);
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");
@@ -1636,6 +2502,33 @@ char* tools_execute(tools_context_t* ctx, const char* tool_name, const char* arg
if (strcmp(tool_name, "nostr_relay_status") == 0) {
return execute_nostr_relay_status(args_json);
}
if (strcmp(tool_name, "nostr_nip05_lookup") == 0) {
return execute_nostr_nip05_lookup(args_json);
}
if (strcmp(tool_name, "nostr_encode") == 0) {
return execute_nostr_encode(args_json);
}
if (strcmp(tool_name, "nostr_decode") == 0) {
return execute_nostr_decode(args_json);
}
if (strcmp(tool_name, "nostr_dm_send") == 0) {
return execute_nostr_dm_send(args_json);
}
if (strcmp(tool_name, "nostr_relay_info") == 0) {
return execute_nostr_relay_info(args_json);
}
if (strcmp(tool_name, "nostr_encrypt") == 0) {
return execute_nostr_encrypt(ctx, args_json);
}
if (strcmp(tool_name, "nostr_decrypt") == 0) {
return execute_nostr_decrypt(ctx, args_json);
}
if (strcmp(tool_name, "nostr_dm_send_nip17") == 0) {
return execute_nostr_dm_send_nip17(args_json);
}
if (strcmp(tool_name, "nostr_list_manage") == 0) {
return execute_nostr_list_manage(ctx, args_json);
}
if (strcmp(tool_name, "nostr_query") == 0) {
return execute_nostr_query(args_json);
}