267 lines
9.4 KiB
C
267 lines
9.4 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "tools_internal.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "cjson/cJSON.h"
|
|
#include "../debug.h"
|
|
#include "../nostr_handler.h"
|
|
|
|
/* nostr_reply — reply to a Nostr event (NIP-10).
|
|
*
|
|
* This is a general primitive: any agent, in a swarm or not, should be able
|
|
* to reply to a note. It handles the NIP-10 threading tags correctly:
|
|
*
|
|
* ["e", <parent_id>, <relay_hint>, "reply"]
|
|
* ["e", <root_id>, <relay_hint>, "root"] (only if the parent is itself
|
|
* a reply, so the whole thread
|
|
* stays connected)
|
|
* ["p", <parent_author>] (so the parent author is
|
|
* notified and clients thread it)
|
|
*
|
|
* The parent event is fetched to discover its author and its root, so the
|
|
* caller only needs to supply the event id and the reply text.
|
|
*/
|
|
|
|
static char* json_error_local(const char* msg) {
|
|
cJSON* root = cJSON_CreateObject();
|
|
if (!root) return NULL;
|
|
cJSON_AddBoolToObject(root, "success", 0);
|
|
cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
|
|
char* out = cJSON_PrintUnformatted(root);
|
|
cJSON_Delete(root);
|
|
return out;
|
|
}
|
|
|
|
static cJSON* parse_args_local(const char* args_json) {
|
|
const char* raw = args_json ? args_json : "{}";
|
|
cJSON* args = cJSON_Parse(raw);
|
|
if (!args || !cJSON_IsObject(args)) {
|
|
cJSON_Delete(args);
|
|
return NULL;
|
|
}
|
|
return args;
|
|
}
|
|
|
|
static const char* get_string_local(cJSON* obj, const char* key) {
|
|
cJSON* item = cJSON_GetObjectItemCaseSensitive(obj, key);
|
|
if (item && cJSON_IsString(item) && item->valuestring && item->valuestring[0] != '\0') {
|
|
return item->valuestring;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Find the first tag with the given key and return its value at index 1.
|
|
* Returns a strdup'd string or NULL. */
|
|
static char* first_tag_value_local(cJSON* event, const char* key) {
|
|
cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
|
|
if (!tags || !cJSON_IsArray(tags)) return NULL;
|
|
|
|
int n = cJSON_GetArraySize(tags);
|
|
for (int i = 0; i < n; i++) {
|
|
cJSON* tag = cJSON_GetArrayItem(tags, i);
|
|
if (!tag || !cJSON_IsArray(tag)) continue;
|
|
cJSON* k = cJSON_GetArrayItem(tag, 0);
|
|
cJSON* v = cJSON_GetArrayItem(tag, 1);
|
|
if (k && cJSON_IsString(k) && k->valuestring &&
|
|
strcmp(k->valuestring, key) == 0 &&
|
|
v && cJSON_IsString(v) && v->valuestring) {
|
|
return strdup(v->valuestring);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Find the "root" e-tag value (the 4th element == "root"), or NULL. */
|
|
static char* find_root_e_tag_local(cJSON* event) {
|
|
cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags");
|
|
if (!tags || !cJSON_IsArray(tags)) return NULL;
|
|
|
|
int n = cJSON_GetArraySize(tags);
|
|
for (int i = 0; i < n; i++) {
|
|
cJSON* tag = cJSON_GetArrayItem(tags, i);
|
|
if (!tag || !cJSON_IsArray(tag)) continue;
|
|
cJSON* k = cJSON_GetArrayItem(tag, 0);
|
|
cJSON* v = cJSON_GetArrayItem(tag, 1);
|
|
cJSON* marker = cJSON_GetArrayItem(tag, 3);
|
|
if (k && cJSON_IsString(k) && k->valuestring &&
|
|
strcmp(k->valuestring, "e") == 0 &&
|
|
v && cJSON_IsString(v) && v->valuestring &&
|
|
marker && cJSON_IsString(marker) && marker->valuestring &&
|
|
strcmp(marker->valuestring, "root") == 0) {
|
|
return strdup(v->valuestring);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Fetch an event by id. Returns a cJSON object (caller frees) or NULL. */
|
|
static cJSON* fetch_event_local(const char* event_id) {
|
|
if (!event_id || event_id[0] == '\0') return NULL;
|
|
|
|
cJSON* filter = cJSON_CreateObject();
|
|
cJSON* ids = cJSON_CreateArray();
|
|
if (!filter || !ids) {
|
|
cJSON_Delete(filter);
|
|
cJSON_Delete(ids);
|
|
return NULL;
|
|
}
|
|
cJSON_AddItemToArray(ids, cJSON_CreateString(event_id));
|
|
cJSON_AddItemToObject(filter, "ids", ids);
|
|
cJSON_AddNumberToObject(filter, "limit", 1);
|
|
|
|
char* events_json = nostr_handler_query_json(filter, 5000);
|
|
cJSON_Delete(filter);
|
|
if (!events_json) return NULL;
|
|
|
|
cJSON* arr = cJSON_Parse(events_json);
|
|
free(events_json);
|
|
if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) <= 0) {
|
|
cJSON_Delete(arr);
|
|
return NULL;
|
|
}
|
|
|
|
cJSON* ev = cJSON_Duplicate(cJSON_GetArrayItem(arr, 0), 1);
|
|
cJSON_Delete(arr);
|
|
return ev;
|
|
}
|
|
|
|
/* Append ["e", id, relay_hint, marker] to tags. */
|
|
static void add_e_tag_local(cJSON* tags, const char* id, const char* relay_hint, const char* marker) {
|
|
cJSON* e = cJSON_CreateArray();
|
|
if (!e) return;
|
|
cJSON_AddItemToArray(e, cJSON_CreateString("e"));
|
|
cJSON_AddItemToArray(e, cJSON_CreateString(id));
|
|
cJSON_AddItemToArray(e, cJSON_CreateString(relay_hint ? relay_hint : ""));
|
|
cJSON_AddItemToArray(e, cJSON_CreateString(marker ? marker : ""));
|
|
cJSON_AddItemToArray(tags, e);
|
|
}
|
|
|
|
/* Append ["p", pubkey] to tags. */
|
|
static void add_p_tag_local(cJSON* tags, const char* pubkey) {
|
|
if (!pubkey || pubkey[0] == '\0') return;
|
|
cJSON* p = cJSON_CreateArray();
|
|
if (!p) return;
|
|
cJSON_AddItemToArray(p, cJSON_CreateString("p"));
|
|
cJSON_AddItemToArray(p, cJSON_CreateString(pubkey));
|
|
cJSON_AddItemToArray(tags, p);
|
|
}
|
|
|
|
char* execute_nostr_reply(tools_context_t* ctx, const char* args_json) {
|
|
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
|
|
|
cJSON* args = parse_args_local(args_json);
|
|
if (!args) return json_error_local("invalid arguments JSON");
|
|
|
|
const char* event_id = get_string_local(args, "event_id");
|
|
const char* content = get_string_local(args, "content");
|
|
const char* relay_hint = get_string_local(args, "relay_hint");
|
|
|
|
if (!event_id || !content) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("nostr_reply requires 'event_id' and 'content'");
|
|
}
|
|
|
|
/* Optional kind override (default 1). */
|
|
int kind = 1;
|
|
cJSON* kind_j = cJSON_GetObjectItemCaseSensitive(args, "kind");
|
|
if (kind_j && cJSON_IsNumber(kind_j)) {
|
|
kind = (int)kind_j->valuedouble;
|
|
}
|
|
|
|
/* Copy the ids we need after args is freed. */
|
|
char parent_id[128];
|
|
char hint[256];
|
|
snprintf(parent_id, sizeof(parent_id), "%s", event_id);
|
|
snprintf(hint, sizeof(hint), "%s", relay_hint ? relay_hint : "");
|
|
|
|
/* Fetch the parent so we can thread correctly. */
|
|
cJSON* parent = fetch_event_local(parent_id);
|
|
|
|
char* parent_author = parent ? first_tag_value_local(parent, "p") : NULL;
|
|
/* The parent's author is its "pubkey" field, not a p-tag. */
|
|
if (parent) {
|
|
cJSON* pk = cJSON_GetObjectItemCaseSensitive(parent, "pubkey");
|
|
if (pk && cJSON_IsString(pk) && pk->valuestring) {
|
|
free(parent_author);
|
|
parent_author = strdup(pk->valuestring);
|
|
}
|
|
}
|
|
|
|
/* If the parent is itself a reply, find the thread root so we keep the
|
|
* whole conversation connected. */
|
|
char* root_id = parent ? find_root_e_tag_local(parent) : NULL;
|
|
|
|
cJSON* tags = cJSON_CreateArray();
|
|
if (!tags) {
|
|
free(parent_author);
|
|
free(root_id);
|
|
cJSON_Delete(parent);
|
|
cJSON_Delete(args);
|
|
return json_error_local("nostr_reply allocation failure");
|
|
}
|
|
|
|
/* NIP-10: include the root first (if any), then the direct parent. */
|
|
if (root_id && root_id[0] != '\0' && strcmp(root_id, parent_id) != 0) {
|
|
add_e_tag_local(tags, root_id, hint[0] ? hint : NULL, "root");
|
|
}
|
|
add_e_tag_local(tags, parent_id, hint[0] ? hint : NULL, "reply");
|
|
add_p_tag_local(tags, parent_author);
|
|
|
|
/* Allow the caller to add extra tags. */
|
|
cJSON* extra_tags = cJSON_GetObjectItemCaseSensitive(args, "tags");
|
|
if (extra_tags && cJSON_IsArray(extra_tags)) {
|
|
int n = cJSON_GetArraySize(extra_tags);
|
|
for (int i = 0; i < n; i++) {
|
|
cJSON* t = cJSON_GetArrayItem(extra_tags, i);
|
|
if (t && cJSON_IsArray(t)) {
|
|
cJSON_AddItemToArray(tags, cJSON_Duplicate(t, 1));
|
|
}
|
|
}
|
|
}
|
|
|
|
nostr_publish_result_t publish_result;
|
|
memset(&publish_result, 0, sizeof(publish_result));
|
|
|
|
int rc = nostr_handler_publish_kind_event(kind, content, tags, &publish_result);
|
|
cJSON_Delete(tags);
|
|
cJSON_Delete(parent);
|
|
cJSON_Delete(args);
|
|
|
|
if (rc != 0) {
|
|
free(parent_author);
|
|
free(root_id);
|
|
nostr_handler_publish_result_free(&publish_result);
|
|
return json_error_local("nostr_reply failed to publish");
|
|
}
|
|
|
|
cJSON* out = cJSON_CreateObject();
|
|
if (!out) {
|
|
free(parent_author);
|
|
free(root_id);
|
|
nostr_handler_publish_result_free(&publish_result);
|
|
return NULL;
|
|
}
|
|
cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0);
|
|
cJSON_AddStringToObject(out, "message", "reply published");
|
|
cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
|
|
cJSON_AddStringToObject(out, "reply_to", parent_id);
|
|
if (root_id && root_id[0] != '\0') {
|
|
cJSON_AddStringToObject(out, "thread_root", root_id);
|
|
}
|
|
if (parent_author && parent_author[0] != '\0') {
|
|
cJSON_AddStringToObject(out, "reply_to_author", parent_author);
|
|
}
|
|
cJSON_AddNumberToObject(out, "relays_published", publish_result.accepted_by_pool_count);
|
|
|
|
free(parent_author);
|
|
free(root_id);
|
|
nostr_handler_publish_result_free(&publish_result);
|
|
char* json = cJSON_PrintUnformatted(out);
|
|
cJSON_Delete(out);
|
|
return json;
|
|
}
|