v0.0.15 - Auto-add NIP-23 title/image/summary/published_at/d tags when missing

This commit is contained in:
Your Name
2026-02-28 18:02:02 -04:00
parent 56b9ae421c
commit 02d4e2caa0
3 changed files with 277 additions and 4 deletions
+2 -2
View File
@@ -12,11 +12,11 @@ Because all identity, communication, and memory live on Nostr, the agent is **po
**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.
## Current Status — v0.0.14
## Current Status — v0.0.15
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.0.14Handle malformed nostr_post arguments with loose JSON fallback parsing
> Last release update: v0.0.15Auto-add NIP-23 title/image/summary/published_at/d tags when missing
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected
+2 -2
View File
@@ -12,8 +12,8 @@
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
#define DIDACTYL_VERSION_MAJOR 0
#define DIDACTYL_VERSION_MINOR 0
#define DIDACTYL_VERSION_PATCH 14
#define DIDACTYL_VERSION "v0.0.14"
#define DIDACTYL_VERSION_PATCH 15
#define DIDACTYL_VERSION "v0.0.15"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"
+273
View File
@@ -7,6 +7,7 @@
#include <string.h>
#include <limits.h>
#include <ctype.h>
#include <time.h>
#include "cjson/cJSON.h"
#include "nostr_handler.h"
@@ -262,6 +263,276 @@ static cJSON* parse_loose_nostr_post_args(const char* in) {
return args;
}
static cJSON* ensure_tags_array(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(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(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(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(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(line + j, len - j);
}
}
if (!nl) break;
p = nl + 1;
}
return NULL;
}
static int is_paragraph_line(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(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(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(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(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(url_start, (size_t)(url_end - url_start));
}
return NULL;
}
static char* slugify_string(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(int kind, const char* content, cJSON** tags_inout) {
if (!content || (kind != 30023 && kind != 30024) || !tags_inout) {
return;
}
cJSON* tags = ensure_tags_array(tags_inout);
if (!tags) return;
char* title = has_tag_key(tags, "title") ? NULL : first_markdown_h1(content);
char* summary = has_tag_key(tags, "summary") ? NULL : first_markdown_paragraph(content);
char* image = has_tag_key(tags, "image") ? NULL : first_markdown_image_url(content);
if (title) {
(void)add_string_tag(tags, "title", title);
}
if (summary) {
(void)add_string_tag(tags, "summary", summary);
}
if (image) {
(void)add_string_tag(tags, "image", image);
}
if (!has_tag_key(tags, "published_at")) {
char published_at[32];
snprintf(published_at, sizeof(published_at), "%lld", (long long)time(NULL));
(void)add_string_tag(tags, "published_at", published_at);
}
if (!has_tag_key(tags, "d")) {
char* slug = NULL;
if (title) {
slug = slugify_string(title, "long-form-note");
} else {
char* derived_title = first_markdown_h1(content);
slug = slugify_string(derived_title, "long-form-note");
free(derived_title);
}
if (slug) {
(void)add_string_tag(tags, "d", slug);
free(slug);
}
}
free(title);
free(summary);
free(image);
}
static int is_safe_relative_path(const char* path) {
if (!path || path[0] == '\0') return 0;
if (path[0] == '/') return 0;
@@ -487,6 +758,8 @@ static char* execute_nostr_post(const char* args_json) {
}
}
ensure_nip23_metadata_tags((int)kind->valuedouble, content->valuestring, &tags_dup);
nostr_publish_result_t publish_result;
memset(&publish_result, 0, sizeof(publish_result));