From 67467c179e9e053356c698cad1ea4776906fbb56 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Fri, 1 May 2026 13:31:20 -0400 Subject: [PATCH] v0.0.1 - Unify publish reporting, improve login test mnemonic flow, and fix profile edit persistence --- CMakeLists.txt | 5 +- build.sh | 49 +++++++- include/net.h | 55 ++++++++- include/publish.h | 16 +++ include/state.h | 4 + increment_and_push.sh | 147 ++++++++++++++++++++++++ src/main.c | 10 ++ src/menu_follows.c | 44 ++----- src/menu_login.c | 96 ++++++++-------- src/menu_profile.c | 43 ++----- src/menu_relays.c | 48 ++------ src/menu_tweet.c | 45 +------- src/net.c | 258 ++++++++++++++++++++++++++++++------------ src/publish.c | 96 ++++++++++++++++ src/state.c | 211 +++++++++++++++++++++++++++++----- 15 files changed, 824 insertions(+), 303 deletions(-) mode change 100644 => 100755 build.sh create mode 100644 include/publish.h create mode 100755 increment_and_push.sh create mode 100644 src/publish.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a5186c..0fdfcaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 ) diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 index 3de4650..0751e61 --- a/build.sh +++ b/build.sh @@ -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" diff --git a/include/net.h b/include/net.h index 7b581d9..d345395 100644 --- a/include/net.h +++ b/include/net.h @@ -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); diff --git a/include/publish.h b/include/publish.h new file mode 100644 index 0000000..55f3c93 --- /dev/null +++ b/include/publish.h @@ -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 */ diff --git a/include/state.h b/include/state.h index 2338e65..2791577 100644 --- a/include/state.h +++ b/include/state.h @@ -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; diff --git a/increment_and_push.sh b/increment_and_push.sh new file mode 100755 index 0000000..4b80ef8 --- /dev/null +++ b/increment_and_push.sh @@ -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" diff --git a/src/main.c b/src/main.c index 297830f..f7f34d0 100644 --- a/src/main.c +++ b/src/main.c @@ -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 #include #include @@ -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"); diff --git a/src/menu_follows.c b/src/menu_follows.c index c94892d..c777c5b 100644 --- a/src/menu_follows.c +++ b/src/menu_follows.c @@ -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 #include #include -#include 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') { diff --git a/src/menu_login.c b/src/menu_login.c index 4cdf70f..98d72de 100644 --- a/src/menu_login.c +++ b/src/menu_login.c @@ -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) { diff --git a/src/menu_profile.c b/src/menu_profile.c index 4e6e2a6..f12c26d 100644 --- a/src/menu_profile.c +++ b/src/menu_profile.c @@ -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 #include #include -#include 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; diff --git a/src/menu_relays.c b/src/menu_relays.c index 79f046b..172e256 100644 --- a/src/menu_relays.c +++ b/src/menu_relays.c @@ -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 #include #include -#include 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') { diff --git a/src/menu_tweet.c b/src/menu_tweet.c index b14f834..a9d927b 100644 --- a/src/menu_tweet.c +++ b/src/menu_tweet.c @@ -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 -#include #include -#include 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)); } diff --git a/src/net.c b/src/net.c index 9f869f3..c850f01 100644 --- a/src/net.c +++ b/src/net.c @@ -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 @@ -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; } diff --git a/src/publish.c b/src/publish.c new file mode 100644 index 0000000..1ccec11 --- /dev/null +++ b/src/publish.c @@ -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 +#include +#include + +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; +} diff --git a/src/state.c b/src/state.c index 37cbcf3..6d6c9fa 100644 --- a/src/state.c +++ b/src/state.c @@ -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);