v0.0.1 - Unify publish reporting, improve login test mnemonic flow, and fix profile edit persistence

This commit is contained in:
Laan Tungir
2026-05-01 13:31:20 -04:00
parent d3b8633c9d
commit 67467c179e
15 changed files with 824 additions and 303 deletions
+3 -2
View File
@@ -8,7 +8,7 @@ set(CMAKE_C_EXTENSIONS OFF)
add_compile_options(-Wall -Wextra -D_GNU_SOURCE)
# nt application sources
file(GLOB NT_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/*.c")
file(GLOB NT_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.c")
# nostr_core_lib sources
file(GLOB NOSTR_CORE_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/resources/nostr_core_lib/nostr_core/*.c")
@@ -45,10 +45,11 @@ target_include_directories(nt PRIVATE
target_link_libraries(nt PRIVATE
nostr_core_lib
sqlite3
curl
secp256k1
ssl
crypto
pthread
m
z
dl
)
Regular → Executable
+45 -4
View File
@@ -1,5 +1,46 @@
#!/bin/bash
mkdir -p build
cd build
cmake ..
make -j$(nproc)
set -e
# nt MUSL static build script (emulates c-relay style)
# Produces build/nt_static_x86_64 on x86_64 hosts.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="$SCRIPT_DIR/build"
DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl"
OUTPUT_NAME="nt_static_x86_64"
if ! command -v docker >/dev/null 2>&1; then
echo "[ERROR] docker is required"
exit 1
fi
if ! docker info >/dev/null 2>&1; then
echo "[ERROR] docker daemon is not running"
exit 1
fi
mkdir -p "$BUILD_DIR"
echo "[INFO] Building MUSL static binary via Docker (builder stage)..."
docker build \
--platform linux/amd64 \
--target builder \
-f "$DOCKERFILE" \
-t nt-musl-builder:latest \
.
echo "[INFO] Extracting binary..."
CONTAINER_ID=$(docker create nt-musl-builder:latest)
docker cp "$CONTAINER_ID:/build/nt_static" "$BUILD_DIR/$OUTPUT_NAME"
docker rm "$CONTAINER_ID" >/dev/null
chmod +x "$BUILD_DIR/$OUTPUT_NAME"
echo "[INFO] Verifying static link..."
if ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1 | grep -q "not a dynamic executable\|statically linked"; then
echo "[SUCCESS] Binary is static: $BUILD_DIR/$OUTPUT_NAME"
else
echo "[WARNING] ldd output unexpected:"
ldd "$BUILD_DIR/$OUTPUT_NAME" || true
fi
echo "[DONE] $BUILD_DIR/$OUTPUT_NAME"
+50 -5
View File
@@ -5,15 +5,35 @@
extern "C" {
#endif
typedef struct {
char *relay_url;
int posted;
char *response;
} nt_publish_result_t;
/*
* Publish a pre-built event JSON to relays.
* Returns the number of relays that acknowledged with OK=true.
* Publish a pre-built event JSON to relays with per-relay result capture.
* Returns number of relays that acknowledged with OK=true.
* On success, caller owns results_out and must free via nt_publish_results_free().
*/
int nt_publish_detailed(const char *event_json,
const char **relay_urls,
int relay_count,
int timeout_ms,
nt_publish_result_t **results_out,
int *result_count_out);
/*
* Publish wrapper that only returns accepted relay count.
*/
int nt_publish(const char *event_json,
const char **relay_urls,
int relay_count,
int timeout_ms);
/* Free result array returned by nt_publish_detailed(). */
void nt_publish_results_free(nt_publish_result_t *results, int result_count);
/*
* Run a synchronous REQ query across relays, deduplicating by event id.
* On success returns 0 and sets events_out/event_count (caller frees strings and array).
@@ -26,6 +46,19 @@ int nt_query_sync(const char *filter_json,
char ***events_out,
int *event_count);
/*
* Same as nt_query_sync, but with optional user-facing progress output.
* If verbose != 0 and phase_label is provided, status lines are printed.
*/
int nt_query_sync_verbose(const char *filter_json,
const char **relay_urls,
int relay_count,
int timeout_ms,
const char *phase_label,
int verbose,
char ***events_out,
int *event_count);
/*
* Query relays and return only the newest event found (by created_at).
* On success returns 0, sets event_out to malloc'd JSON string or NULL if none.
@@ -37,9 +70,21 @@ int nt_get_first(const char *filter_json,
char **event_out);
/*
* Fetch relay NIP-11 information document via HTTP(S).
* Converts ws:// -> http:// and wss:// -> https:// and sends
* Accept: application/nostr+json.
* Same as nt_get_first, but with optional user-facing progress output.
*/
int nt_get_first_verbose(const char *filter_json,
const char **relay_urls,
int relay_count,
int timeout_ms,
const char *phase_label,
int verbose,
char **event_out);
/*
* Fetch relay NIP-11 information document.
*
* Current implementation in nt is a stub for static MUSL builds and
* returns -1 (no HTTP client linked).
*/
int nt_fetch_nip11(const char *relay_url, char **info_json_out);
+16
View File
@@ -0,0 +1,16 @@
#ifndef PUBLISH_H
#define PUBLISH_H
#include "../resources/nostr_core_lib/cjson/cJSON.h"
/*
* Build, sign, publish, and print standardized per-relay results.
* Returns number of relays that acknowledged with OK=true, or -1 on hard failure.
*/
int publish_signed_event_with_report(const char *label,
int kind,
const char *content,
cJSON *tags,
int timeout_ms);
#endif /* PUBLISH_H */
+4
View File
@@ -14,6 +14,10 @@ typedef struct {
char *kind0_json;
char *kind3_json;
char *kind10002_json;
char *kind10096_json;
char *kind17375_json;
char **kind30078_events;
int kind30078_count;
const char *bootstrap_relays[8];
int bootstrap_relay_count;
+147
View File
@@ -0,0 +1,147 @@
#!/bin/bash
set -e
# nt increment + push script
# Canonical version source is latest git tag (vX.Y.Z)
# This script increments the tag and updates NT_VERSION macros in src/main.c
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
print_status() { echo -e "${BLUE}[INFO]${NC} $1" >&2; }
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" >&2; }
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" >&2; }
print_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
COMMIT_MESSAGE=""
VERSION_INCREMENT_TYPE="patch" # patch|minor|major
show_usage() {
echo "nt increment and push"
echo ""
echo "USAGE:"
echo " $0 [OPTIONS] \"commit message\""
echo ""
echo "OPTIONS:"
echo " -p, --patch Increment patch version (default)"
echo " -m, --minor Increment minor version"
echo " -M, --major Increment major version"
echo " -h, --help Show help"
}
while [[ $# -gt 0 ]]; do
case "$1" in
-p|--patch)
VERSION_INCREMENT_TYPE="patch"
shift
;;
-m|--minor)
VERSION_INCREMENT_TYPE="minor"
shift
;;
-M|--major)
VERSION_INCREMENT_TYPE="major"
shift
;;
-h|--help)
show_usage
exit 0
;;
*)
if [[ -z "$COMMIT_MESSAGE" ]]; then
COMMIT_MESSAGE="$1"
fi
shift
;;
esac
done
if [[ -z "$COMMIT_MESSAGE" ]]; then
print_error "Commit message is required"
echo ""
show_usage
exit 1
fi
if ! git rev-parse --git-dir > /dev/null 2>&1; then
print_error "Not in a git repository"
exit 1
fi
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || true)
if [[ -z "$LATEST_TAG" ]]; then
LATEST_TAG="v0.0.0"
print_warning "No version tags found, starting from $LATEST_TAG"
fi
VERSION=${LATEST_TAG#v}
if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
MAJOR=${BASH_REMATCH[1]}
MINOR=${BASH_REMATCH[2]}
PATCH=${BASH_REMATCH[3]}
else
print_error "Invalid version format in tag: $LATEST_TAG"
exit 1
fi
case "$VERSION_INCREMENT_TYPE" in
major)
NEW_MAJOR=$((MAJOR + 1))
NEW_MINOR=0
NEW_PATCH=0
;;
minor)
NEW_MAJOR=$MAJOR
NEW_MINOR=$((MINOR + 1))
NEW_PATCH=0
;;
*)
NEW_MAJOR=$MAJOR
NEW_MINOR=$MINOR
NEW_PATCH=$((PATCH + 1))
;;
esac
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
print_status "Current version: $LATEST_TAG"
print_status "New version: $NEW_VERSION"
if [[ ! -f "src/main.c" ]]; then
print_error "src/main.c not found"
exit 1
fi
print_status "Updating version macros in src/main.c"
sed -i "s/^#define NT_VERSION_MAJOR .*/#define NT_VERSION_MAJOR ${NEW_MAJOR}/" src/main.c
sed -i "s/^#define NT_VERSION_MINOR .*/#define NT_VERSION_MINOR ${NEW_MINOR}/" src/main.c
sed -i "s/^#define NT_VERSION_PATCH .*/#define NT_VERSION_PATCH ${NEW_PATCH}/" src/main.c
sed -i "s/^#define NT_VERSION \".*\"/#define NT_VERSION \"${NEW_VERSION}\"/" src/main.c
print_success "Updated version in src/main.c to $NEW_VERSION"
print_status "Staging changes"
git add src/main.c CMakeLists.txt build.sh increment_and_push.sh README.md plans/planning.md include/*.h src/*.c .test_mnemonic 2>/dev/null || git add .
if git diff --staged --quiet; then
print_warning "No changes to commit"
else
git commit -m "$NEW_VERSION - $COMMIT_MESSAGE"
print_success "Committed changes"
fi
if git rev-parse "$NEW_VERSION" >/dev/null 2>&1; then
print_warning "Tag $NEW_VERSION already exists locally; recreating"
git tag -d "$NEW_VERSION" >/dev/null 2>&1 || true
fi
git tag "$NEW_VERSION"
print_success "Created tag: $NEW_VERSION"
print_status "Pushing commit and tag"
git push
git push origin "$NEW_VERSION"
print_success "Done. Pushed $NEW_VERSION"
+10
View File
@@ -4,6 +4,15 @@
#include "../resources/nostr_core_lib/nostr_core/nostr_common.h"
/*
* Canonical version is the latest git tag.
* increment_and_push.sh updates these macros in-place.
*/
#define NT_VERSION_MAJOR 0
#define NT_VERSION_MINOR 0
#define NT_VERSION_PATCH 1
#define NT_VERSION "v0.0.1"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -96,6 +105,7 @@ int main(int argc, char *argv[]) {
tui_init();
tui_set_window_title("Nostr Terminal");
state_init();
snprintf(g_state.version, sizeof(g_state.version), "%s", NT_VERSION);
if (nostr_init() != 0) {
fprintf(stderr, "Failed to initialize nostr library.\n");
+10 -34
View File
@@ -2,58 +2,32 @@
#include "state.h"
#include "net.h"
#include "publish.h"
#include "../resources/nostr_core_lib/cjson/cJSON.h"
#include "../resources/nostr_core_lib/nostr_core/nip001.h"
#include "../resources/nostr_core_lib/nostr_core/nip019.h"
#include "../resources/nostr_core_lib/nostr_core/utils.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
static int follows_publish_kind3(cJSON *event_obj) {
unsigned char priv[32];
cJSON *tags;
cJSON *content;
cJSON *new_event;
char *event_json;
const char **write_relays;
int relay_count = 0;
int ok_count;
if (!event_obj || nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
if (!event_obj) {
return -1;
}
tags = cJSON_Duplicate(cJSON_GetObjectItemCaseSensitive(event_obj, "tags"), 1);
if (!tags) {
tags = cJSON_CreateArray();
}
tags = cJSON_GetObjectItemCaseSensitive(event_obj, "tags");
content = cJSON_GetObjectItemCaseSensitive(event_obj, "content");
new_event = nostr_create_and_sign_event(3,
return publish_signed_event_with_report("Follows",
3,
(cJSON_IsString(content) && content->valuestring) ? content->valuestring : "",
tags,
priv,
time(NULL));
cJSON_Delete(tags);
if (!new_event) {
return -1;
}
event_json = cJSON_PrintUnformatted(new_event);
cJSON_Delete(new_event);
if (!event_json) {
return -1;
}
write_relays = state_get_write_relays(&relay_count);
ok_count = nt_publish(event_json, write_relays, relay_count, 8000);
free(event_json);
return ok_count;
8000);
}
void menu_follows(void) {
@@ -282,7 +256,9 @@ void menu_follows(void) {
free(g_state.kind3_json);
g_state.kind3_json = new_json;
}
tui_print("Posted KIND 3 to %d relay(s).", posted);
if (posted < 0) {
tui_print("Follows publish failed.");
}
tui_get_line(">", input, (int)sizeof(input));
break;
} else if (input[0] == 'x' || input[0] == 'X') {
+51 -45
View File
@@ -2,8 +2,9 @@
#include "state.h"
#include "net.h"
#include "publish.h"
#include "../resources/nostr_core_lib/cjson/cJSON.h"
#include "../resources/nostr_core_lib/nostr_core/nip001.h"
#include "../resources/nostr_core_lib/nostr_core/nip006.h"
#include "../resources/nostr_core_lib/nostr_core/nip019.h"
#include "../resources/nostr_core_lib/nostr_core/utils.h"
@@ -16,20 +17,38 @@
#define NT_DEV_SEED_PHRASE "cube dirt movie learn depth axis ball view aunt electric finish release"
static char *menu_strdup_local(const char *s) {
size_t n;
char *out;
static int menu_load_test_mnemonic(char *out, int out_size) {
FILE *fp;
if (!s) {
return NULL;
if (!out || out_size <= 1) {
return -1;
}
n = strlen(s);
out = (char *)malloc(n + 1U);
if (!out) {
return NULL;
out[0] = '\0';
fp = fopen(".test_mnemonic", "r");
if (!fp) {
fp = fopen("../.test_mnemonic", "r");
}
memcpy(out, s, n + 1U);
return out;
if (!fp) {
return -1;
}
if (!fgets(out, out_size, fp)) {
fclose(fp);
return -1;
}
fclose(fp);
{
size_t n = strlen(out);
while (n > 0 && (out[n - 1] == '\n' || out[n - 1] == '\r' || out[n - 1] == ' ' || out[n - 1] == '\t')) {
out[n - 1] = '\0';
n--;
}
}
return out[0] == '\0' ? -1 : 0;
}
static int menu_set_keys_from_private_bytes(const unsigned char *private_key, const char *seed_phrase_opt) {
@@ -67,37 +86,7 @@ static int menu_set_keys_from_private_bytes(const unsigned char *private_key, co
}
static int menu_publish_signed_event(int kind, const char *content, cJSON *tags) {
unsigned char priv[32];
cJSON *event;
char *event_json;
const char **write_relays;
int relay_count = 0;
int ok_count;
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
return -1;
}
event = nostr_create_and_sign_event(kind,
content ? content : "",
tags,
priv,
time(NULL));
if (!event) {
return -1;
}
event_json = cJSON_PrintUnformatted(event);
cJSON_Delete(event);
if (!event_json) {
return -1;
}
write_relays = state_get_write_relays(&relay_count);
ok_count = nt_publish(event_json, write_relays, relay_count, 8000);
free(event_json);
return ok_count;
return publish_signed_event_with_report("Account setup", kind, content ? content : "", tags, 8000);
}
static void menu_add_new_user(void) {
@@ -150,13 +139,19 @@ static void menu_add_new_user(void) {
cJSON_AddItemToArray(tags10002, tag);
}
(void)menu_publish_signed_event(0, g_state.kind0_json ? g_state.kind0_json : "{}", cJSON_CreateArray());
{
cJSON *empty_tags = cJSON_CreateArray();
(void)menu_publish_signed_event(0, g_state.kind0_json ? g_state.kind0_json : "{}", empty_tags);
cJSON_Delete(empty_tags);
}
(void)menu_publish_signed_event(10002, "", tags10002);
cJSON_Delete(tags10002);
}
void menu_login(void) {
char input[512];
char pending_test_mnemonic[512] = {0};
int pending_test_mnemonic_ready = 0;
while (1) {
unsigned char priv[32];
@@ -219,7 +214,18 @@ void menu_login(void) {
}
if (input[0] == '\0') {
snprintf(input, sizeof(input), "%s", NT_DEV_SEED_PHRASE);
if (pending_test_mnemonic_ready) {
snprintf(input, sizeof(input), "%s", pending_test_mnemonic);
pending_test_mnemonic_ready = 0;
} else if (menu_load_test_mnemonic(pending_test_mnemonic, (int)sizeof(pending_test_mnemonic)) == 0) {
pending_test_mnemonic_ready = 1;
tui_print("Loaded .test_mnemonic. Press Enter again to use it.");
continue;
} else {
snprintf(input, sizeof(input), "%s", NT_DEV_SEED_PHRASE);
}
} else {
pending_test_mnemonic_ready = 0;
}
if (strlen(input) == 64) {
+8 -35
View File
@@ -2,46 +2,16 @@
#include "state.h"
#include "net.h"
#include "publish.h"
#include "../resources/nostr_core_lib/cjson/cJSON.h"
#include "../resources/nostr_core_lib/nostr_core/nip001.h"
#include "../resources/nostr_core_lib/nostr_core/utils.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
static int profile_publish_kind0(const char *content_json) {
unsigned char priv[32];
cJSON *event;
cJSON *tags;
char *event_json;
const char **write_relays;
int relay_count = 0;
int ok_count;
if (!content_json || nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
return -1;
}
tags = cJSON_CreateArray();
event = nostr_create_and_sign_event(0, content_json, tags, priv, time(NULL));
cJSON_Delete(tags);
if (!event) {
return -1;
}
event_json = cJSON_PrintUnformatted(event);
cJSON_Delete(event);
if (!event_json) {
return -1;
}
write_relays = state_get_write_relays(&relay_count);
ok_count = nt_publish(event_json, write_relays, relay_count, 8000);
free(event_json);
return ok_count;
return publish_signed_event_with_report("Profile", 0, content_json ? content_json : "{}", NULL, 8000);
}
void menu_profile(void) {
@@ -88,7 +58,8 @@ void menu_profile(void) {
snprintf(prompt, sizeof(prompt), "%-20s >", fields[i]);
tui_get_line(prompt, edit_buf, (int)sizeof(edit_buf));
if (edit_buf[0] != '\0') {
cJSON_ReplaceItemInObjectCaseSensitive(meta, fields[i], cJSON_CreateString(edit_buf));
cJSON_DeleteItemFromObjectCaseSensitive(meta, fields[i]);
cJSON_AddStringToObject(meta, fields[i], edit_buf);
} else if (!cJSON_GetObjectItemCaseSensitive(meta, fields[i])) {
cJSON_AddStringToObject(meta, fields[i], cur);
}
@@ -113,7 +84,9 @@ void menu_profile(void) {
if (menu_sel[0] == 'p' || menu_sel[0] == 'P') {
int posted = profile_publish_kind0(g_state.kind0_json ? g_state.kind0_json : "{}");
tui_print("Posted KIND 0 to %d relay(s).", posted);
if (posted < 0) {
tui_print("Profile publish failed.");
}
cJSON_Delete(meta);
tui_get_line(">", edit_buf, (int)sizeof(edit_buf));
break;
+11 -37
View File
@@ -2,57 +2,29 @@
#include "state.h"
#include "net.h"
#include "../resources/nostr_core_lib/cjson/cJSON.h"
#include "../resources/nostr_core_lib/nostr_core/nip001.h"
#include "../resources/nostr_core_lib/nostr_core/utils.h"
#include "publish.h"
#include "../resources/nostr_core_lib/cjson/cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
static int relays_publish_kind10002(cJSON *event_obj) {
unsigned char priv[32];
cJSON *tags;
cJSON *content;
cJSON *new_event;
char *event_json;
const char **write_relays;
int relay_count = 0;
int ok_count;
if (!event_obj || nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
if (!event_obj) {
return -1;
}
tags = cJSON_Duplicate(cJSON_GetObjectItemCaseSensitive(event_obj, "tags"), 1);
if (!tags) {
tags = cJSON_CreateArray();
}
tags = cJSON_GetObjectItemCaseSensitive(event_obj, "tags");
content = cJSON_GetObjectItemCaseSensitive(event_obj, "content");
new_event = nostr_create_and_sign_event(10002,
return publish_signed_event_with_report("Relay list",
10002,
(cJSON_IsString(content) && content->valuestring) ? content->valuestring : "",
tags,
priv,
time(NULL));
cJSON_Delete(tags);
if (!new_event) {
return -1;
}
event_json = cJSON_PrintUnformatted(new_event);
cJSON_Delete(new_event);
if (!event_json) {
return -1;
}
write_relays = state_get_write_relays(&relay_count);
ok_count = nt_publish(event_json, write_relays, relay_count, 8000);
free(event_json);
return ok_count;
8000);
}
void menu_relays(void) {
@@ -212,7 +184,9 @@ void menu_relays(void) {
g_state.kind10002_json = new_json;
state_parse_relay_list();
}
tui_print("Posted KIND 10002 to %d relay(s).", posted);
if (posted < 0) {
tui_print("Relay list publish failed.");
}
tui_get_line(">", input, (int)sizeof(input));
break;
} else if (input[0] == 'x' || input[0] == 'X') {
+5 -40
View File
@@ -2,24 +2,14 @@
#include "state.h"
#include "net.h"
#include "../resources/nostr_core_lib/cjson/cJSON.h"
#include "../resources/nostr_core_lib/nostr_core/nip001.h"
#include "../resources/nostr_core_lib/nostr_core/utils.h"
#include "publish.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
void menu_tweet(void) {
char text[4096];
unsigned char priv[32];
cJSON *tags;
cJSON *event;
char *event_json;
const char **write_relays;
int relay_count = 0;
int ok_count;
int posted;
tui_clear_screen();
tui_print("TWEET\n");
@@ -29,34 +19,9 @@ void menu_tweet(void) {
return;
}
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
tui_print("Invalid private key in state.");
tui_get_line(">", text, (int)sizeof(text));
return;
posted = publish_signed_event_with_report("Tweet", 1, text, NULL, 8000);
if (posted < 0) {
tui_print("Tweet publish failed.");
}
tags = cJSON_CreateArray();
event = nostr_create_and_sign_event(1, text, tags, priv, time(NULL));
cJSON_Delete(tags);
if (!event) {
tui_print("Failed to create event.");
tui_get_line(">", text, (int)sizeof(text));
return;
}
event_json = cJSON_PrintUnformatted(event);
cJSON_Delete(event);
if (!event_json) {
tui_print("Failed to serialize event.");
tui_get_line(">", text, (int)sizeof(text));
return;
}
write_relays = state_get_write_relays(&relay_count);
ok_count = nt_publish(event_json, write_relays, relay_count, 8000);
free(event_json);
tui_print("Tweet posted to %d relay(s).", ok_count);
tui_get_line(">", text, (int)sizeof(text));
}
+185 -73
View File
@@ -3,7 +3,6 @@
#include "db.h"
#include "../resources/nostr_core_lib/cjson/cJSON.h"
#include "../resources/nostr_core_lib/nostr_core/nostr_http.h"
#include "../resources/nostr_core_lib/nostr_websocket/nostr_websocket_tls.h"
#include <stdio.h>
@@ -202,23 +201,30 @@ static int nt_wait_publish_ok(nostr_ws_client_t *client,
}
/* Publish one event to each relay: connect -> EVENT -> OK -> close. */
int nt_publish(const char *event_json,
const char **relay_urls,
int relay_count,
int timeout_ms) {
int nt_publish_detailed(const char *event_json,
const char **relay_urls,
int relay_count,
int timeout_ms,
nt_publish_result_t **results_out,
int *result_count_out) {
cJSON *event = NULL;
char *event_id = NULL;
nt_publish_result_t *results = NULL;
int result_count = 0;
int accepted_count = 0;
int i;
if (!event_json || !relay_urls || relay_count <= 0) {
fprintf(stderr, "nt_publish: invalid arguments\n");
if (!event_json || !relay_urls || relay_count <= 0 || !results_out || !result_count_out) {
fprintf(stderr, "nt_publish_detailed: invalid arguments\n");
return 0;
}
*results_out = NULL;
*result_count_out = 0;
event = cJSON_Parse(event_json);
if (!event || !cJSON_IsObject(event)) {
fprintf(stderr, "nt_publish: invalid event JSON\n");
fprintf(stderr, "nt_publish_detailed: invalid event JSON\n");
cJSON_Delete(event);
return 0;
}
@@ -227,6 +233,8 @@ int nt_publish(const char *event_json,
for (i = 0; i < relay_count; i++) {
const char *relay = relay_urls[i];
nt_publish_result_t *next;
nt_publish_result_t *slot;
nostr_ws_client_t *client;
int posted = 0;
char *response = NULL;
@@ -235,18 +243,37 @@ int nt_publish(const char *event_json,
continue;
}
next = (nt_publish_result_t *)realloc(results, (size_t)(result_count + 1) * sizeof(nt_publish_result_t));
if (!next) {
nt_publish_results_free(results, result_count);
free(event_id);
cJSON_Delete(event);
return 0;
}
results = next;
slot = &results[result_count];
slot->relay_url = nt_strdup_local(relay);
slot->posted = 0;
slot->response = NULL;
result_count++;
client = nostr_ws_connect(relay);
if (!client) {
fprintf(stderr, "nt_publish: connect failed for %s\n", relay);
db_log_relay_post(relay, event_id, 0, "connect_failed");
response = nt_strdup_local("connect_failed");
db_log_relay_post(relay, event_id, 0, response ? response : "connect_failed");
slot->posted = 0;
slot->response = response;
continue;
}
(void)nostr_ws_set_timeout(client, timeout_ms > 0 ? timeout_ms : 5000);
if (nostr_relay_send_event(client, event) != NOSTR_WS_SUCCESS) {
fprintf(stderr, "nt_publish: send EVENT failed for %s\n", relay);
db_log_relay_post(relay, event_id, 0, "send_failed");
response = nt_strdup_local("send_failed");
db_log_relay_post(relay, event_id, 0, response ? response : "send_failed");
slot->posted = 0;
slot->response = response;
nostr_ws_close(client);
continue;
}
@@ -260,17 +287,53 @@ int nt_publish(const char *event_json,
accepted_count++;
}
db_log_relay_post(relay, event_id, posted, response);
db_log_relay_post(relay, event_id, posted, response ? response : "no_response");
slot->posted = posted;
slot->response = response;
free(response);
nostr_ws_close(client);
}
free(event_id);
cJSON_Delete(event);
*results_out = results;
*result_count_out = result_count;
return accepted_count;
}
int nt_publish(const char *event_json,
const char **relay_urls,
int relay_count,
int timeout_ms) {
nt_publish_result_t *results = NULL;
int result_count = 0;
int accepted_count = nt_publish_detailed(event_json,
relay_urls,
relay_count,
timeout_ms,
&results,
&result_count);
nt_publish_results_free(results, result_count);
return accepted_count;
}
void nt_publish_results_free(nt_publish_result_t *results, int result_count) {
int i;
if (!results || result_count <= 0) {
free(results);
return;
}
for (i = 0; i < result_count; i++) {
free(results[i].relay_url);
free(results[i].response);
}
free(results);
}
/* Query one relay and append deduplicated EVENT payloads until EOSE/timeout. */
static int nt_query_one_relay(const char *relay_url,
cJSON *filter,
@@ -280,22 +343,31 @@ static int nt_query_one_relay(const char *relay_url,
int *event_cap,
char ***seen_ids,
int *seen_count,
int *seen_cap) {
int *seen_cap,
const char **reason_out) {
nostr_ws_client_t *client;
long long deadline_ms;
char buffer[NT_WS_BUFFER_SIZE];
int relay_success = 0;
if (reason_out) {
*reason_out = "unknown";
}
client = nostr_ws_connect(relay_url);
if (!client) {
fprintf(stderr, "nt_query_sync: connect failed for %s\n", relay_url);
if (reason_out) {
*reason_out = "connect_failed";
}
return 0;
}
(void)nostr_ws_set_timeout(client, timeout_ms > 0 ? timeout_ms : 5000);
if (nostr_relay_send_req(client, NT_SUB_ID, filter) != NOSTR_WS_SUCCESS) {
fprintf(stderr, "nt_query_sync: REQ send failed for %s\n", relay_url);
if (reason_out) {
*reason_out = "send_req_failed";
}
nostr_ws_close(client);
return 0;
}
@@ -335,14 +407,23 @@ static int nt_query_one_relay(const char *relay_url,
cJSON_Delete(parsed);
(void)nostr_relay_send_close(client, NT_SUB_ID);
nostr_ws_close(client);
if (reason_out) {
*reason_out = "memory_error";
}
return -1;
}
}
relay_success = 1;
if (reason_out) {
*reason_out = "event_received";
}
}
}
} else if (message_type && strcmp(message_type, "EOSE") == 0) {
relay_success = 1;
if (reason_out && *reason_out && strcmp(*reason_out, "event_received") != 0) {
*reason_out = "eose";
}
free(message_type);
cJSON_Delete(parsed);
break;
@@ -352,18 +433,24 @@ static int nt_query_one_relay(const char *relay_url,
cJSON_Delete(parsed);
}
if (!relay_success && reason_out && *reason_out && strcmp(*reason_out, "unknown") == 0) {
*reason_out = "timeout_or_no_messages";
}
(void)nostr_relay_send_close(client, NT_SUB_ID);
nostr_ws_close(client);
return relay_success;
}
/* Query relays synchronously and collect unique events by id. */
int nt_query_sync(const char *filter_json,
const char **relay_urls,
int relay_count,
int timeout_ms,
char ***events_out,
int *event_count) {
int nt_query_sync_verbose(const char *filter_json,
const char **relay_urls,
int relay_count,
int timeout_ms,
const char *phase_label,
int verbose,
char ***events_out,
int *event_count) {
cJSON *filter = NULL;
char **events = NULL;
char **seen_ids = NULL;
@@ -375,7 +462,7 @@ int nt_query_sync(const char *filter_json,
int i;
if (!events_out || !event_count || !filter_json || !relay_urls || relay_count <= 0) {
fprintf(stderr, "nt_query_sync: invalid arguments\n");
fprintf(stderr, "query: invalid arguments\n");
return -1;
}
@@ -384,18 +471,23 @@ int nt_query_sync(const char *filter_json,
filter = cJSON_Parse(filter_json);
if (!filter) {
fprintf(stderr, "nt_query_sync: invalid filter JSON\n");
fprintf(stderr, "query: invalid filter JSON\n");
return -1;
}
for (i = 0; i < relay_count; i++) {
const char *relay = relay_urls[i];
int rc;
const char *reason = "unknown";
if (!relay || relay[0] == '\0') {
continue;
}
if (verbose && phase_label) {
fprintf(stderr, "%s: attempting relay %d/%d %s\n", phase_label, i + 1, relay_count, relay);
}
rc = nt_query_one_relay(relay,
filter,
timeout_ms,
@@ -404,15 +496,25 @@ int nt_query_sync(const char *filter_json,
&events_cap,
&seen_ids,
&seen_n,
&seen_cap);
&seen_cap,
&reason);
if (rc < 0) {
if (verbose && phase_label) {
fprintf(stderr, "%s: failed %s (%s)\n", phase_label, relay, reason ? reason : "error");
}
nt_free_string_array(events, events_n);
nt_free_string_array(seen_ids, seen_n);
cJSON_Delete(filter);
return -1;
}
if (rc > 0) {
success_relays++;
if (verbose && phase_label) {
fprintf(stderr, "%s: connected %s (%s)\n", phase_label, relay, reason ? reason : "ok");
}
} else if (verbose && phase_label) {
fprintf(stderr, "%s: failed %s (%s)\n", phase_label, relay, reason ? reason : "failed");
}
}
@@ -429,12 +531,30 @@ int nt_query_sync(const char *filter_json,
return 0;
}
int nt_query_sync(const char *filter_json,
const char **relay_urls,
int relay_count,
int timeout_ms,
char ***events_out,
int *event_count) {
return nt_query_sync_verbose(filter_json,
relay_urls,
relay_count,
timeout_ms,
NULL,
0,
events_out,
event_count);
}
/* Query and return only the newest event found (or NULL if none). */
int nt_get_first(const char *filter_json,
const char **relay_urls,
int relay_count,
int timeout_ms,
char **event_out) {
int nt_get_first_verbose(const char *filter_json,
const char **relay_urls,
int relay_count,
int timeout_ms,
const char *phase_label,
int verbose,
char **event_out) {
char **events = NULL;
int count = 0;
int rc;
@@ -447,7 +567,14 @@ int nt_get_first(const char *filter_json,
}
*event_out = NULL;
rc = nt_query_sync(filter_json, relay_urls, relay_count, timeout_ms, &events, &count);
rc = nt_query_sync_verbose(filter_json,
relay_urls,
relay_count,
timeout_ms,
phase_label,
verbose,
&events,
&count);
if (rc != 0) {
return -1;
}
@@ -488,16 +615,28 @@ int nt_get_first(const char *filter_json,
return 0;
}
/* Fetch NIP-11 relay information JSON with proper Accept header. */
int nt_get_first(const char *filter_json,
const char **relay_urls,
int relay_count,
int timeout_ms,
char **event_out) {
return nt_get_first_verbose(filter_json,
relay_urls,
relay_count,
timeout_ms,
NULL,
0,
event_out);
}
/*
* Fetch relay NIP-11 information.
*
* For MUSL static builds, nt intentionally avoids libcurl to keep link closure
* small and deterministic. NIP-11 fetch is therefore disabled here.
*/
int nt_fetch_nip11(const char *relay_url, char **info_json_out) {
char *http_url = NULL;
nostr_http_request_t req;
nostr_http_response_t resp;
const char *headers[] = {
"Accept: application/nostr+json",
NULL
};
int rc;
char *normalized_url;
if (!relay_url || !info_json_out) {
fprintf(stderr, "nt_fetch_nip11: invalid arguments\n");
@@ -506,40 +645,13 @@ int nt_fetch_nip11(const char *relay_url, char **info_json_out) {
*info_json_out = NULL;
http_url = nt_ws_to_http_url(relay_url);
if (!http_url) {
normalized_url = nt_ws_to_http_url(relay_url);
if (!normalized_url) {
fprintf(stderr, "nt_fetch_nip11: unsupported relay URL: %s\n", relay_url);
return -1;
}
memset(&req, 0, sizeof(req));
req.method = "GET";
req.url = http_url;
req.headers = headers;
req.timeout_seconds = 10;
req.follow_redirects = 1;
req.max_redirects = 5;
req.capture_headers = 0;
rc = nostr_http_request(&req, &resp);
free(http_url);
if (rc != NOSTR_SUCCESS) {
fprintf(stderr, "nt_fetch_nip11: HTTP request failed\n");
return -1;
}
if (resp.status_code < 200 || resp.status_code >= 300 || !resp.body) {
fprintf(stderr, "nt_fetch_nip11: unexpected HTTP status %ld\n", resp.status_code);
nostr_http_response_free(&resp);
return -1;
}
*info_json_out = nt_strdup_local(resp.body);
nostr_http_response_free(&resp);
if (!*info_json_out) {
return -1;
}
return 0;
free(normalized_url);
fprintf(stderr, "nt_fetch_nip11: disabled (no HTTP client linked in static build)\n");
return -1;
}
+96
View File
@@ -0,0 +1,96 @@
#include "publish.h"
#include "net.h"
#include "state.h"
#include "tui.h"
#include "../resources/nostr_core_lib/nostr_core/nip001.h"
#include "../resources/nostr_core_lib/nostr_core/utils.h"
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int publish_signed_event_with_report(const char *label,
int kind,
const char *content,
cJSON *tags,
int timeout_ms) {
unsigned char priv[32];
cJSON *tags_copy = NULL;
cJSON *event = NULL;
char *event_json = NULL;
const char **write_relays;
int relay_count = 0;
nt_publish_result_t *results = NULL;
int result_count = 0;
int accepted_count = 0;
int i;
if (!label || label[0] == '\0') {
label = "Event";
}
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
tui_print("%s: invalid private key in state.", label);
return -1;
}
tags_copy = tags ? cJSON_Duplicate(tags, 1) : cJSON_CreateArray();
if (!tags_copy) {
tui_print("%s: failed to prepare tags.", label);
return -1;
}
event = nostr_create_and_sign_event(kind,
content ? content : "",
tags_copy,
priv,
time(NULL));
cJSON_Delete(tags_copy);
if (!event) {
tui_print("%s: failed to create signed event.", label);
return -1;
}
event_json = cJSON_PrintUnformatted(event);
cJSON_Delete(event);
if (!event_json) {
tui_print("%s: failed to serialize event.", label);
return -1;
}
write_relays = state_get_write_relays(&relay_count);
tui_print("%s (kind %d)", label, kind);
tui_print("Posting to %d relay(s)...", relay_count);
accepted_count = nt_publish_detailed(event_json,
write_relays,
relay_count,
timeout_ms,
&results,
&result_count);
free(event_json);
if (result_count <= 0) {
tui_print("No relay attempts were made.");
return accepted_count;
}
for (i = 0; i < result_count; i++) {
const char *relay = results[i].relay_url ? results[i].relay_url : "(unknown relay)";
const char *response = results[i].response ? results[i].response : "(no response)";
if (results[i].posted) {
tui_print("[OK] %s", relay);
} else {
tui_print("[FAIL] %s", relay);
tui_print(" %s", response);
}
}
tui_print("Accepted by %d/%d relay(s).", accepted_count, result_count);
nt_publish_results_free(results, result_count);
return accepted_count;
}
+183 -28
View File
@@ -108,10 +108,12 @@ void state_init(void) {
snprintf(g_state.version, sizeof(g_state.version), "%s", "0.5.8");
g_state.bootstrap_relays[0] = "ws://127.0.0.1:7777";
g_state.bootstrap_relays[1] = "wss://nostr.mom";
g_state.bootstrap_relays[2] = "wss://nos.lol";
g_state.bootstrap_relay_count = 3;
g_state.bootstrap_relays[0] = "wss://127.0.0.1:7777";
g_state.bootstrap_relays[1] = "wss://127.0.0.1:8888";
g_state.bootstrap_relays[2] = "wss://relay.laantungir.net";
g_state.bootstrap_relays[3] = "wss://relay.primal.net";
g_state.bootstrap_relays[4] = "wss://relay.damus.io";
g_state.bootstrap_relay_count = 5;
snprintf(g_state.user_name, sizeof(g_state.user_name), "%s", "not logged in");
}
@@ -126,6 +128,16 @@ void state_cleanup(void) {
free(g_state.kind10002_json);
g_state.kind10002_json = NULL;
free(g_state.kind10096_json);
g_state.kind10096_json = NULL;
free(g_state.kind17375_json);
g_state.kind17375_json = NULL;
state_free_string_array(g_state.kind30078_events, g_state.kind30078_count);
g_state.kind30078_events = NULL;
g_state.kind30078_count = 0;
state_free_string_array(g_state.read_relays, g_state.read_relay_count);
g_state.read_relays = NULL;
g_state.read_relay_count = 0;
@@ -220,13 +232,97 @@ void state_parse_relay_list(void) {
}
}
static long long state_event_created_at(const char *event_json) {
cJSON *ev;
cJSON *created;
long long out = 0;
if (!event_json) {
return 0;
}
ev = cJSON_Parse(event_json);
if (!ev) {
return 0;
}
created = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
if (cJSON_IsNumber(created)) {
out = (long long)created->valuedouble;
}
cJSON_Delete(ev);
return out;
}
static int state_pick_latest_json(char **slot, long long *slot_created, const char *candidate_json) {
long long created_at;
char *copy;
if (!slot || !slot_created || !candidate_json) {
return -1;
}
created_at = state_event_created_at(candidate_json);
if (*slot && created_at <= *slot_created) {
return 0;
}
copy = state_strdup_local(candidate_json);
if (!copy) {
return -1;
}
free(*slot);
*slot = copy;
*slot_created = created_at;
return 0;
}
static int state_push_event_copy(char ***arr, int *count, const char *event_json) {
char **next;
char *copy;
if (!arr || !count || !event_json) {
return -1;
}
copy = state_strdup_local(event_json);
if (!copy) {
return -1;
}
next = (char **)realloc(*arr, (size_t)(*count + 1) * sizeof(char *));
if (!next) {
free(copy);
return -1;
}
*arr = next;
(*arr)[*count] = copy;
(*count)++;
return 0;
}
int state_load_user_info(void) {
char filter[512];
char filter[768];
char *event_json = NULL;
const char **relays = NULL;
int relay_count = 0;
cJSON *meta = NULL;
cJSON *name = NULL;
char **events = NULL;
int event_count = 0;
int i;
char *kind0_event = NULL;
char *kind3_event = NULL;
char *kind10096_event = NULL;
char *kind17375_event = NULL;
long long kind0_created = -1;
long long kind3_created = -1;
long long kind10096_created = -1;
long long kind17375_created = -1;
if (!g_state.logged_in || g_state.npub_hex[0] == '\0') {
fprintf(stderr, "state_load_user_info: user is not logged in\n");
@@ -239,53 +335,112 @@ int state_load_user_info(void) {
g_state.kind3_json = NULL;
free(g_state.kind10002_json);
g_state.kind10002_json = NULL;
free(g_state.kind10096_json);
g_state.kind10096_json = NULL;
free(g_state.kind17375_json);
g_state.kind17375_json = NULL;
state_free_string_array(g_state.kind30078_events, g_state.kind30078_count);
g_state.kind30078_events = NULL;
g_state.kind30078_count = 0;
fprintf(stderr, "Attempting to fetch kind 10002 relays from bootstrap relays\n");
snprintf(filter,
sizeof(filter),
"{\"authors\":[\"%s\"],\"kinds\":[10002],\"limit\":1}",
g_state.npub_hex);
if (nt_get_first(filter,
g_state.bootstrap_relays,
g_state.bootstrap_relay_count,
6000,
&event_json) == 0 && event_json) {
if (nt_get_first_verbose(filter,
g_state.bootstrap_relays,
g_state.bootstrap_relay_count,
6000,
"Phase 1 (bootstrap 10002)",
1,
&event_json) == 0 && event_json) {
g_state.kind10002_json = event_json;
event_json = NULL;
}
state_parse_relay_list();
relays = state_get_read_relays(&relay_count);
fprintf(stderr, "Attempting to fetch kinds [0,3,10096,17375,30078] from user relays\n");
snprintf(filter,
sizeof(filter),
"{\"authors\":[\"%s\"],\"kinds\":[0],\"limit\":1}",
"{\"authors\":[\"%s\"],\"kinds\":[0,3,10096,17375,30078],\"limit\":200}",
g_state.npub_hex);
if (nt_get_first(filter, relays, relay_count, 6000, &event_json) == 0 && event_json) {
g_state.kind0_json = state_extract_event_content_json(event_json);
if (!g_state.kind0_json) {
g_state.kind0_json = state_strdup_local("{}");
if (nt_query_sync_verbose(filter,
relays,
relay_count,
7000,
"Phase 2 (user relays)",
1,
&events,
&event_count) == 0 && events && event_count > 0) {
for (i = 0; i < event_count; i++) {
cJSON *ev = cJSON_Parse(events[i]);
cJSON *kind_item;
int kind;
if (!ev) {
continue;
}
kind_item = cJSON_GetObjectItemCaseSensitive(ev, "kind");
if (!cJSON_IsNumber(kind_item)) {
cJSON_Delete(ev);
continue;
}
kind = (int)kind_item->valuedouble;
if (kind == 0) {
(void)state_pick_latest_json(&kind0_event, &kind0_created, events[i]);
} else if (kind == 3) {
(void)state_pick_latest_json(&kind3_event, &kind3_created, events[i]);
} else if (kind == 10096) {
(void)state_pick_latest_json(&kind10096_event, &kind10096_created, events[i]);
} else if (kind == 17375) {
(void)state_pick_latest_json(&kind17375_event, &kind17375_created, events[i]);
} else if (kind == 30078) {
(void)state_push_event_copy(&g_state.kind30078_events, &g_state.kind30078_count, events[i]);
}
cJSON_Delete(ev);
}
free(event_json);
event_json = NULL;
} else {
}
if (kind0_event) {
g_state.kind0_json = state_extract_event_content_json(kind0_event);
}
if (!g_state.kind0_json) {
g_state.kind0_json = state_strdup_local("{}");
}
snprintf(filter,
sizeof(filter),
"{\"authors\":[\"%s\"],\"kinds\":[3],\"limit\":1}",
g_state.npub_hex);
if (nt_get_first(filter, relays, relay_count, 6000, &event_json) == 0 && event_json) {
g_state.kind3_json = event_json;
event_json = NULL;
} else {
if (kind3_event) {
g_state.kind3_json = state_strdup_local(kind3_event);
}
if (!g_state.kind3_json) {
g_state.kind3_json = state_strdup_local("{\"kind\":3,\"content\":\"\",\"tags\":[]}");
}
g_state.kind10096_json = kind10096_event ? state_strdup_local(kind10096_event) : state_strdup_local("{}");
g_state.kind17375_json = kind17375_event ? state_strdup_local(kind17375_event) : state_strdup_local("{}");
state_free_string_array(events, event_count);
free(kind0_event);
free(kind3_event);
free(kind10096_event);
free(kind17375_event);
fprintf(stderr,
"Load summary: 10002=%s 0=%s 3=%s 10096=%s 17375=%s 30078=%d\n",
g_state.kind10002_json ? "ok" : "missing",
(g_state.kind0_json && strcmp(g_state.kind0_json, "{}") != 0) ? "ok" : "missing",
(g_state.kind3_json && strstr(g_state.kind3_json, "\"tags\"") != NULL) ? "ok" : "missing",
(g_state.kind10096_json && strcmp(g_state.kind10096_json, "{}") != 0) ? "ok" : "missing",
(g_state.kind17375_json && strcmp(g_state.kind17375_json, "{}") != 0) ? "ok" : "missing",
g_state.kind30078_count);
snprintf(g_state.user_name, sizeof(g_state.user_name), "%s", "not logged in");
if (g_state.kind0_json) {
meta = cJSON_Parse(g_state.kind0_json);