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:
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user