928 lines
28 KiB
C
928 lines
28 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "tools_internal.h"
|
|
|
|
#include <ctype.h>
|
|
#include <limits.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#include "cjson/cJSON.h"
|
|
#include "../nostr_handler.h"
|
|
|
|
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 const char* skip_ws_local(const char* p) {
|
|
while (p && *p && isspace((unsigned char)*p)) p++;
|
|
return p;
|
|
}
|
|
|
|
static char* sanitize_json_string_controls_local(const char* in) {
|
|
if (!in) return NULL;
|
|
|
|
size_t len = strlen(in);
|
|
size_t cap = (len * 2U) + 1U;
|
|
char* out = (char*)malloc(cap);
|
|
if (!out) return NULL;
|
|
|
|
int in_string = 0;
|
|
int escaping = 0;
|
|
size_t j = 0;
|
|
|
|
for (size_t i = 0; i < len; i++) {
|
|
char c = in[i];
|
|
|
|
if (escaping) {
|
|
if (j + 1U >= cap) {
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
out[j++] = c;
|
|
escaping = 0;
|
|
continue;
|
|
}
|
|
|
|
if (c == '\\') {
|
|
if (j + 1U >= cap) {
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
out[j++] = c;
|
|
if (in_string) escaping = 1;
|
|
continue;
|
|
}
|
|
|
|
if (c == '"') {
|
|
if (j + 1U >= cap) {
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
out[j++] = c;
|
|
in_string = !in_string;
|
|
continue;
|
|
}
|
|
|
|
if (in_string && (c == '\n' || c == '\r' || c == '\t')) {
|
|
if (j + 2U >= cap) {
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
out[j++] = '\\';
|
|
out[j++] = (c == '\n') ? 'n' : (c == '\r') ? 'r' : 't';
|
|
continue;
|
|
}
|
|
|
|
if (j + 1U >= cap) {
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
out[j++] = c;
|
|
}
|
|
|
|
out[j] = '\0';
|
|
return out;
|
|
}
|
|
|
|
static cJSON* parse_tool_args_json_local(const char* args_json) {
|
|
const char* raw_args_json = args_json ? args_json : "{}";
|
|
raw_args_json = skip_ws_local(raw_args_json);
|
|
if (!raw_args_json || raw_args_json[0] == '\0') {
|
|
raw_args_json = "{}";
|
|
}
|
|
|
|
cJSON* args = cJSON_Parse(raw_args_json);
|
|
char* repaired_args_json = NULL;
|
|
|
|
if (args && cJSON_IsString(args) && args->valuestring) {
|
|
cJSON* nested = cJSON_Parse(args->valuestring);
|
|
if (nested) {
|
|
cJSON_Delete(args);
|
|
args = nested;
|
|
}
|
|
}
|
|
|
|
if (!args) {
|
|
repaired_args_json = sanitize_json_string_controls_local(raw_args_json);
|
|
if (repaired_args_json) {
|
|
args = cJSON_Parse(repaired_args_json);
|
|
if (args && cJSON_IsString(args) && args->valuestring) {
|
|
cJSON* nested = cJSON_Parse(args->valuestring);
|
|
if (nested) {
|
|
cJSON_Delete(args);
|
|
args = nested;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
free(repaired_args_json);
|
|
return args;
|
|
}
|
|
|
|
static const char* find_key_start_local(const char* in, const char* key) {
|
|
if (!in || !key) return NULL;
|
|
|
|
char pattern[64];
|
|
int n = snprintf(pattern, sizeof(pattern), "\"%s\"", key);
|
|
if (n <= 0 || (size_t)n >= sizeof(pattern)) return NULL;
|
|
|
|
return strstr(in, pattern);
|
|
}
|
|
|
|
static int parse_loose_kind_local(const char* in, int* out_kind) {
|
|
if (!in || !out_kind) return -1;
|
|
|
|
const char* p = find_key_start_local(in, "kind");
|
|
if (!p) return -1;
|
|
|
|
p = strchr(p, ':');
|
|
if (!p) return -1;
|
|
p = skip_ws_local(p + 1);
|
|
if (!p || !*p) return -1;
|
|
|
|
char* end = NULL;
|
|
long v = strtol(p, &end, 10);
|
|
if (end == p) return -1;
|
|
|
|
*out_kind = (int)v;
|
|
return 0;
|
|
}
|
|
|
|
static char* parse_loose_json_string_value_local(const char* in, const char* key) {
|
|
if (!in || !key) return NULL;
|
|
|
|
const char* p = find_key_start_local(in, key);
|
|
if (!p) return NULL;
|
|
|
|
p = strchr(p, ':');
|
|
if (!p) return NULL;
|
|
p = skip_ws_local(p + 1);
|
|
if (!p || *p != '"') return NULL;
|
|
p++;
|
|
|
|
size_t max_len = strlen(p);
|
|
char* out = (char*)malloc(max_len + 1U);
|
|
if (!out) return NULL;
|
|
|
|
size_t j = 0;
|
|
int escaping = 0;
|
|
for (size_t i = 0; p[i] != '\0'; i++) {
|
|
char c = p[i];
|
|
|
|
if (escaping) {
|
|
switch (c) {
|
|
case 'n': out[j++] = '\n'; break;
|
|
case 'r': out[j++] = '\r'; break;
|
|
case 't': out[j++] = '\t'; break;
|
|
case '"': out[j++] = '"'; break;
|
|
case '\\': out[j++] = '\\'; break;
|
|
default: out[j++] = c; break;
|
|
}
|
|
escaping = 0;
|
|
continue;
|
|
}
|
|
|
|
if (c == '\\') {
|
|
escaping = 1;
|
|
continue;
|
|
}
|
|
|
|
if (c == '"') {
|
|
out[j] = '\0';
|
|
return out;
|
|
}
|
|
|
|
out[j++] = c;
|
|
}
|
|
|
|
out[j] = '\0';
|
|
return out;
|
|
}
|
|
|
|
static cJSON* parse_loose_nostr_post_args_local(const char* in) {
|
|
if (!in) return NULL;
|
|
|
|
int kind = 0;
|
|
if (parse_loose_kind_local(in, &kind) != 0) {
|
|
return NULL;
|
|
}
|
|
|
|
char* content = parse_loose_json_string_value_local(in, "content");
|
|
if (!content) {
|
|
return NULL;
|
|
}
|
|
|
|
cJSON* args = cJSON_CreateObject();
|
|
if (!args) {
|
|
free(content);
|
|
return NULL;
|
|
}
|
|
|
|
cJSON_AddNumberToObject(args, "kind", kind);
|
|
cJSON_AddStringToObject(args, "content", content);
|
|
free(content);
|
|
return args;
|
|
}
|
|
|
|
static cJSON* ensure_tags_array_local(cJSON** tags_inout) {
|
|
if (!tags_inout) return NULL;
|
|
|
|
if (*tags_inout) {
|
|
if (cJSON_IsArray(*tags_inout)) {
|
|
return *tags_inout;
|
|
}
|
|
cJSON_Delete(*tags_inout);
|
|
*tags_inout = NULL;
|
|
}
|
|
|
|
*tags_inout = cJSON_CreateArray();
|
|
return *tags_inout;
|
|
}
|
|
|
|
static int has_tag_key_local(cJSON* tags, const char* key) {
|
|
if (!tags || !cJSON_IsArray(tags) || !key) return 0;
|
|
|
|
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);
|
|
if (k && cJSON_IsString(k) && k->valuestring && strcmp(k->valuestring, key) == 0) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int add_string_tag_local(cJSON* tags, const char* key, const char* value) {
|
|
if (!tags || !cJSON_IsArray(tags) || !key || !value || value[0] == '\0') return -1;
|
|
|
|
cJSON* tag = cJSON_CreateArray();
|
|
if (!tag) return -1;
|
|
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString(key));
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString(value));
|
|
cJSON_AddItemToArray(tags, tag);
|
|
return 0;
|
|
}
|
|
|
|
static char* trim_copy_local(const char* start, size_t len) {
|
|
while (len > 0 && isspace((unsigned char)start[0])) {
|
|
start++;
|
|
len--;
|
|
}
|
|
while (len > 0 && isspace((unsigned char)start[len - 1])) {
|
|
len--;
|
|
}
|
|
|
|
char* out = (char*)malloc(len + 1U);
|
|
if (!out) return NULL;
|
|
memcpy(out, start, len);
|
|
out[len] = '\0';
|
|
return out;
|
|
}
|
|
|
|
static char* first_markdown_h1_local(const char* content) {
|
|
if (!content) return NULL;
|
|
|
|
const char* p = content;
|
|
while (*p) {
|
|
const char* line = p;
|
|
const char* nl = strchr(line, '\n');
|
|
size_t len = nl ? (size_t)(nl - line) : strlen(line);
|
|
while (len > 0 && line[len - 1] == '\r') len--;
|
|
|
|
size_t i = 0;
|
|
while (i < len && isspace((unsigned char)line[i])) i++;
|
|
if (i < len && line[i] == '#') {
|
|
size_t j = i;
|
|
while (j < len && line[j] == '#') j++;
|
|
if (j < len && isspace((unsigned char)line[j])) {
|
|
while (j < len && isspace((unsigned char)line[j])) j++;
|
|
if (j < len) return trim_copy_local(line + j, len - j);
|
|
}
|
|
}
|
|
|
|
if (!nl) break;
|
|
p = nl + 1;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static int is_paragraph_line_local(const char* line, size_t len) {
|
|
size_t i = 0;
|
|
while (i < len && isspace((unsigned char)line[i])) i++;
|
|
if (i >= len) return 0;
|
|
|
|
char c = line[i];
|
|
if (c == '#' || c == '>' || c == '-' || c == '*' || c == '`' || c == '|') return 0;
|
|
if (isdigit((unsigned char)c)) {
|
|
size_t j = i;
|
|
while (j < len && isdigit((unsigned char)line[j])) j++;
|
|
if (j < len && line[j] == '.') return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static char* first_markdown_paragraph_local(const char* content) {
|
|
if (!content) return NULL;
|
|
|
|
const char* p = content;
|
|
while (*p) {
|
|
const char* line = p;
|
|
const char* nl = strchr(line, '\n');
|
|
size_t len = nl ? (size_t)(nl - line) : strlen(line);
|
|
while (len > 0 && line[len - 1] == '\r') len--;
|
|
|
|
if (is_paragraph_line_local(line, len)) {
|
|
size_t cap = 1024;
|
|
size_t used = 0;
|
|
char* out = (char*)malloc(cap);
|
|
if (!out) return NULL;
|
|
|
|
const char* q = line;
|
|
const char* qnl = nl;
|
|
size_t qlen = len;
|
|
|
|
while (1) {
|
|
if (!is_paragraph_line_local(q, qlen)) break;
|
|
|
|
size_t start = 0;
|
|
while (start < qlen && isspace((unsigned char)q[start])) start++;
|
|
size_t end = qlen;
|
|
while (end > start && isspace((unsigned char)q[end - 1])) end--;
|
|
size_t part_len = end - start;
|
|
|
|
if (used + part_len + 2 >= cap) {
|
|
cap = (cap * 2U) + part_len + 16U;
|
|
char* bigger = (char*)realloc(out, cap);
|
|
if (!bigger) {
|
|
free(out);
|
|
return NULL;
|
|
}
|
|
out = bigger;
|
|
}
|
|
|
|
if (part_len > 0) {
|
|
if (used > 0) out[used++] = ' ';
|
|
memcpy(out + used, q + start, part_len);
|
|
used += part_len;
|
|
}
|
|
|
|
if (!qnl) break;
|
|
q = qnl + 1;
|
|
qnl = strchr(q, '\n');
|
|
qlen = qnl ? (size_t)(qnl - q) : strlen(q);
|
|
while (qlen > 0 && q[qlen - 1] == '\r') qlen--;
|
|
}
|
|
|
|
out[used] = '\0';
|
|
return out;
|
|
}
|
|
|
|
if (!nl) break;
|
|
p = nl + 1;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static char* first_markdown_image_url_local(const char* content) {
|
|
if (!content) return NULL;
|
|
|
|
const char* p = content;
|
|
while ((p = strstr(p, "![")) != NULL) {
|
|
const char* close_bracket = strchr(p + 2, ']');
|
|
if (!close_bracket || close_bracket[1] != '(') {
|
|
p += 2;
|
|
continue;
|
|
}
|
|
|
|
const char* url_start = close_bracket + 2;
|
|
const char* url_end = strchr(url_start, ')');
|
|
if (!url_end || url_end <= url_start) {
|
|
p += 2;
|
|
continue;
|
|
}
|
|
|
|
return trim_copy_local(url_start, (size_t)(url_end - url_start));
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static char* d_tagify_string_local(const char* in, const char* fallback) {
|
|
if (!in || in[0] == '\0') {
|
|
return fallback ? strdup(fallback) : NULL;
|
|
}
|
|
|
|
size_t len = strlen(in);
|
|
char* out = (char*)malloc(len + 1U);
|
|
if (!out) return NULL;
|
|
|
|
size_t j = 0;
|
|
int prev_dash = 0;
|
|
for (size_t i = 0; i < len; i++) {
|
|
unsigned char c = (unsigned char)in[i];
|
|
if (isalnum(c)) {
|
|
out[j++] = (char)tolower(c);
|
|
prev_dash = 0;
|
|
} else if (!prev_dash && j > 0) {
|
|
out[j++] = '-';
|
|
prev_dash = 1;
|
|
}
|
|
}
|
|
|
|
while (j > 0 && out[j - 1] == '-') j--;
|
|
out[j] = '\0';
|
|
|
|
if (j == 0) {
|
|
free(out);
|
|
return fallback ? strdup(fallback) : NULL;
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
static void ensure_nip23_metadata_tags_local(int kind, const char* content, cJSON** tags_inout) {
|
|
if (!content || (kind != 30023 && kind != 30024) || !tags_inout) {
|
|
return;
|
|
}
|
|
|
|
cJSON* tags = ensure_tags_array_local(tags_inout);
|
|
if (!tags) return;
|
|
|
|
char* title = has_tag_key_local(tags, "title") ? NULL : first_markdown_h1_local(content);
|
|
char* summary = has_tag_key_local(tags, "summary") ? NULL : first_markdown_paragraph_local(content);
|
|
char* image = has_tag_key_local(tags, "image") ? NULL : first_markdown_image_url_local(content);
|
|
|
|
if (title) (void)add_string_tag_local(tags, "title", title);
|
|
if (summary) (void)add_string_tag_local(tags, "summary", summary);
|
|
if (image) (void)add_string_tag_local(tags, "image", image);
|
|
|
|
if (!has_tag_key_local(tags, "published_at")) {
|
|
char published_at[32];
|
|
snprintf(published_at, sizeof(published_at), "%lld", (long long)time(NULL));
|
|
(void)add_string_tag_local(tags, "published_at", published_at);
|
|
}
|
|
|
|
if (!has_tag_key_local(tags, "d")) {
|
|
char* d_tag = NULL;
|
|
if (title) {
|
|
d_tag = d_tagify_string_local(title, "long-form-note");
|
|
} else {
|
|
char* derived_title = first_markdown_h1_local(content);
|
|
d_tag = d_tagify_string_local(derived_title, "long-form-note");
|
|
free(derived_title);
|
|
}
|
|
if (d_tag) {
|
|
(void)add_string_tag_local(tags, "d", d_tag);
|
|
free(d_tag);
|
|
}
|
|
}
|
|
|
|
free(title);
|
|
free(summary);
|
|
free(image);
|
|
}
|
|
|
|
static int is_safe_relative_path_local(const char* path) {
|
|
if (!path || path[0] == '\0') return 0;
|
|
if (path[0] == '/') return 0;
|
|
if (strstr(path, "..") != NULL) return 0;
|
|
if (strchr(path, '\\') != NULL) return 0;
|
|
return 1;
|
|
}
|
|
|
|
static int build_tool_path_local(tools_context_t* ctx, const char* rel_path, char* out, size_t out_size) {
|
|
if (!ctx || !ctx->cfg || !rel_path || !out || out_size == 0) return -1;
|
|
if (!is_safe_relative_path_local(rel_path)) return -1;
|
|
|
|
const char* cwd = ctx->cfg->tools.shell.working_directory[0] != '\0'
|
|
? ctx->cfg->tools.shell.working_directory
|
|
: ".";
|
|
|
|
int n = 0;
|
|
if (strcmp(cwd, ".") == 0) {
|
|
n = snprintf(out, out_size, "%s", rel_path);
|
|
} else {
|
|
n = snprintf(out, out_size, "%s/%s", cwd, rel_path);
|
|
}
|
|
|
|
if (n < 0 || (size_t)n >= out_size) return -1;
|
|
return 0;
|
|
}
|
|
|
|
static char* read_entire_file_local(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* basename_lowercase_dup_local(const char* path) {
|
|
if (!path || path[0] == '\0') return NULL;
|
|
|
|
const char* base = strrchr(path, '/');
|
|
base = base ? (base + 1) : path;
|
|
if (base[0] == '\0') return NULL;
|
|
|
|
char* out = strdup(base);
|
|
if (!out) return NULL;
|
|
|
|
for (size_t i = 0; out[i] != '\0'; i++) {
|
|
out[i] = (char)tolower((unsigned char)out[i]);
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
char* execute_nostr_post(const char* args_json) {
|
|
const char* raw_args_json = args_json ? args_json : "{}";
|
|
raw_args_json = skip_ws_local(raw_args_json);
|
|
if (!raw_args_json || raw_args_json[0] == '\0') {
|
|
raw_args_json = "{}";
|
|
}
|
|
|
|
cJSON* args = cJSON_Parse(raw_args_json);
|
|
char* repaired_args_json = NULL;
|
|
|
|
if (args && cJSON_IsString(args) && args->valuestring) {
|
|
cJSON* nested = cJSON_Parse(args->valuestring);
|
|
if (nested) {
|
|
cJSON_Delete(args);
|
|
args = nested;
|
|
}
|
|
}
|
|
|
|
if (!args && raw_args_json) {
|
|
repaired_args_json = sanitize_json_string_controls_local(raw_args_json);
|
|
if (repaired_args_json) {
|
|
args = cJSON_Parse(repaired_args_json);
|
|
if (args && cJSON_IsString(args) && args->valuestring) {
|
|
cJSON* nested = cJSON_Parse(args->valuestring);
|
|
if (nested) {
|
|
cJSON_Delete(args);
|
|
args = nested;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!args && raw_args_json) {
|
|
args = parse_loose_nostr_post_args_local(raw_args_json);
|
|
}
|
|
if (!args) {
|
|
free(repaired_args_json);
|
|
return json_error_local("invalid arguments JSON");
|
|
}
|
|
|
|
cJSON* kind = cJSON_GetObjectItemCaseSensitive(args, "kind");
|
|
cJSON* content = cJSON_GetObjectItemCaseSensitive(args, "content");
|
|
cJSON* tags = cJSON_GetObjectItemCaseSensitive(args, "tags");
|
|
if (!kind || !cJSON_IsNumber(kind) || !content || !cJSON_IsString(content) || !content->valuestring) {
|
|
cJSON_Delete(args);
|
|
free(repaired_args_json);
|
|
return json_error_local("nostr_post requires integer kind and string content");
|
|
}
|
|
if (tags && !cJSON_IsArray(tags)) {
|
|
cJSON_Delete(args);
|
|
free(repaired_args_json);
|
|
return json_error_local("nostr_post tags must be an array when provided");
|
|
}
|
|
|
|
cJSON* tags_dup = NULL;
|
|
if (tags) {
|
|
tags_dup = cJSON_Duplicate(tags, 1);
|
|
if (!tags_dup) {
|
|
cJSON_Delete(args);
|
|
free(repaired_args_json);
|
|
return json_error_local("nostr_post failed to duplicate tags");
|
|
}
|
|
}
|
|
|
|
ensure_nip23_metadata_tags_local((int)kind->valuedouble, content->valuestring, &tags_dup);
|
|
|
|
nostr_publish_result_t publish_result;
|
|
memset(&publish_result, 0, sizeof(publish_result));
|
|
|
|
int rc = nostr_handler_publish_kind_event((int)kind->valuedouble,
|
|
content->valuestring,
|
|
tags_dup,
|
|
&publish_result);
|
|
cJSON_Delete(tags_dup);
|
|
cJSON_Delete(args);
|
|
free(repaired_args_json);
|
|
|
|
if (rc != 0) {
|
|
nostr_handler_publish_result_free(&publish_result);
|
|
return json_error_local("nostr_post 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 published");
|
|
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;
|
|
}
|
|
|
|
char* execute_nostr_post_readme(tools_context_t* ctx, const char* args_json) {
|
|
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
|
|
|
cJSON* args = parse_tool_args_json_local(args_json);
|
|
if (!args) return json_error_local("invalid arguments JSON");
|
|
cJSON_Delete(args);
|
|
|
|
char readme_path[PATH_MAX];
|
|
if (build_tool_path_local(ctx, "README.md", readme_path, sizeof(readme_path)) != 0) {
|
|
return json_error_local("failed to resolve README path");
|
|
}
|
|
|
|
size_t bytes_read = 0;
|
|
char* content = read_entire_file_local(readme_path, &bytes_read);
|
|
if (!content) {
|
|
return json_error_local("failed to read README.md");
|
|
}
|
|
|
|
cJSON* tags = cJSON_CreateArray();
|
|
if (!tags) {
|
|
free(content);
|
|
return json_error_local("failed to create tags");
|
|
}
|
|
|
|
if (add_string_tag_local(tags, "d", "readme.md") != 0 ||
|
|
add_string_tag_local(tags,
|
|
"image",
|
|
"https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg") != 0) {
|
|
cJSON_Delete(tags);
|
|
free(content);
|
|
return json_error_local("failed to set required tags");
|
|
}
|
|
|
|
ensure_nip23_metadata_tags_local(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_local("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;
|
|
}
|
|
|
|
char* execute_nostr_file_md_to_longform_post(tools_context_t* ctx, const char* args_json) {
|
|
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
|
|
|
cJSON* args = parse_tool_args_json_local(args_json);
|
|
if (!args) return json_error_local("invalid arguments JSON");
|
|
|
|
cJSON* file = cJSON_GetObjectItemCaseSensitive(args, "file");
|
|
cJSON* title = cJSON_GetObjectItemCaseSensitive(args, "title");
|
|
cJSON* image = cJSON_GetObjectItemCaseSensitive(args, "image");
|
|
cJSON* summary = cJSON_GetObjectItemCaseSensitive(args, "summary");
|
|
|
|
if (!file || !cJSON_IsString(file) || !file->valuestring || file->valuestring[0] == '\0') {
|
|
cJSON_Delete(args);
|
|
return json_error_local("nostr_file_md_to_longform_post requires string file");
|
|
}
|
|
if (title && !cJSON_IsString(title)) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("nostr_file_md_to_longform_post title must be string when provided");
|
|
}
|
|
if (image && !cJSON_IsString(image)) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("nostr_file_md_to_longform_post image must be string when provided");
|
|
}
|
|
if (summary && !cJSON_IsString(summary)) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("nostr_file_md_to_longform_post summary must be string when provided");
|
|
}
|
|
|
|
char file_path[PATH_MAX];
|
|
if (build_tool_path_local(ctx, file->valuestring, file_path, sizeof(file_path)) != 0) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("failed to resolve markdown file path");
|
|
}
|
|
|
|
size_t bytes_read = 0;
|
|
char* content = read_entire_file_local(file_path, &bytes_read);
|
|
if (!content) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("failed to read markdown file");
|
|
}
|
|
|
|
char* d_tag = basename_lowercase_dup_local(file->valuestring);
|
|
if (!d_tag || d_tag[0] == '\0') {
|
|
free(d_tag);
|
|
free(content);
|
|
cJSON_Delete(args);
|
|
return json_error_local("failed to derive d tag from filename");
|
|
}
|
|
|
|
cJSON* tags = cJSON_CreateArray();
|
|
if (!tags) {
|
|
free(d_tag);
|
|
free(content);
|
|
cJSON_Delete(args);
|
|
return json_error_local("failed to create tags");
|
|
}
|
|
|
|
if (add_string_tag_local(tags, "d", d_tag) != 0) {
|
|
free(d_tag);
|
|
cJSON_Delete(tags);
|
|
free(content);
|
|
cJSON_Delete(args);
|
|
return json_error_local("failed to set d tag");
|
|
}
|
|
|
|
if (title && title->valuestring && title->valuestring[0] != '\0') {
|
|
if (add_string_tag_local(tags, "title", title->valuestring) != 0) {
|
|
free(d_tag);
|
|
cJSON_Delete(tags);
|
|
free(content);
|
|
cJSON_Delete(args);
|
|
return json_error_local("failed to set title tag");
|
|
}
|
|
}
|
|
|
|
if (image && image->valuestring && image->valuestring[0] != '\0') {
|
|
if (add_string_tag_local(tags, "image", image->valuestring) != 0) {
|
|
free(d_tag);
|
|
cJSON_Delete(tags);
|
|
free(content);
|
|
cJSON_Delete(args);
|
|
return json_error_local("failed to set image tag");
|
|
}
|
|
}
|
|
|
|
if (summary && summary->valuestring && summary->valuestring[0] != '\0') {
|
|
if (add_string_tag_local(tags, "summary", summary->valuestring) != 0) {
|
|
free(d_tag);
|
|
cJSON_Delete(tags);
|
|
free(content);
|
|
cJSON_Delete(args);
|
|
return json_error_local("failed to set summary tag");
|
|
}
|
|
}
|
|
|
|
ensure_nip23_metadata_tags_local(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);
|
|
cJSON_Delete(args);
|
|
|
|
if (rc != 0) {
|
|
free(d_tag);
|
|
nostr_handler_publish_result_free(&publish_result);
|
|
return json_error_local("nostr_file_md_to_longform_post failed");
|
|
}
|
|
|
|
cJSON* out = cJSON_CreateObject();
|
|
if (!out) {
|
|
free(d_tag);
|
|
nostr_handler_publish_result_free(&publish_result);
|
|
return NULL;
|
|
}
|
|
|
|
cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0);
|
|
cJSON_AddStringToObject(out, "message", "nostr_file_md_to_longform_post published");
|
|
cJSON_AddStringToObject(out, "path", file_path);
|
|
cJSON_AddStringToObject(out, "d_tag", d_tag);
|
|
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);
|
|
|
|
cJSON* relays = cJSON_CreateArray();
|
|
if (!relays) {
|
|
free(d_tag);
|
|
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);
|
|
free(d_tag);
|
|
nostr_handler_publish_result_free(&publish_result);
|
|
return json;
|
|
}
|