v0.0.15 - Auto-add NIP-23 title/image/summary/published_at/d tags when missing
This commit is contained in:
@@ -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.14 — Handle malformed nostr_post arguments with loose JSON fallback parsing
|
||||
> Last release update: v0.0.15 — Auto-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
@@ -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
@@ -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));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user