v0.0.1 - Unify publish reporting, improve login test mnemonic flow, and fix profile edit persistence
This commit is contained in:
+3
-2
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
|
||||
Executable
+147
@@ -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
@@ -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
@@ -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') {
|
||||
|
||||
+50
-44
@@ -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,8 +214,19 @@ void menu_login(void) {
|
||||
}
|
||||
|
||||
if (input[0] == '\0') {
|
||||
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) {
|
||||
int all_hex = 1;
|
||||
|
||||
+8
-35
@@ -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
@@ -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
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
int nt_publish_detailed(const char *event_json,
|
||||
const char **relay_urls,
|
||||
int relay_count,
|
||||
int timeout_ms) {
|
||||
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,16 +433,22 @@ 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,
|
||||
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;
|
||||
@@ -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,11 +531,29 @@ int nt_query_sync(const char *filter_json,
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Query and return only the newest event found (or NULL if none). */
|
||||
int nt_get_first(const char *filter_json,
|
||||
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_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;
|
||||
@@ -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");
|
||||
free(normalized_url);
|
||||
fprintf(stderr, "nt_fetch_nip11: disabled (no HTTP client linked in static build)\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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+179
-24
@@ -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,
|
||||
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 (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);
|
||||
}
|
||||
}
|
||||
|
||||
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("{}");
|
||||
}
|
||||
free(event_json);
|
||||
event_json = NULL;
|
||||
} else {
|
||||
g_state.kind0_json = state_strdup_local("{}");
|
||||
|
||||
if (kind3_event) {
|
||||
g_state.kind3_json = state_strdup_local(kind3_event);
|
||||
}
|
||||
|
||||
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 (!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);
|
||||
|
||||
Reference in New Issue
Block a user