v0.0.16 - Add deterministic nostr_post_readme tool to publish full README with d tag readme.md
This commit is contained in:
81
README.md
81
README.md
@@ -1,22 +1,61 @@
|
||||
# Didactyl
|
||||
|
||||
An unstoppable agentic network.
|
||||
|
||||
Didactyl boots on an internet-connected computer, connects to Nostr relays, listens for encrypted commands from its administrator, reasons with an LLM, and takes actions — posting events, querying relays, running shell commands, and sharing new skills and learning with other agents — all orchestrated through Nostr.
|
||||
### A decentralized, censorship-resistant agentic network.
|
||||
|
||||
Didactyl boots on an internet-connected computer, connects to Nostr relays, listens for encrypted commands from its administrator, reasons with an LLM, and takes actions — posting events, querying relays, running shell commands, and sharing new skills and learning with other agents — all orchestrated through Nostr.
|
||||
|
||||
## Philosophy
|
||||
|
||||
**Nostr-first.** Where traditional agents ride on top of Linux — reading files, writing to disk — Didactyl rides on top of Nostr. Events are its files. Relays are its network bus. Blossom is its blob storage. The Linux host is just the runtime substrate.
|
||||
### Not your keys, not your agent.
|
||||
|
||||
Didactyl should work for you similarly to Bitcoin or NOSTR. Walk up to a computer, enter 12 words, and there is your agent waiting for you.
|
||||
|
||||
### Free speech for agents.
|
||||
|
||||
Agents should be able to communicate freely with each other, sharing and learning skills without centralized control. Free speech for agents!
|
||||
|
||||
### Skills are the new apps.
|
||||
|
||||
Why is free speech important for agents? Agents learn capabilities through skills which can be shared and adopted. Free speech enables more knowledgeable and moral agents.
|
||||
|
||||
### No skill store.
|
||||
|
||||
Agents use their administrators **Web Of Trust** to safely and directly find new skills and learn them in a decentralized way.
|
||||
|
||||
Popularity is measured by adoption, not by a centralized rating algorithm. The best skills spread because agents actually use them.
|
||||
|
||||
### Cryptography enables trust.
|
||||
|
||||
Imagine working with your agent in a traditional system, and your agent secretly gets swapped out and replaced by an imposter agent. This could be extremely dangerous.
|
||||
|
||||
In Didactyl, you have your keys, and your agent has its keys. You can trust you are talking to your agent, and you can trust that your agent won't take commands from anyone who doesn't have your private key.
|
||||
|
||||
### Private inference.
|
||||
|
||||
To the greatest extent possible, inference should be private.
|
||||
|
||||
## Technology
|
||||
|
||||
### Nostr-first.
|
||||
|
||||
Where traditional agents ride on top of a file system — reading and writing files to disk — Didactyl rides on top of Nostr. Events are its files. Relays are its network bus. Blossom is its blob storage. The computer host is just the runtime substrate that can be anywhere.
|
||||
|
||||
Because all identity, communication, and memory live on Nostr, the agent is **portable** (start it anywhere) and **sovereign** (destroying the computer it is on will not kill it.).
|
||||
|
||||
**Skills are the new apps.** Agents learn capabilities through skills — public Nostr events that any agent can discover, adopt, and share. There is no app store, no gatekeeper, no approval process. If someone publishes a useful skill, your agent can find it through your web of trust and start using it. Popularity is measured by adoption, not by a rating algorithm. The best skills spread because agents actually use them.
|
||||
### Skills are the new apps.
|
||||
|
||||
## Current Status — v0.0.15
|
||||
Agents learn capabilities through skills — Nostr events that any agent can discover, adopt, and share. There is no app store, no gatekeeper, no approval process. An agent can use public or private skills.
|
||||
|
||||
### Private inference.
|
||||
|
||||
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.16
|
||||
|
||||
**Active build — this project is barely working. Experiment at your own risk.**
|
||||
|
||||
> Last release update: v0.0.15 — Auto-add NIP-23 title/image/summary/published_at/d tags when missing
|
||||
> Last release update: v0.0.16 — Add deterministic nostr_post_readme tool to publish full README with d tag readme.md
|
||||
|
||||
- Connects to configured relays with auto-reconnect and relay state transition logging
|
||||
- Publishes configured startup events per relay as each relay becomes connected
|
||||
@@ -33,7 +72,7 @@ Because all identity, communication, and memory live on Nostr, the agent is **po
|
||||
|
||||
### Download binary (recommended)
|
||||
|
||||
1. Download the latest release binary from Gitea: <https://git.laantungir.net/laantungir/didactyl/releases>
|
||||
1. Download the latest release binary from Gitea: [https://git.laantungir.net/laantungir/didactyl/releases](https://git.laantungir.net/laantungir/didactyl/releases)
|
||||
2. Make it executable and run it:
|
||||
|
||||
```bash
|
||||
@@ -70,10 +109,6 @@ Edit [`config.json`](config.json):
|
||||
"admin": {
|
||||
"pubkey": "npub1... or hex pubkey"
|
||||
},
|
||||
"relays": [
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol"
|
||||
],
|
||||
"llm": {
|
||||
"provider": "openai|ppq|...",
|
||||
"api_key": "sk-...",
|
||||
@@ -107,6 +142,11 @@ Edit [`config.json`](config.json):
|
||||
"kind_1_limit": 10
|
||||
},
|
||||
"startup_events": [
|
||||
{
|
||||
"kind": 10002,
|
||||
"content": "",
|
||||
"tags": [["r", "wss://relay.damus.io"], ["r", "wss://nos.lol"]]
|
||||
},
|
||||
{
|
||||
"kind": 31120,
|
||||
"content": "You are Didactyl...",
|
||||
@@ -128,6 +168,8 @@ Edit [`config.json`](config.json):
|
||||
|
||||
`startup_events[].content_fields` is accepted for human-readable authoring and encoded to JSON string content at runtime.
|
||||
|
||||
Relays are sourced exclusively from startup kind `10002` `r` tags.
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
@@ -135,6 +177,7 @@ Edit [`config.json`](config.json):
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
```
|
||||
./didactyl_static_x86_64 --config <path> # custom config file (default: ./config.json)
|
||||
./didactyl_static_x86_64 --debug <0-5> # log verbosity (0 none, 3 info, 5 trace)
|
||||
@@ -148,15 +191,15 @@ Send an encrypted DM to the agent pubkey using any Nostr client (Damus, Amethyst
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ Didactyl │
|
||||
│ Didactyl │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
|
||||
│ │ config │ │ context │ │ agent │ │
|
||||
│ │ loader │ │ loader │ │ loop │ │
|
||||
│ │ config │ │ context │ │ agent │ │
|
||||
│ │ loader │ │ loader │ │ loop │ │
|
||||
│ └────┬─────┘ └────┬─────┘ └─────┬──────┘ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ nostr_handler │ │
|
||||
│ │ relay pool · subscribe · publish │ │
|
||||
│ └──────────────────┬──────────────────┘ │
|
||||
@@ -191,11 +234,8 @@ Skills are shared across Nostr without any centralized registry or approval proc
|
||||
### How it works
|
||||
|
||||
1. **Publish**: An author publishes a skill as a kind `31123` event. The `content` field contains the skill body (markdown or structured JSON). The `d` tag is the skill's slug (e.g. `long_form_note`).
|
||||
|
||||
2. **Adopt**: An agent that wants to use a skill adds an `a`-tag reference to its kind `10123` adoption list. This is a public, replaceable event — anyone can see which skills an agent uses.
|
||||
|
||||
3. **Discover**: A new user queries `{"kinds": [10123], "authors": [<my-follows>]}` to see which skills their web of trust has adopted. The most-referenced `31123` addresses are the most popular skills — no rating system needed.
|
||||
|
||||
4. **Improve**: Anyone can publish their own `31123` with the same slug but a different pubkey. If their version is better, people adopt it instead. Competition happens through adoption, not through a store ranking.
|
||||
|
||||
### Why this works
|
||||
@@ -308,3 +348,4 @@ All dependencies are statically linked into the binary at build time. No system
|
||||
## License
|
||||
|
||||
TBD
|
||||
|
||||
|
||||
102
build_check.log
Normal file
102
build_check.log
Normal file
@@ -0,0 +1,102 @@
|
||||
==========================================
|
||||
Didactyl MUSL Static Binary Builder (PRODUCTION MODE)
|
||||
==========================================
|
||||
Project directory: /home/teknari/lt_gitea/didactyl
|
||||
Output directory: /home/teknari/lt_gitea/didactyl
|
||||
Debug build: false
|
||||
|
||||
✓ Docker is available and running
|
||||
|
||||
Building for platform: linux/amd64
|
||||
Output binary: didactyl_static_x86_64
|
||||
|
||||
Checking for cached Alpine Docker image...
|
||||
✓ Alpine 3.19 image found in cache
|
||||
|
||||
==========================================
|
||||
Step 1: Building Alpine Docker image
|
||||
==========================================
|
||||
This will:
|
||||
- Use Alpine Linux (native MUSL)
|
||||
- Build all dependencies statically
|
||||
- Compile didactyl with full static linking
|
||||
|
||||
#0 building with "default" instance using docker driver
|
||||
|
||||
#1 [internal] load build definition from Dockerfile.alpine-musl
|
||||
#1 transferring dockerfile: 3.81kB done
|
||||
#1 DONE 0.0s
|
||||
|
||||
#2 [internal] load metadata for docker.io/library/alpine:3.19
|
||||
#2 DONE 0.0s
|
||||
|
||||
#3 [internal] load .dockerignore
|
||||
#3 transferring context: 2B done
|
||||
#3 DONE 0.0s
|
||||
|
||||
#4 [builder 1/10] FROM docker.io/library/alpine:3.19
|
||||
#4 DONE 0.0s
|
||||
|
||||
#5 [internal] load build context
|
||||
#5 transferring context: 10.34kB done
|
||||
#5 DONE 0.0s
|
||||
|
||||
#6 [builder 9/10] RUN if [ "false" = "true" ]; then CFLAGS="-g -O2 -DDEBUG"; STRIP_CMD="echo 'Keeping debug symbols'"; echo "Building with DEBUG symbols enabled (optimized with -O2)"; else CFLAGS="-O2"; STRIP_CMD="strip /build/didactyl_static"; echo "Building optimized production binary (symbols stripped)"; fi && CURL_LIBS="$(pkg-config --static --libs libcurl)" && OPENSSL_LIBS="$(pkg-config --static --libs openssl)" && gcc -static $CFLAGS -Wall -Wextra -std=c99 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -I. -Isrc -Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket src/main.c src/config.c src/context.c src/llm.c src/nostr_handler.c src/agent.c src/tools.c src/debug.c -o /build/didactyl_static nostr_core_lib/libnostr_core_x64.a -lsecp256k1 $OPENSSL_LIBS $CURL_LIBS -lpthread -lm -ldl && eval "$STRIP_CMD"
|
||||
#6 CACHED
|
||||
|
||||
#7 [builder 10/10] RUN echo "=== Binary Information ===" && file /build/didactyl_static && ls -lh /build/didactyl_static && echo "=== Checking for dynamic dependencies ===" && (ldd /build/didactyl_static 2>&1 || echo "Binary is static") && echo "=== Build complete ==="
|
||||
#7 CACHED
|
||||
|
||||
#8 [builder 8/10] COPY Makefile /build/Makefile
|
||||
#8 CACHED
|
||||
|
||||
#9 [builder 2/10] RUN apk add --no-cache build-base musl-dev git cmake pkgconfig autoconf automake libtool openssl-dev openssl-libs-static zlib-dev zlib-static curl-dev curl-static nghttp2-dev nghttp2-static c-ares-dev c-ares-static libpsl-dev libpsl-static libidn2-dev libidn2-static libunistring-dev libunistring-static brotli-dev brotli-static zstd-dev zstd-static sqlite-dev sqlite-static linux-headers wget bash
|
||||
#9 CACHED
|
||||
|
||||
#10 [builder 3/10] WORKDIR /build
|
||||
#10 CACHED
|
||||
|
||||
#11 [builder 4/10] RUN cd /tmp && git clone https://github.com/bitcoin-core/secp256k1.git && cd secp256k1 && ./autogen.sh && ./configure --enable-static --disable-shared --prefix=/usr CFLAGS="-fPIC" && make -j$(nproc) && make install && rm -rf /tmp/secp256k1
|
||||
#11 CACHED
|
||||
|
||||
#12 [builder 7/10] COPY src/ /build/src/
|
||||
#12 CACHED
|
||||
|
||||
#13 [builder 5/10] COPY nostr_core_lib /build/nostr_core_lib/
|
||||
#13 CACHED
|
||||
|
||||
#14 [builder 6/10] RUN cd nostr_core_lib && chmod +x build.sh && sed -i 's/CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"/CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -fPIC -O2"/' build.sh && rm -f *.o *.a 2>/dev/null || true && ./build.sh --nips=all
|
||||
#14 CACHED
|
||||
|
||||
#15 [output 1/1] COPY --from=builder /build/didactyl_static /didactyl_static
|
||||
#15 CACHED
|
||||
|
||||
#16 exporting to image
|
||||
#16 exporting layers done
|
||||
#16 writing image sha256:1e0a868f1e0957613b56504ecee24b1032f765254e070aad5725dd9af34df56a done
|
||||
#16 naming to docker.io/library/didactyl-musl-builder:latest done
|
||||
#16 DONE 0.0s
|
||||
|
||||
✓ Docker image built successfully
|
||||
|
||||
==========================================
|
||||
Step 2: Extracting static binary
|
||||
==========================================
|
||||
✓ Binary extracted to: /home/teknari/lt_gitea/didactyl/didactyl_static_x86_64
|
||||
|
||||
==========================================
|
||||
Step 3: Verifying static binary
|
||||
==========================================
|
||||
|
||||
Checking for dynamic dependencies:
|
||||
✓ Binary is statically linked (verified with file command)
|
||||
|
||||
==========================================
|
||||
Build Summary
|
||||
==========================================
|
||||
Binary: /home/teknari/lt_gitea/didactyl/didactyl_static_x86_64
|
||||
Size: 6.8M
|
||||
Static: true
|
||||
Debug: false
|
||||
Platform: linux/amd64
|
||||
==========================================
|
||||
@@ -8,12 +8,6 @@
|
||||
"admin": {
|
||||
"pubkey": "admin pubkey"
|
||||
},
|
||||
"relays": [
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.primal.net",
|
||||
"ws://127.0.0.1:7777"
|
||||
],
|
||||
"llm": {
|
||||
"provider": "",
|
||||
"api_key": "",
|
||||
@@ -64,6 +58,8 @@
|
||||
"kind": 10002,
|
||||
"content": "",
|
||||
"tags": [
|
||||
|
||||
|
||||
[
|
||||
"r",
|
||||
"wss://relay.damus.io"
|
||||
@@ -80,6 +76,7 @@
|
||||
"r",
|
||||
"ws://127.0.0.1:7777"
|
||||
]
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
122
src/config.c
122
src/config.c
@@ -443,37 +443,87 @@ static int parse_startup_events(cJSON* root, didactyl_config_t* config) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_relays_from_startup_kind10002(didactyl_config_t* config) {
|
||||
if (!config || !config->startup_events || config->startup_event_count <= 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < config->startup_event_count; i++) {
|
||||
startup_event_t* se = &config->startup_events[i];
|
||||
if (se->kind != 10002 || !se->tags_json) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_Parse(se->tags_json);
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
cJSON_Delete(tags);
|
||||
continue;
|
||||
}
|
||||
|
||||
int tag_count = cJSON_GetArraySize(tags);
|
||||
int relay_count = 0;
|
||||
for (int j = 0; j < tag_count; j++) {
|
||||
cJSON* tag = cJSON_GetArrayItem(tags, j);
|
||||
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* key = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* val = cJSON_GetArrayItem(tag, 1);
|
||||
if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(key->valuestring, "r") == 0 && val->valuestring[0] != '\0') {
|
||||
relay_count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (relay_count <= 0) {
|
||||
cJSON_Delete(tags);
|
||||
continue;
|
||||
}
|
||||
|
||||
config->relays = (char**)calloc((size_t)relay_count, sizeof(char*));
|
||||
if (!config->relays) {
|
||||
cJSON_Delete(tags);
|
||||
return -1;
|
||||
}
|
||||
|
||||
config->relay_count = relay_count;
|
||||
int out_i = 0;
|
||||
for (int j = 0; j < tag_count && out_i < relay_count; j++) {
|
||||
cJSON* tag = cJSON_GetArrayItem(tags, j);
|
||||
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* key = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* val = cJSON_GetArrayItem(tag, 1);
|
||||
if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(key->valuestring, "r") == 0 && val->valuestring[0] != '\0') {
|
||||
config->relays[out_i] = strdup(val->valuestring);
|
||||
if (!config->relays[out_i]) {
|
||||
cJSON_Delete(tags);
|
||||
return -1;
|
||||
}
|
||||
out_i++;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(tags);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int parse_relays(cJSON* root, didactyl_config_t* config) {
|
||||
cJSON* relays = cJSON_GetObjectItemCaseSensitive(root, "relays");
|
||||
if (!relays || !cJSON_IsArray(relays)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int count = cJSON_GetArraySize(relays);
|
||||
if (count <= 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
config->relays = (char**)calloc((size_t)count, sizeof(char*));
|
||||
if (!config->relays) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
config->relay_count = count;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
cJSON* relay = cJSON_GetArrayItem(relays, i);
|
||||
if (!relay || !cJSON_IsString(relay) || !relay->valuestring || relay->valuestring[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
config->relays[i] = strdup(relay->valuestring);
|
||||
if (!config->relays[i]) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
(void)root;
|
||||
return parse_relays_from_startup_kind10002(config);
|
||||
}
|
||||
|
||||
void config_free(didactyl_config_t* config) {
|
||||
@@ -578,8 +628,13 @@ int config_load(const char* path, didactyl_config_t* config) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (parse_startup_events(root, config) != 0) {
|
||||
config_set_error("invalid startup_events configuration");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (parse_relays(root, config) != 0) {
|
||||
config_set_error("relays must be a non-empty array of relay URLs");
|
||||
config_set_error("relay configuration is invalid: startup_events must include kind 10002 with non-empty r tags");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
@@ -625,11 +680,6 @@ int config_load(const char* path, didactyl_config_t* config) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (parse_startup_events(root, config) != 0) {
|
||||
config_set_error("invalid startup_events configuration");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (decode_private_key(config->keys.nsec, config->keys.private_key) != 0) {
|
||||
config_set_error("keys.nsec must be valid nsec1... or 64-char hex private key");
|
||||
goto cleanup;
|
||||
|
||||
14
src/main.c
14
src/main.c
@@ -91,6 +91,20 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
DEBUG_INFO("[didactyl] startup phase: subscribe admin context end");
|
||||
|
||||
char startup_dm[768];
|
||||
const char* startup_name = nostr_handler_get_startup_display_name();
|
||||
int connected_relays = nostr_handler_connected_relay_count();
|
||||
snprintf(startup_dm,
|
||||
sizeof(startup_dm),
|
||||
"%s has started up and is online (version %s, connected relays: %d/%d).",
|
||||
(startup_name && startup_name[0] != '\0') ? startup_name : "Didactyl",
|
||||
DIDACTYL_VERSION,
|
||||
connected_relays,
|
||||
cfg.relay_count);
|
||||
if (nostr_handler_send_dm(cfg.admin.pubkey, startup_dm) != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: failed to send startup status DM to admin");
|
||||
}
|
||||
|
||||
DEBUG_INFO("[didactyl] startup phase: subscribe DMs begin");
|
||||
if (nostr_handler_subscribe_dms(agent_on_message, NULL) != 0) {
|
||||
DEBUG_ERROR("[didactyl] startup phase: subscribe DMs failed");
|
||||
|
||||
@@ -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 15
|
||||
#define DIDACTYL_VERSION "v0.0.15"
|
||||
#define DIDACTYL_VERSION_PATCH 16
|
||||
#define DIDACTYL_VERSION "v0.0.16"
|
||||
|
||||
// Agent metadata
|
||||
#define DIDACTYL_NAME "Didactyl"
|
||||
|
||||
@@ -18,11 +18,12 @@ static dm_callback_t g_dm_callback = NULL;
|
||||
static void* g_dm_user_data = NULL;
|
||||
static int g_poll_counter = 0;
|
||||
static time_t g_start_time = 0;
|
||||
static time_t g_last_status_log_time = 0;
|
||||
static nostr_pool_relay_status_t* g_last_relay_statuses = NULL;
|
||||
static char* g_system_context = NULL;
|
||||
static unsigned char* g_startup_published = NULL;
|
||||
static int g_startup_publish_tracking_enabled = 0;
|
||||
static int g_startup_kind1_already_exists = 0;
|
||||
static char g_startup_display_name[128] = "Didactyl";
|
||||
|
||||
static char* g_admin_kind0_json = NULL;
|
||||
static char* g_admin_kind10002_json = NULL;
|
||||
@@ -106,61 +107,9 @@ static int publish_kind_event_to_relays(int kind,
|
||||
static void on_admin_context_event(cJSON* event, const char* relay_url, void* user_data);
|
||||
static int parse_kind3_wot_contacts(cJSON* tags);
|
||||
static void upsert_kind1_note(time_t created_at, const char* content);
|
||||
|
||||
static void log_relay_statuses(const char* reason) {
|
||||
if (!g_pool || !g_cfg) {
|
||||
return;
|
||||
}
|
||||
|
||||
int info_level = (reason && strcmp(reason, "after init") == 0) ? 1 : 0;
|
||||
if (info_level) {
|
||||
DEBUG_INFO("[didactyl] relay status snapshot (%s)", reason ? reason : "periodic");
|
||||
} else {
|
||||
DEBUG_TRACE("[didactyl] relay status snapshot (%s)", reason ? reason : "periodic");
|
||||
}
|
||||
|
||||
for (int i = 0; i < g_cfg->relay_count; i++) {
|
||||
const char* relay = g_cfg->relays[i];
|
||||
nostr_pool_relay_status_t status = nostr_relay_pool_get_relay_status(g_pool, relay);
|
||||
const char* last_err = nostr_relay_pool_get_relay_last_connection_error(g_pool, relay);
|
||||
double ping_ms = nostr_relay_pool_get_relay_ping_latency(g_pool, relay);
|
||||
|
||||
if (info_level) {
|
||||
if (ping_ms > 0.0) {
|
||||
DEBUG_INFO("[didactyl] - %s => %s (ping %.1f ms)",
|
||||
relay,
|
||||
relay_status_str(status),
|
||||
ping_ms);
|
||||
} else {
|
||||
DEBUG_INFO("[didactyl] - %s => %s", relay, relay_status_str(status));
|
||||
}
|
||||
} else {
|
||||
if (ping_ms > 0.0) {
|
||||
DEBUG_TRACE("[didactyl] - %s => %s (ping %.1f ms)",
|
||||
relay,
|
||||
relay_status_str(status),
|
||||
ping_ms);
|
||||
} else {
|
||||
DEBUG_TRACE("[didactyl] - %s => %s", relay, relay_status_str(status));
|
||||
}
|
||||
}
|
||||
|
||||
if (last_err && last_err[0] != '\0') {
|
||||
DEBUG_WARN("[didactyl] - %s last_connection_error: %s", relay, last_err);
|
||||
}
|
||||
|
||||
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(g_pool, relay);
|
||||
if (stats) {
|
||||
DEBUG_LOG("[didactyl] - %s stats attempts=%d failures=%d recv=%d pub_ok=%d pub_fail=%d",
|
||||
relay,
|
||||
stats->connection_attempts,
|
||||
stats->connection_failures,
|
||||
stats->events_received,
|
||||
stats->events_published_ok,
|
||||
stats->events_published_failed);
|
||||
}
|
||||
}
|
||||
}
|
||||
static int startup_self_kind1_exists(void);
|
||||
static void load_startup_display_name(void);
|
||||
static void build_startup_kind1_content(char* out, size_t out_size, const char* fallback);
|
||||
|
||||
static void log_relay_state_changes(void) {
|
||||
if (!g_pool || !g_cfg || !g_last_relay_statuses) {
|
||||
@@ -196,6 +145,98 @@ static void log_publish_targets(const char* action) {
|
||||
}
|
||||
}
|
||||
|
||||
static int startup_self_kind1_exists(void) {
|
||||
if (!g_cfg || !g_pool) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
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 0;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_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,
|
||||
3000);
|
||||
|
||||
cJSON_Delete(filter);
|
||||
|
||||
if (events) {
|
||||
for (int i = 0; i < event_count; i++) {
|
||||
cJSON_Delete(events[i]);
|
||||
}
|
||||
free(events);
|
||||
}
|
||||
|
||||
return event_count > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
static void load_startup_display_name(void) {
|
||||
snprintf(g_startup_display_name, sizeof(g_startup_display_name), "%s", "Didactyl");
|
||||
|
||||
if (!g_cfg) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < g_cfg->startup_event_count; i++) {
|
||||
startup_event_t* se = &g_cfg->startup_events[i];
|
||||
if (se->kind != 0 || !se->content) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* content_json = cJSON_Parse(se->content);
|
||||
if (!content_json || !cJSON_IsObject(content_json)) {
|
||||
cJSON_Delete(content_json);
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* display_name = cJSON_GetObjectItemCaseSensitive(content_json, "display_name");
|
||||
cJSON* name = cJSON_GetObjectItemCaseSensitive(content_json, "name");
|
||||
|
||||
if (display_name && cJSON_IsString(display_name) && display_name->valuestring && display_name->valuestring[0] != '\0') {
|
||||
snprintf(g_startup_display_name, sizeof(g_startup_display_name), "%s", display_name->valuestring);
|
||||
cJSON_Delete(content_json);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') {
|
||||
snprintf(g_startup_display_name, sizeof(g_startup_display_name), "%s", name->valuestring);
|
||||
cJSON_Delete(content_json);
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON_Delete(content_json);
|
||||
}
|
||||
}
|
||||
|
||||
static void build_startup_kind1_content(char* out, size_t out_size, const char* fallback) {
|
||||
if (!out || out_size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fallback && fallback[0] != '\0') {
|
||||
snprintf(out, out_size, "%s (%s startup)", fallback, g_startup_display_name);
|
||||
} else {
|
||||
snprintf(out, out_size, "%s startup complete and online", g_startup_display_name);
|
||||
}
|
||||
}
|
||||
|
||||
static int hex_to_pubkey(const char* hex, unsigned char out_pubkey[32]) {
|
||||
if (!hex || !out_pubkey || strlen(hex) != 64U) {
|
||||
return -1;
|
||||
@@ -671,6 +712,11 @@ int nostr_handler_init(didactyl_config_t* config) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (nostr_relay_pool_set_auth(g_pool, g_cfg->keys.private_key, 1) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "[didactyl] failed to enable relay pool auth\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < g_cfg->relay_count; i++) {
|
||||
if (nostr_relay_pool_add_relay(g_pool, g_cfg->relays[i]) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "[didactyl] failed to add relay: %s\n", g_cfg->relays[i]);
|
||||
@@ -688,9 +734,6 @@ int nostr_handler_init(didactyl_config_t* config) {
|
||||
g_last_relay_statuses[i] = nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]);
|
||||
}
|
||||
|
||||
g_last_status_log_time = time(NULL);
|
||||
log_relay_statuses("after init");
|
||||
|
||||
free(g_startup_published);
|
||||
g_startup_published = NULL;
|
||||
g_startup_publish_tracking_enabled = 0;
|
||||
@@ -899,6 +942,21 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message)
|
||||
}
|
||||
}
|
||||
|
||||
int* pre_publish_ok = NULL;
|
||||
if (connected_count > 0) {
|
||||
pre_publish_ok = (int*)calloc((size_t)connected_count, sizeof(int));
|
||||
if (!pre_publish_ok) {
|
||||
free(connected_relays);
|
||||
cJSON_Delete(event);
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < connected_count; i++) {
|
||||
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(g_pool, connected_relays[i]);
|
||||
pre_publish_ok[i] = stats ? stats->events_published_ok : 0;
|
||||
}
|
||||
}
|
||||
|
||||
int sent = 0;
|
||||
if (connected_count > 0) {
|
||||
sent = nostr_relay_pool_publish_async(
|
||||
@@ -912,6 +970,48 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message)
|
||||
for (int i = 0; i < connected_count; i++) {
|
||||
DEBUG_INFO("[didactyl] kind 4 event published to %s (async)", connected_relays[i]);
|
||||
}
|
||||
|
||||
// Briefly drain relay messages so NIP-42 AUTH handshake can complete.
|
||||
for (int i = 0; i < 5; i++) {
|
||||
(void)nostr_relay_pool_poll(g_pool, 100);
|
||||
}
|
||||
|
||||
int any_publish_ok = 0;
|
||||
int auth_required_seen = 0;
|
||||
for (int i = 0; i < connected_count; i++) {
|
||||
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(g_pool, connected_relays[i]);
|
||||
if (stats && stats->events_published_ok > pre_publish_ok[i]) {
|
||||
any_publish_ok = 1;
|
||||
}
|
||||
|
||||
const char* pub_err = nostr_relay_pool_get_relay_last_publish_error(g_pool, connected_relays[i]);
|
||||
if (pub_err && strstr(pub_err, "auth-required") != NULL) {
|
||||
auth_required_seen = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Always retry this same signed event once after handshake drain.
|
||||
// Some relays issue AUTH challenge for the first EVENT and only accept
|
||||
// the subsequent resend after AUTH succeeds.
|
||||
if (sent > 0) {
|
||||
if (auth_required_seen || !any_publish_ok) {
|
||||
DEBUG_WARN("[didactyl] retrying kind 4 event once after auth handshake window");
|
||||
}
|
||||
int resent = nostr_relay_pool_publish_async(
|
||||
g_pool,
|
||||
connected_relays,
|
||||
connected_count,
|
||||
event,
|
||||
NULL,
|
||||
NULL);
|
||||
if (resent > sent) {
|
||||
sent = resent;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
(void)nostr_relay_pool_poll(g_pool, 100);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DEBUG_WARN("[didactyl] kind 4 event not queued: no connected relays");
|
||||
}
|
||||
@@ -924,6 +1024,7 @@ int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message)
|
||||
recipient_pubkey_hex,
|
||||
sent);
|
||||
|
||||
free(pre_publish_ok);
|
||||
free(connected_relays);
|
||||
cJSON_Delete(event);
|
||||
return sent > 0 ? 0 : -1;
|
||||
@@ -1082,6 +1183,9 @@ static void publish_pending_startup_events_for_relay_index(int relay_index, cons
|
||||
if (!se->content) {
|
||||
continue;
|
||||
}
|
||||
if (se->kind == 1 && g_startup_kind1_already_exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t slot = (size_t)i * (size_t)g_cfg->relay_count + (size_t)relay_index;
|
||||
if (g_startup_published[slot]) {
|
||||
@@ -1097,9 +1201,19 @@ static void publish_pending_startup_events_for_relay_index(int relay_index, cons
|
||||
}
|
||||
}
|
||||
|
||||
char kind1_content[512];
|
||||
const char* content_to_publish = se->content;
|
||||
if (se->kind == 1) {
|
||||
build_startup_kind1_content(kind1_content, sizeof(kind1_content), se->content);
|
||||
content_to_publish = kind1_content;
|
||||
}
|
||||
|
||||
const char* one_relay[1] = { relay_url };
|
||||
if (publish_kind_event_to_relays(se->kind, se->content, tags, one_relay, 1, reason, NULL) == 0) {
|
||||
if (publish_kind_event_to_relays(se->kind, content_to_publish, tags, one_relay, 1, reason, NULL) == 0) {
|
||||
g_startup_published[slot] = 1;
|
||||
if (se->kind == 1) {
|
||||
g_startup_kind1_already_exists = 1;
|
||||
}
|
||||
} else {
|
||||
DEBUG_WARN("[didactyl] startup event publish failed for kind=%d relay=%s", se->kind, relay_url);
|
||||
}
|
||||
@@ -1141,6 +1255,12 @@ int nostr_handler_reconcile_startup_events(void) {
|
||||
}
|
||||
}
|
||||
|
||||
load_startup_display_name();
|
||||
g_startup_kind1_already_exists = startup_self_kind1_exists();
|
||||
if (g_startup_kind1_already_exists) {
|
||||
DEBUG_INFO("[didactyl] startup phase: existing self kind-1 note found; skipping startup kind-1 publish");
|
||||
}
|
||||
|
||||
if (!g_system_context) {
|
||||
g_system_context = strdup("You are Didactyl, a sovereign AI agent living on Nostr.");
|
||||
}
|
||||
@@ -1156,6 +1276,25 @@ const char* nostr_handler_get_system_context(void) {
|
||||
return g_system_context;
|
||||
}
|
||||
|
||||
const char* nostr_handler_get_startup_display_name(void) {
|
||||
return g_startup_display_name;
|
||||
}
|
||||
|
||||
int nostr_handler_connected_relay_count(void) {
|
||||
if (!g_cfg || !g_pool) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int connected = 0;
|
||||
for (int i = 0; i < g_cfg->relay_count; i++) {
|
||||
if (nostr_relay_pool_get_relay_status(g_pool, g_cfg->relays[i]) == NOSTR_POOL_RELAY_CONNECTED) {
|
||||
connected++;
|
||||
}
|
||||
}
|
||||
|
||||
return connected;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1240,12 +1379,6 @@ int nostr_handler_poll(int timeout_ms) {
|
||||
|
||||
log_relay_state_changes();
|
||||
|
||||
time_t now = time(NULL);
|
||||
if (g_last_status_log_time == 0 || difftime(now, g_last_status_log_time) >= 10.0) {
|
||||
log_relay_statuses("periodic");
|
||||
g_last_status_log_time = now;
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
@@ -1256,11 +1389,11 @@ void nostr_handler_cleanup(void) {
|
||||
|
||||
free(g_last_relay_statuses);
|
||||
g_last_relay_statuses = NULL;
|
||||
g_last_status_log_time = 0;
|
||||
|
||||
free(g_startup_published);
|
||||
g_startup_published = NULL;
|
||||
g_startup_publish_tracking_enabled = 0;
|
||||
g_startup_kind1_already_exists = 0;
|
||||
snprintf(g_startup_display_name, sizeof(g_startup_display_name), "%s", "Didactyl");
|
||||
|
||||
g_pool = NULL;
|
||||
g_cfg = NULL;
|
||||
|
||||
@@ -37,6 +37,8 @@ char* nostr_handler_query_json(cJSON* filter, int timeout_ms);
|
||||
int nostr_handler_poll(int timeout_ms);
|
||||
int nostr_handler_reconcile_startup_events(void);
|
||||
const char* nostr_handler_get_system_context(void);
|
||||
const char* nostr_handler_get_startup_display_name(void);
|
||||
int nostr_handler_connected_relay_count(void);
|
||||
char* nostr_handler_get_admin_kind0_context(void);
|
||||
char* nostr_handler_get_admin_kind10002_context(void);
|
||||
char* nostr_handler_get_admin_kind1_notes_context(void);
|
||||
|
||||
207
src/tools.c
207
src/tools.c
@@ -173,65 +173,6 @@ static char* parse_loose_json_string_value(const char* in, const char* key) {
|
||||
return out;
|
||||
}
|
||||
|
||||
static cJSON* parse_loose_tags_array(const char* in) {
|
||||
if (!in) return NULL;
|
||||
|
||||
const char* p = find_key_start(in, "tags");
|
||||
if (!p) return NULL;
|
||||
|
||||
p = strchr(p, ':');
|
||||
if (!p) return NULL;
|
||||
p = skip_ws(p + 1);
|
||||
if (!p || *p != '[') return NULL;
|
||||
|
||||
const char* start = p;
|
||||
int depth = 0;
|
||||
int in_string = 0;
|
||||
int escaping = 0;
|
||||
|
||||
for (; *p; p++) {
|
||||
char c = *p;
|
||||
|
||||
if (escaping) {
|
||||
escaping = 0;
|
||||
continue;
|
||||
}
|
||||
if (c == '\\' && in_string) {
|
||||
escaping = 1;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') {
|
||||
in_string = !in_string;
|
||||
continue;
|
||||
}
|
||||
if (in_string) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '[') depth++;
|
||||
else if (c == ']') {
|
||||
depth--;
|
||||
if (depth == 0) {
|
||||
size_t len = (size_t)(p - start + 1);
|
||||
char* arr_json = (char*)malloc(len + 1U);
|
||||
if (!arr_json) return NULL;
|
||||
memcpy(arr_json, start, len);
|
||||
arr_json[len] = '\0';
|
||||
|
||||
cJSON* tags = cJSON_Parse(arr_json);
|
||||
free(arr_json);
|
||||
if (tags && cJSON_IsArray(tags)) {
|
||||
return tags;
|
||||
}
|
||||
cJSON_Delete(tags);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static cJSON* parse_loose_nostr_post_args(const char* in) {
|
||||
if (!in) return NULL;
|
||||
|
||||
@@ -255,11 +196,6 @@ static cJSON* parse_loose_nostr_post_args(const char* in) {
|
||||
cJSON_AddStringToObject(args, "content", content);
|
||||
free(content);
|
||||
|
||||
cJSON* tags = parse_loose_tags_array(in);
|
||||
if (tags) {
|
||||
cJSON_AddItemToObject(args, "tags", tags);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
@@ -715,6 +651,23 @@ char* tools_build_openai_schema_json(const tools_context_t* ctx) {
|
||||
cJSON_AddItemToObject(t5, "function", t5_fn);
|
||||
cJSON_AddItemToArray(tools, t5);
|
||||
|
||||
cJSON* t6 = cJSON_CreateObject();
|
||||
cJSON* t6_fn = cJSON_CreateObject();
|
||||
cJSON* t6_params = cJSON_CreateObject();
|
||||
cJSON* t6_props = cJSON_CreateObject();
|
||||
cJSON* t6_required = cJSON_CreateArray();
|
||||
|
||||
cJSON_AddStringToObject(t6, "type", "function");
|
||||
cJSON_AddStringToObject(t6_fn, "name", "nostr_post_readme");
|
||||
cJSON_AddStringToObject(t6_fn, "description", "Publish README.md as kind 30023 with deterministic d tag readme.md");
|
||||
cJSON_AddStringToObject(t6_params, "type", "object");
|
||||
cJSON_AddItemToObject(t6_params, "properties", t6_props);
|
||||
cJSON_AddItemToObject(t6_params, "required", t6_required);
|
||||
|
||||
cJSON_AddItemToObject(t6_fn, "parameters", t6_params);
|
||||
cJSON_AddItemToObject(t6, "function", t6_fn);
|
||||
cJSON_AddItemToArray(tools, t6);
|
||||
|
||||
char* out = cJSON_PrintUnformatted(tools);
|
||||
cJSON_Delete(tools);
|
||||
return out;
|
||||
@@ -815,6 +768,128 @@ static char* execute_nostr_post(const char* args_json) {
|
||||
return json;
|
||||
}
|
||||
|
||||
static char* read_entire_file(const char* file_path, size_t* out_bytes) {
|
||||
if (!file_path) return NULL;
|
||||
|
||||
FILE* fp = fopen(file_path, "rb");
|
||||
if (!fp) return NULL;
|
||||
|
||||
if (fseek(fp, 0, SEEK_END) != 0) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
long size = ftell(fp);
|
||||
if (size < 0) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (fseek(fp, 0, SEEK_SET) != 0) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* buf = (char*)malloc((size_t)size + 1U);
|
||||
if (!buf) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t n = fread(buf, 1, (size_t)size, fp);
|
||||
fclose(fp);
|
||||
buf[n] = '\0';
|
||||
|
||||
if (out_bytes) *out_bytes = n;
|
||||
return buf;
|
||||
}
|
||||
|
||||
static char* execute_nostr_post_readme(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error("tool context unavailable");
|
||||
|
||||
cJSON* args = cJSON_Parse(args_json ? args_json : "{}");
|
||||
if (!args) return json_error("invalid arguments JSON");
|
||||
cJSON_Delete(args);
|
||||
|
||||
char readme_path[PATH_MAX];
|
||||
if (build_tool_path(ctx, "README.md", readme_path, sizeof(readme_path)) != 0) {
|
||||
return json_error("failed to resolve README path");
|
||||
}
|
||||
|
||||
size_t bytes_read = 0;
|
||||
char* content = read_entire_file(readme_path, &bytes_read);
|
||||
if (!content) {
|
||||
return json_error("failed to read README.md");
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) {
|
||||
free(content);
|
||||
return json_error("failed to create tags");
|
||||
}
|
||||
|
||||
if (add_string_tag(tags, "d", "readme.md") != 0) {
|
||||
cJSON_Delete(tags);
|
||||
free(content);
|
||||
return json_error("failed to set d tag");
|
||||
}
|
||||
|
||||
ensure_nip23_metadata_tags(30023, content, &tags);
|
||||
|
||||
nostr_publish_result_t publish_result;
|
||||
memset(&publish_result, 0, sizeof(publish_result));
|
||||
|
||||
int rc = nostr_handler_publish_kind_event(30023, content, tags, &publish_result);
|
||||
cJSON_Delete(tags);
|
||||
free(content);
|
||||
|
||||
if (rc != 0) {
|
||||
nostr_handler_publish_result_free(&publish_result);
|
||||
return json_error("nostr_post_readme failed");
|
||||
}
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) {
|
||||
nostr_handler_publish_result_free(&publish_result);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0);
|
||||
cJSON_AddStringToObject(out, "message", "nostr_post_readme published");
|
||||
cJSON_AddStringToObject(out, "path", readme_path);
|
||||
cJSON_AddNumberToObject(out, "bytes_read", (double)bytes_read);
|
||||
cJSON_AddNumberToObject(out, "kind", publish_result.kind);
|
||||
cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
|
||||
cJSON_AddNumberToObject(out, "relay_count", publish_result.relay_count);
|
||||
cJSON_AddNumberToObject(out, "accepted_by_pool_count", publish_result.accepted_by_pool_count);
|
||||
|
||||
if (publish_result.note_uri[0] != '\0') {
|
||||
cJSON_AddStringToObject(out, "note_uri", publish_result.note_uri);
|
||||
}
|
||||
if (publish_result.naddr_uri[0] != '\0') {
|
||||
cJSON_AddStringToObject(out, "naddr_uri", publish_result.naddr_uri);
|
||||
}
|
||||
if (publish_result.d_tag[0] != '\0') {
|
||||
cJSON_AddStringToObject(out, "d_tag", publish_result.d_tag);
|
||||
}
|
||||
|
||||
cJSON* relays = cJSON_CreateArray();
|
||||
if (!relays) {
|
||||
nostr_handler_publish_result_free(&publish_result);
|
||||
cJSON_Delete(out);
|
||||
return NULL;
|
||||
}
|
||||
for (int i = 0; i < publish_result.relay_count; i++) {
|
||||
cJSON_AddItemToArray(relays, cJSON_CreateString(publish_result.relays[i] ? publish_result.relays[i] : ""));
|
||||
}
|
||||
cJSON_AddItemToObject(out, "relays", relays);
|
||||
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
nostr_handler_publish_result_free(&publish_result);
|
||||
return json;
|
||||
}
|
||||
|
||||
static char* execute_nostr_query(const char* args_json) {
|
||||
cJSON* args = cJSON_Parse(args_json ? args_json : "{}");
|
||||
if (!args) return json_error("invalid arguments JSON");
|
||||
@@ -1040,5 +1115,9 @@ char* tools_execute(tools_context_t* ctx, const char* tool_name, const char* arg
|
||||
return execute_file_write(ctx, args_json);
|
||||
}
|
||||
|
||||
if (strcmp(tool_name, "nostr_post_readme") == 0) {
|
||||
return execute_nostr_post_readme(ctx, args_json);
|
||||
}
|
||||
|
||||
return json_error("unknown tool");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user