Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6221f019b | ||
|
|
1630a872b1 | ||
|
|
e8de95a949 | ||
|
|
cdf90295a2 | ||
|
|
ca1c95c41c | ||
|
|
be27b088bf | ||
|
|
9b446d854a |
@@ -1 +1,13 @@
|
||||
alarm impact educate burden vague honey horn buyer sight vocal age render
|
||||
alarm impact educate burden vague honey horn buyer sight vocal age render
|
||||
|
||||
index 0
|
||||
|
||||
{
|
||||
"index": 0,
|
||||
"nsec": "nsec1z2lrfamae2dzax7dmnlhv497uuxe4mw0m3w694upx5x54q6dgttqvzrrwl",
|
||||
"npub": "npub1j7d7yf47w8k2kseknqjr3045jvm00u0wnt3433kk6vu67d2zamcs8ynuw4",
|
||||
"npubHex": "979be226be71ecab4336982438beb49336f7f1ee9ae358c6d6d339af3542eef1",
|
||||
"nsecHex": "12be34f77dca9a2e9bcddcff7654bee70d9aedcfdc5da2d781350d4a834d42d6",
|
||||
"fipsIpv6": "fd55:b7c6:536e:26ee:a79:6e25:6f05:a85f",
|
||||
"strDerivationPath": "m/44'/1237'/0'/0/0"
|
||||
}
|
||||
@@ -7,6 +7,9 @@ set(CMAKE_C_EXTENSIONS OFF)
|
||||
|
||||
add_compile_options(-Wall -Wextra -D_GNU_SOURCE)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(LIBCURL REQUIRED libcurl)
|
||||
|
||||
# nt application sources
|
||||
file(GLOB NT_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.c")
|
||||
|
||||
@@ -42,9 +45,24 @@ target_include_directories(nt PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/resources/nostr_core_lib/cjson
|
||||
)
|
||||
|
||||
if(CMAKE_EXE_LINKER_FLAGS MATCHES "(^| )-static( |$)")
|
||||
target_include_directories(nt PRIVATE ${LIBCURL_STATIC_INCLUDE_DIRS})
|
||||
target_link_directories(nt PRIVATE ${LIBCURL_STATIC_LIBRARY_DIRS})
|
||||
target_compile_options(nt PRIVATE ${LIBCURL_STATIC_CFLAGS_OTHER})
|
||||
target_link_options(nt PRIVATE ${LIBCURL_STATIC_LDFLAGS_OTHER})
|
||||
set(NT_CURL_LIBS ${LIBCURL_STATIC_LIBRARIES})
|
||||
else()
|
||||
target_include_directories(nt PRIVATE ${LIBCURL_INCLUDE_DIRS})
|
||||
target_link_directories(nt PRIVATE ${LIBCURL_LIBRARY_DIRS})
|
||||
target_compile_options(nt PRIVATE ${LIBCURL_CFLAGS_OTHER})
|
||||
target_link_options(nt PRIVATE ${LIBCURL_LDFLAGS_OTHER})
|
||||
set(NT_CURL_LIBS ${LIBCURL_LIBRARIES})
|
||||
endif()
|
||||
|
||||
target_link_libraries(nt PRIVATE
|
||||
nostr_core_lib
|
||||
sqlite3
|
||||
${NT_CURL_LIBS}
|
||||
secp256k1
|
||||
ssl
|
||||
crypto
|
||||
|
||||
170
build.sh
170
build.sh
@@ -1,46 +1,170 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# nt MUSL static build script (emulates c-relay style)
|
||||
# Produces build/nt_static_x86_64 on x86_64 hosts.
|
||||
# nt MUSL static build script
|
||||
# Produces build/nt_static_x86_64 or build/nt_static_arm64
|
||||
|
||||
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; }
|
||||
|
||||
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"
|
||||
TARGET_ARCH=""
|
||||
|
||||
show_usage() {
|
||||
echo "nt static builder"
|
||||
echo ""
|
||||
echo "USAGE:"
|
||||
echo " $0 [--arch <x86_64|arm64>]"
|
||||
echo ""
|
||||
echo "OPTIONS:"
|
||||
echo " --arch <arch> Target architecture: x86_64 or arm64"
|
||||
echo " -h, --help Show this help"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--arch)
|
||||
if [[ -z "$2" ]]; then
|
||||
print_error "--arch requires a value: x86_64 or arm64"
|
||||
exit 1
|
||||
fi
|
||||
case "$2" in
|
||||
x86_64|arm64)
|
||||
TARGET_ARCH="$2"
|
||||
;;
|
||||
*)
|
||||
print_error "Unsupported architecture '$2'"
|
||||
print_error "Supported values: x86_64, arm64"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
show_usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown argument '$1'"
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "[ERROR] docker is required"
|
||||
print_error "docker is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "[ERROR] docker daemon is not running"
|
||||
print_error "docker daemon is not running"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
HOST_UNAME="$(uname -m)"
|
||||
case "$HOST_UNAME" in
|
||||
x86_64)
|
||||
HOST_ARCH="x86_64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
HOST_ARCH="arm64"
|
||||
;;
|
||||
*)
|
||||
HOST_ARCH="unknown"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -z "$TARGET_ARCH" ]]; then
|
||||
if [[ "$HOST_ARCH" == "unknown" ]]; then
|
||||
TARGET_ARCH="x86_64"
|
||||
print_warning "Unknown host architecture '$HOST_UNAME'; defaulting target arch to x86_64"
|
||||
else
|
||||
TARGET_ARCH="$HOST_ARCH"
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$TARGET_ARCH" in
|
||||
x86_64)
|
||||
PLATFORM="linux/amd64"
|
||||
OUTPUT_NAME="nt_static_x86_64"
|
||||
;;
|
||||
arm64)
|
||||
PLATFORM="linux/arm64"
|
||||
OUTPUT_NAME="nt_static_arm64"
|
||||
;;
|
||||
*)
|
||||
print_error "Unsupported target architecture '$TARGET_ARCH'"
|
||||
print_error "Supported values: x86_64, arm64"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$HOST_ARCH" != "unknown" && "$HOST_ARCH" != "$TARGET_ARCH" ]]; then
|
||||
print_status "Cross-building from host '$HOST_ARCH' to target '$TARGET_ARCH'"
|
||||
if ! docker buildx version >/dev/null 2>&1; then
|
||||
print_error "docker buildx is required for cross-architecture builds"
|
||||
print_error "Install/enable buildx + binfmt and retry."
|
||||
echo "docker run --privileged --rm tonistiigi/binfmt --install arm64" >&2
|
||||
exit 1
|
||||
fi
|
||||
print_success "docker buildx is available"
|
||||
|
||||
if ! docker run --rm --platform "$PLATFORM" alpine:3.19 /bin/true >/dev/null 2>&1; then
|
||||
print_error "Cross-architecture execution is not available (binfmt/QEMU not configured)"
|
||||
print_error "Enable binfmt support and retry:"
|
||||
echo "docker run --privileged --rm tonistiigi/binfmt --install arm64" >&2
|
||||
exit 1
|
||||
fi
|
||||
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 \
|
||||
.
|
||||
print_status "Building MUSL static binary for $TARGET_ARCH ($PLATFORM)..."
|
||||
(
|
||||
cd "$SCRIPT_DIR"
|
||||
docker build \
|
||||
--platform "$PLATFORM" \
|
||||
-f Dockerfile.alpine-musl \
|
||||
-t nt-musl-output: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
|
||||
print_status "Extracting /nt_static from image..."
|
||||
CONTAINER_ID=$(docker create nt-musl-output:latest /nt_static)
|
||||
if ! docker cp "$CONTAINER_ID:/nt_static" "$BUILD_DIR/$OUTPUT_NAME"; then
|
||||
print_error "Could not copy /nt_static from output image"
|
||||
docker rm "$CONTAINER_ID" >/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
docker rm "$CONTAINER_ID" >/dev/null || true
|
||||
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
|
||||
print_status "Verifying binary with ldd/file"
|
||||
LDD_OK=false
|
||||
LDD_OUTPUT=$(ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1 || true)
|
||||
echo "$LDD_OUTPUT" >&2
|
||||
if echo "$LDD_OUTPUT" | grep -q "not a dynamic executable\|statically linked"; then
|
||||
LDD_OK=true
|
||||
fi
|
||||
|
||||
echo "[DONE] $BUILD_DIR/$OUTPUT_NAME"
|
||||
FILE_OUTPUT=$(file "$BUILD_DIR/$OUTPUT_NAME" 2>&1 || true)
|
||||
echo "$FILE_OUTPUT" >&2
|
||||
|
||||
if [[ "$LDD_OK" == true ]] || echo "$FILE_OUTPUT" | grep -q "statically linked"; then
|
||||
print_success "Binary verified as static: $BUILD_DIR/$OUTPUT_NAME"
|
||||
else
|
||||
print_warning "Could not conclusively verify static linking"
|
||||
fi
|
||||
|
||||
print_success "Done: $BUILD_DIR/$OUTPUT_NAME"
|
||||
|
||||
8
include/editor.h
Normal file
8
include/editor.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#ifndef EDITOR_H
|
||||
#define EDITOR_H
|
||||
|
||||
/* Launch editor, return malloc'd string with user content, or NULL on failure/empty.
|
||||
* If initial_content is non-NULL, it is pre-loaded into the editor buffer. */
|
||||
char *editor_launch(const char *initial_content);
|
||||
|
||||
#endif /* EDITOR_H */
|
||||
@@ -69,6 +69,8 @@ int nt_get_first(const char *filter_json,
|
||||
int timeout_ms,
|
||||
char **event_out);
|
||||
|
||||
typedef void (*nt_event_callback_t)(const char *event_json, void *userdata);
|
||||
|
||||
/*
|
||||
* Same as nt_get_first, but with optional user-facing progress output.
|
||||
*/
|
||||
@@ -80,6 +82,18 @@ int nt_get_first_verbose(const char *filter_json,
|
||||
int verbose,
|
||||
char **event_out);
|
||||
|
||||
/*
|
||||
* Blocking live subscription. Connects to relays, sends REQ with given filter,
|
||||
* and calls on_event for each incoming EVENT. Returns when user presses any key
|
||||
* or on timeout/error.
|
||||
* Returns number of events received, or -1 on total failure.
|
||||
*/
|
||||
int nt_live_feed(const char *filter_json,
|
||||
const char **relay_urls,
|
||||
int relay_count,
|
||||
nt_event_callback_t on_event,
|
||||
void *userdata);
|
||||
|
||||
/*
|
||||
* Fetch relay NIP-11 information document.
|
||||
*
|
||||
|
||||
124
include/nsigner_client.h
Normal file
124
include/nsigner_client.h
Normal file
@@ -0,0 +1,124 @@
|
||||
#ifndef NSIGNER_CLIENT_H
|
||||
#define NSIGNER_CLIENT_H
|
||||
|
||||
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
||||
|
||||
/* Error codes */
|
||||
#define NT_NSIGNER_OK 0
|
||||
#define NT_NSIGNER_E_IO -1
|
||||
#define NT_NSIGNER_E_DENIED -2
|
||||
#define NT_NSIGNER_E_UNAUTH -3
|
||||
#define NT_NSIGNER_E_PARSE -4
|
||||
#define NT_NSIGNER_E_ERROR -5
|
||||
|
||||
/*
|
||||
* Enumerate running nsigner instances by reading /proc/net/unix.
|
||||
* Returns 0 on success. Caller must free each name and the array.
|
||||
* Names are returned WITHOUT the leading '@'.
|
||||
*/
|
||||
int nsigner_list(char ***names_out, int *count_out);
|
||||
|
||||
/*
|
||||
* Connect to an nsigner abstract namespace socket.
|
||||
* Returns file descriptor >= 0 on success, -1 on failure.
|
||||
* The name should NOT include the leading '@'.
|
||||
*/
|
||||
int nsigner_connect(const char *name);
|
||||
|
||||
/*
|
||||
* Send a JSON-RPC request and receive the response.
|
||||
* Uses 4-byte big-endian length-prefixed framing.
|
||||
* read_timeout_ms: timeout for reading response (use 60000 for prompt-requiring methods).
|
||||
* Returns NT_NSIGNER_OK on success with response in *response_out (caller must cJSON_Delete).
|
||||
* On error returns one of the NT_NSIGNER_E_* codes.
|
||||
*/
|
||||
int nsigner_rpc(int fd, const cJSON *request, cJSON **response_out, int read_timeout_ms);
|
||||
|
||||
/*
|
||||
* High-level helpers (each opens a connection, sends RPC, closes).
|
||||
* socket_name: without '@' prefix.
|
||||
* role: the role selector string (e.g. "main").
|
||||
*/
|
||||
int nsigner_get_public_key(const char *socket_name, const char *role, char *out_hex_65);
|
||||
|
||||
int nsigner_sign_event(const char *socket_name, const char *role,
|
||||
const char *unsigned_event_json, char **signed_event_json_out);
|
||||
|
||||
int nsigner_nip04_encrypt(const char *socket_name, const char *role,
|
||||
const char *peer_pub_hex, const char *plaintext, char **cipher_out);
|
||||
|
||||
int nsigner_nip04_decrypt(const char *socket_name, const char *role,
|
||||
const char *peer_pub_hex, const char *ciphertext, char **plain_out);
|
||||
|
||||
int nsigner_nip44_encrypt(const char *socket_name, const char *role,
|
||||
const char *peer_pub_hex, const char *plaintext, char **cipher_out);
|
||||
|
||||
int nsigner_nip44_decrypt(const char *socket_name, const char *role,
|
||||
const char *peer_pub_hex, const char *ciphertext, char **plain_out);
|
||||
|
||||
/*
|
||||
* URL helpers for FIPS/TCP transport.
|
||||
* URL format: http://host:port (path/query ignored if present).
|
||||
*/
|
||||
int nsigner_get_public_key_url(const char *url, const char *role, char *out_hex_65);
|
||||
|
||||
int nsigner_sign_event_url(const char *url, const char *role,
|
||||
const char *unsigned_event_json, char **signed_event_json_out);
|
||||
|
||||
int nsigner_nip04_encrypt_url(const char *url, const char *role,
|
||||
const char *peer_pub_hex, const char *plaintext, char **cipher_out);
|
||||
|
||||
int nsigner_nip04_decrypt_url(const char *url, const char *role,
|
||||
const char *peer_pub_hex, const char *ciphertext, char **plain_out);
|
||||
|
||||
int nsigner_nip44_encrypt_url(const char *url, const char *role,
|
||||
const char *peer_pub_hex, const char *plaintext, char **cipher_out);
|
||||
|
||||
int nsigner_nip44_decrypt_url(const char *url, const char *role,
|
||||
const char *peer_pub_hex, const char *ciphertext, char **plain_out);
|
||||
|
||||
/*
|
||||
* qrexec helpers for cross-qube n_signer transport.
|
||||
* target_qube: destination signer qube (e.g. "nsigner-vault").
|
||||
* service_name: qrexec service name (e.g. "qubes.NsignerRpc").
|
||||
*/
|
||||
int nsigner_qrexec_get_public_key(const char *target_qube,
|
||||
const char *service_name,
|
||||
const char *role,
|
||||
char *out_hex_65);
|
||||
|
||||
int nsigner_qrexec_sign_event(const char *target_qube,
|
||||
const char *service_name,
|
||||
const char *role,
|
||||
const char *unsigned_event_json,
|
||||
char **signed_event_json_out);
|
||||
|
||||
int nsigner_qrexec_nip04_encrypt(const char *target_qube,
|
||||
const char *service_name,
|
||||
const char *role,
|
||||
const char *peer_pub_hex,
|
||||
const char *plaintext,
|
||||
char **cipher_out);
|
||||
|
||||
int nsigner_qrexec_nip04_decrypt(const char *target_qube,
|
||||
const char *service_name,
|
||||
const char *role,
|
||||
const char *peer_pub_hex,
|
||||
const char *ciphertext,
|
||||
char **plain_out);
|
||||
|
||||
int nsigner_qrexec_nip44_encrypt(const char *target_qube,
|
||||
const char *service_name,
|
||||
const char *role,
|
||||
const char *peer_pub_hex,
|
||||
const char *plaintext,
|
||||
char **cipher_out);
|
||||
|
||||
int nsigner_qrexec_nip44_decrypt(const char *target_qube,
|
||||
const char *service_name,
|
||||
const char *role,
|
||||
const char *peer_pub_hex,
|
||||
const char *ciphertext,
|
||||
char **plain_out);
|
||||
|
||||
#endif /* NSIGNER_CLIENT_H */
|
||||
52
include/signer.h
Normal file
52
include/signer.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#ifndef SIGNER_H
|
||||
#define SIGNER_H
|
||||
|
||||
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
||||
|
||||
#include <time.h>
|
||||
|
||||
typedef enum {
|
||||
NT_SIGNER_LOCAL = 0,
|
||||
NT_SIGNER_NSIGNER = 1,
|
||||
NT_SIGNER_NSIGNER_QREXEC = 2,
|
||||
NT_SIGNER_NSIGNER_URL = 3
|
||||
} nt_signer_kind_t;
|
||||
|
||||
typedef struct {
|
||||
nt_signer_kind_t kind;
|
||||
char socket_name[128];
|
||||
char qrexec_target[128];
|
||||
char qrexec_service[128];
|
||||
char endpoint_url[256];
|
||||
char role[64];
|
||||
} nt_signer_t;
|
||||
|
||||
extern nt_signer_t g_signer;
|
||||
|
||||
void signer_init(void);
|
||||
int signer_init_local(void);
|
||||
int signer_init_nsigner(const char *socket_name);
|
||||
int signer_init_nsigner_qrexec(const char *target_qube, const char *service_name);
|
||||
int signer_init_nsigner_url(const char *endpoint_url);
|
||||
|
||||
int signer_create_and_sign(int kind,
|
||||
const char *content,
|
||||
cJSON *tags,
|
||||
time_t timestamp,
|
||||
char **signed_event_json_out);
|
||||
|
||||
int signer_nip04_encrypt(const char *peer_pub_hex, const char *plaintext, char **cipher_out);
|
||||
int signer_nip04_decrypt(const char *peer_pub_hex, const char *ciphertext, char **plain_out);
|
||||
|
||||
int signer_nip44_encrypt(const char *peer_pub_hex, const char *plaintext, char **cipher_out);
|
||||
int signer_nip44_decrypt(const char *peer_pub_hex, const char *ciphertext, char **plain_out);
|
||||
|
||||
int signer_nip44_self_encrypt(const char *plaintext, char **cipher_out);
|
||||
int signer_nip44_self_decrypt(const char *sender_pub_hex, const char *ciphertext, char **plain_out);
|
||||
|
||||
int signer_sign_event_json(const char *unsigned_event_json, char **signed_event_json_out);
|
||||
|
||||
/* Returns 0 on success. Only works in NT_SIGNER_LOCAL mode; returns -1 for NT_SIGNER_NSIGNER. */
|
||||
int signer_get_local_private_key(unsigned char out_priv[32]);
|
||||
|
||||
#endif /* SIGNER_H */
|
||||
@@ -16,8 +16,7 @@ typedef struct {
|
||||
char *kind10002_json;
|
||||
char *kind10096_json;
|
||||
char *kind17375_json;
|
||||
char **kind30078_events;
|
||||
int kind30078_count;
|
||||
char *user_settings_json;
|
||||
|
||||
const char *bootstrap_relays[8];
|
||||
int bootstrap_relay_count;
|
||||
@@ -39,4 +38,7 @@ void state_parse_relay_list(void);
|
||||
const char **state_get_read_relays(int *count);
|
||||
const char **state_get_write_relays(int *count);
|
||||
|
||||
int state_nip44_self_encrypt(const char *plaintext, char **cipher_out);
|
||||
char *state_nip44_self_decrypt(const char *sender_pub_hex, const char *ciphertext);
|
||||
|
||||
#endif /* STATE_H */
|
||||
|
||||
@@ -18,6 +18,7 @@ print_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
|
||||
|
||||
COMMIT_MESSAGE=""
|
||||
VERSION_INCREMENT_TYPE="patch" # patch|minor|major
|
||||
RELEASE_MODE=false
|
||||
|
||||
show_usage() {
|
||||
echo "nt increment and push"
|
||||
@@ -29,7 +30,21 @@ show_usage() {
|
||||
echo " -p, --patch Increment patch version (default)"
|
||||
echo " -m, --minor Increment minor version"
|
||||
echo " -M, --major Increment major version"
|
||||
echo " -r, --release After bump+push, build/upload release assets to Gitea"
|
||||
echo " -h, --help Show help"
|
||||
echo ""
|
||||
echo "EXAMPLES:"
|
||||
echo " $0 \"Fix relay reconnect bug\""
|
||||
echo " $0 -m \"Add profile editing\""
|
||||
echo " $0 -r -p \"Release patch build\""
|
||||
echo " $0 -r -m \"Release minor build\""
|
||||
echo ""
|
||||
echo "RELEASE MODE (-r):"
|
||||
echo " 1) Normal bump + commit + tag + push runs first"
|
||||
echo " 2) Build x86_64 static binary (required)"
|
||||
echo " 3) Build arm64 static binary (warning only on failure)"
|
||||
echo " 4) Create source tarball"
|
||||
echo " 5) Create/upload Gitea release assets if ~/.gitea_token exists"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@@ -46,6 +61,10 @@ while [[ $# -gt 0 ]]; do
|
||||
VERSION_INCREMENT_TYPE="major"
|
||||
shift
|
||||
;;
|
||||
-r|--release)
|
||||
RELEASE_MODE=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
show_usage
|
||||
exit 0
|
||||
@@ -71,77 +90,289 @@ if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
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
|
||||
get_and_increment_version() {
|
||||
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
|
||||
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
|
||||
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"
|
||||
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
|
||||
update_version_macros() {
|
||||
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_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_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 .
|
||||
git_commit_tag_push() {
|
||||
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 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
|
||||
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"
|
||||
git tag "$NEW_VERSION"
|
||||
print_success "Created tag: $NEW_VERSION"
|
||||
|
||||
print_status "Pushing commit and tag"
|
||||
git push
|
||||
git push origin "$NEW_VERSION"
|
||||
print_status "Pushing commit and tag"
|
||||
git push
|
||||
git push origin "$NEW_VERSION"
|
||||
|
||||
print_success "Done. Pushed $NEW_VERSION"
|
||||
print_success "Pushed $NEW_VERSION"
|
||||
}
|
||||
|
||||
build_release_binary() {
|
||||
print_status "Building release binaries"
|
||||
|
||||
if [[ ! -x "./build.sh" ]]; then
|
||||
print_error "./build.sh not found or not executable"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ./build.sh; then
|
||||
print_success "Built x86_64 static binary"
|
||||
else
|
||||
print_error "x86_64 build failed; aborting release mode"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ./build.sh --arch arm64; then
|
||||
print_success "Built arm64 static binary"
|
||||
else
|
||||
print_warning "arm64 build failed (continuing release with available assets)"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
create_source_tarball() {
|
||||
local tarball_name="nostr_terminal-${NEW_VERSION#v}.tar.gz"
|
||||
|
||||
print_status "Creating source tarball: $tarball_name"
|
||||
if tar -czf "$tarball_name" \
|
||||
--exclude='build/*' \
|
||||
--exclude='.git*' \
|
||||
--exclude='*.db' \
|
||||
--exclude='*.db-*' \
|
||||
--exclude='*.log' \
|
||||
--exclude='*.tar.gz' \
|
||||
.; then
|
||||
print_success "Created source tarball: $tarball_name"
|
||||
echo "$tarball_name"
|
||||
return 0
|
||||
else
|
||||
print_error "Failed to create source tarball"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
create_gitea_release() {
|
||||
if [[ ! -f "$HOME/.gitea_token" ]]; then
|
||||
print_warning "No $HOME/.gitea_token found. Skipping release creation/upload."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local token
|
||||
token=$(tr -d '\n\r' < "$HOME/.gitea_token")
|
||||
local api_url="https://git.laantungir.net/api/v1/repos/laantungir/nostr_terminal"
|
||||
|
||||
print_status "Creating Gitea release: $NEW_VERSION"
|
||||
local response
|
||||
response=$(curl -s -X POST "$api_url/releases" \
|
||||
-H "Authorization: token $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"$NEW_VERSION\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\"}")
|
||||
|
||||
if echo "$response" | grep -q '"id"'; then
|
||||
local release_id
|
||||
release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
|
||||
print_success "Created release $NEW_VERSION (id=$release_id)"
|
||||
echo "$release_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -qi "already exists"; then
|
||||
print_warning "Release $NEW_VERSION already exists; resolving release id"
|
||||
local existing
|
||||
existing=$(curl -s -H "Authorization: token $token" "$api_url/releases/tags/$NEW_VERSION")
|
||||
if echo "$existing" | grep -q '"id"'; then
|
||||
local release_id
|
||||
release_id=$(echo "$existing" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
|
||||
print_status "Using existing release id=$release_id"
|
||||
echo "$release_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
print_error "Could not resolve existing release ID for $NEW_VERSION"
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_error "Failed to create release"
|
||||
print_error "Response: $response"
|
||||
return 1
|
||||
}
|
||||
|
||||
upload_one_asset_with_retry() {
|
||||
local assets_url="$1"
|
||||
local token="$2"
|
||||
local file_path="$3"
|
||||
local max_attempts=3
|
||||
local attempt=1
|
||||
|
||||
while [[ $attempt -le $max_attempts ]]; do
|
||||
print_status "Uploading $(basename "$file_path") (attempt $attempt/$max_attempts)"
|
||||
local response
|
||||
response=$(curl -s -X POST "$assets_url" \
|
||||
-H "Authorization: token $token" \
|
||||
-F "attachment=@$file_path;filename=$(basename "$file_path")")
|
||||
|
||||
if echo "$response" | grep -q '"id"'; then
|
||||
print_success "Uploaded $(basename "$file_path")"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ $attempt -lt $max_attempts ]]; then
|
||||
print_warning "Upload attempt failed for $(basename "$file_path"), retrying..."
|
||||
sleep 2
|
||||
else
|
||||
print_error "Failed to upload $(basename "$file_path") after $max_attempts attempts"
|
||||
print_error "Response: $response"
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
upload_release_assets() {
|
||||
local release_id="$1"
|
||||
local tarball_path="$2"
|
||||
|
||||
if [[ ! -f "$HOME/.gitea_token" ]]; then
|
||||
print_warning "No $HOME/.gitea_token found. Skipping asset uploads."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local token
|
||||
token=$(tr -d '\n\r' < "$HOME/.gitea_token")
|
||||
local api_url="https://git.laantungir.net/api/v1/repos/laantungir/nostr_terminal"
|
||||
local assets_url="$api_url/releases/$release_id/assets"
|
||||
|
||||
if [[ -f "build/nt_static_x86_64" ]]; then
|
||||
upload_one_asset_with_retry "$assets_url" "$token" "build/nt_static_x86_64" || true
|
||||
else
|
||||
print_warning "Asset missing: build/nt_static_x86_64"
|
||||
fi
|
||||
|
||||
if [[ -f "build/nt_static_arm64" ]]; then
|
||||
upload_one_asset_with_retry "$assets_url" "$token" "build/nt_static_arm64" || true
|
||||
else
|
||||
print_warning "Asset missing: build/nt_static_arm64 (skipping)"
|
||||
fi
|
||||
|
||||
if [[ -n "$tarball_path" && -f "$tarball_path" ]]; then
|
||||
print_status "Uploading $(basename "$tarball_path")"
|
||||
local response
|
||||
response=$(curl -s -X POST "$assets_url" \
|
||||
-H "Authorization: token $token" \
|
||||
-F "attachment=@$tarball_path;filename=$(basename "$tarball_path")")
|
||||
|
||||
if echo "$response" | grep -q '"id"'; then
|
||||
print_success "Uploaded $(basename "$tarball_path")"
|
||||
else
|
||||
print_warning "Failed to upload $(basename "$tarball_path"): $response"
|
||||
fi
|
||||
else
|
||||
print_warning "Tarball missing, skipping upload"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
get_and_increment_version
|
||||
update_version_macros
|
||||
git_commit_tag_push
|
||||
|
||||
if [[ "$RELEASE_MODE" == true ]]; then
|
||||
print_status "Release mode enabled"
|
||||
|
||||
if ! build_release_binary; then
|
||||
print_error "Release mode aborted due to required x86_64 build failure"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local tarball_path=""
|
||||
if tarball_path=$(create_source_tarball); then
|
||||
:
|
||||
else
|
||||
print_warning "Proceeding without source tarball"
|
||||
fi
|
||||
|
||||
local release_id=""
|
||||
if release_id=$(create_gitea_release); then
|
||||
if [[ "$release_id" =~ ^[0-9]+$ ]]; then
|
||||
upload_release_assets "$release_id" "$tarball_path"
|
||||
print_success "Release workflow finished for $NEW_VERSION"
|
||||
else
|
||||
print_warning "Release creation/upload skipped (no release id)"
|
||||
fi
|
||||
else
|
||||
print_error "Release creation failed"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
print_success "Done. Pushed $NEW_VERSION"
|
||||
}
|
||||
|
||||
main
|
||||
|
||||
@@ -363,37 +363,57 @@ Based on [`nostr_core_lib/todo.md`](resources/nostr_core_lib/todo.md), the follo
|
||||
## Implementation Roadmap
|
||||
|
||||
### Phase 1: Foundation (P0 Infrastructure)
|
||||
- [ ] Set up CMake build with linkage to nostr_core_lib (static lib)
|
||||
- [ ] Implement TUI layer (`tui.c/h`) with ncurses
|
||||
- [ ] Implement SQLite layer (`db.c/h`) with full schema
|
||||
- [ ] Implement event loop (`loop.c/h`)
|
||||
- [ ] Implement HTTP wrapper (libcurl)
|
||||
- [x] Set up CMake build with linkage to nostr_core_lib (static lib) — see [`CMakeLists.txt`](CMakeLists.txt)
|
||||
- [x] Implement TUI layer with ncurses — see [`include/tui.h`](include/tui.h) and [`src/tui.c`](src/tui.c) (raw VT100/termios, not ncurses, but functionally equivalent)
|
||||
- [x] Implement SQLite layer (`db.c/h`) with partial schema — see [`include/db.h`](include/db.h) and [`src/db.c`](src/db.c) (events / relays / people / local / relay_posts present; full original schema split into regular/replacable/addressable/ephemeral still pending)
|
||||
- [x] Replace event-loop concept with synchronous per-action network helpers — see [`include/net.h`](include/net.h) (nt_publish, nt_query_sync, nt_get_first all implemented)
|
||||
- [ ] Implement HTTP wrapper (libcurl) — [`nt_fetch_nip11()`](include/net.h:89) is currently a stub returning -1; needed for NIP-11, nostr.watch, Venice/Ollama
|
||||
|
||||
### Phase 2: Core Nostr Client (P0 Menus)
|
||||
- [ ] [L]ogin (mnemonic + bunker)
|
||||
- [ ] [P]rofile
|
||||
- [ ] [R]elays
|
||||
- [ ] [T]weet
|
||||
- [ ] [F]ollows
|
||||
- [ ] [Q]uit
|
||||
- [x] [L]ogin (mnemonic) — see [`src/menu_login.c`](src/menu_login.c) (existing-account path: seed phrase / nsec / nsecHex / npub; new-account path; loads KIND 0/3/10002/10096/17375/30078)
|
||||
- [ ] [L]ogin bunker (NIP-46) sub-path — not yet wired
|
||||
- [x] [P]rofile (view + modify + publish KIND 0) — see [`src/menu_profile.c`](src/menu_profile.c)
|
||||
- [x] [R]elays (list / add / delete / modify / publish KIND 10002) — see [`src/menu_relays.c`](src/menu_relays.c) (NIP-11 fetch is stubbed; nostr.watch sync and kind-1 acceptance test pending)
|
||||
- [x] [T]weet (sign + publish KIND 1) — see [`src/menu_tweet.c`](src/menu_tweet.c) (no `vipe` integration yet)
|
||||
- [x] [F]ollows (list / add / delete / publish KIND 3) — see [`src/menu_follows.c`](src/menu_follows.c) (Profile sub-view from follows list pending)
|
||||
- [x] [Q]uit
|
||||
- [x] [K]inds debug dump — implemented in [`src/main.c`](src/main.c:26)
|
||||
|
||||
### Phase 3: Reading & Interaction (P1 Menus)
|
||||
- [ ] Li[v]e Feeds
|
||||
- [ ] [N]otifications
|
||||
- [ ] [W]rite (with vipe integration)
|
||||
- [ ] P[o]sts
|
||||
- [ ] Direct [M]essage (NIP-17)
|
||||
- [ ] Li[v]e Feeds — needs a long-running `nt_live_feed()` helper added to [`src/net.c`](src/net.c)
|
||||
- [ ] [N]otifications — query events with `#p` tag, group/sort
|
||||
- [ ] [W]rite (with vipe integration) — needs external editor wrapper
|
||||
- [ ] P[o]sts (browse / edit user notes by kind, NIP-04 decrypt for 30078)
|
||||
- [ ] Direct [M]essage (NIP-17 send/receive, NIP-04 receive for legacy)
|
||||
|
||||
### Phase 4: Advanced Features (P2 Menus)
|
||||
- [ ] To[d]o
|
||||
- [ ] D[i]ary
|
||||
- [ ] [E]cash wallet
|
||||
- [ ] [A]I integration
|
||||
- [ ] To[d]o (encrypted KIND 30078, d=todo)
|
||||
- [ ] D[i]ary (KIND 30024 daily entries via vipe)
|
||||
- [ ] [E]cash wallet (NIP-60/61, Cashu mint client, QR rendering)
|
||||
- [ ] [A]I integration (Venice/Ollama via libcurl, prefs in KIND 30078)
|
||||
|
||||
### Phase 5: Optional/Experimental (P3 Menus)
|
||||
- [ ] [C]ircus
|
||||
- [ ] Bl[o]ssom
|
||||
- [ ] [DB] maintenance
|
||||
- [ ] [C]ircus (multi-account derivation + AI-driven posts)
|
||||
- [ ] Bl[o]ssom (KIND 10096 manage media servers)
|
||||
- [ ] [DB] maintenance (bulk relay + people + notes refresh)
|
||||
- [ ] [B]rowser launcher
|
||||
- [ ] [K]inds debug
|
||||
- [ ] HTTPS/WSS companion server
|
||||
|
||||
---
|
||||
|
||||
## Current Status Snapshot (auto-summary)
|
||||
|
||||
**Done — MVP usable:** Build, TUI, SQLite scaffold, synchronous net helpers, login (seed-based), profile, relays (without NIP-11/test), tweet, follows, kinds dump, quit.
|
||||
|
||||
**Immediate gaps blocking the rest of P0:**
|
||||
1. **HTTP client** — [`nt_fetch_nip11()`](src/net.c) is a stub. Without it, the relay menu can't show NIP-11 info, can't query nostr.watch, and the AI menu can't talk to Venice/Ollama. This is the single biggest unblocker.
|
||||
2. **Relay acceptance test** — small wrapper around `nt_publish` to publish a throwaway kind-1 to a single relay with a tight timeout; needed for the `R`elay menu's test feature.
|
||||
3. **NIP-46 bunker login** — alternate auth path inside [`src/menu_login.c`](src/menu_login.c).
|
||||
|
||||
**Next P1 tickets in recommended order:**
|
||||
1. **`nt_live_feed(filters, relays, on_event_cb)`** in [`src/net.c`](src/net.c) — blocking subscription that exits on any keypress; foundation for both Live Feeds and Notifications menus.
|
||||
2. **External editor wrapper** (`fork`/`exec`/`pipe` to `vipe`, return captured stdout) — unlocks `W`rite, `D`iary, and richer `T`weet.
|
||||
3. **[N]otifications menu** — straight `nt_query_sync` over `#p` filter on read relays, then group/sort.
|
||||
4. **P[o]sts menu** — query addressable kinds (30023/30024/30078); NIP-04 decrypt for 30078.
|
||||
5. **Li[v]e Feeds menu** — built on `nt_live_feed`; sub-options for follows/firehose/mentions.
|
||||
6. **Direct [M]essage** — NIP-17 send via existing `nip017.h`, plus inbox query.
|
||||
|
||||
194
src/editor.c
Normal file
194
src/editor.c
Normal file
@@ -0,0 +1,194 @@
|
||||
#include "editor.h"
|
||||
|
||||
#include "tui.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int editor_write_all(int fd, const char *data, size_t len) {
|
||||
size_t off = 0;
|
||||
|
||||
while (off < len) {
|
||||
ssize_t n = write(fd, data + off, len - off);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
off += (size_t)n;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char *editor_read_file_malloc(const char *path) {
|
||||
FILE *fp;
|
||||
long sz;
|
||||
char *buf;
|
||||
size_t nread;
|
||||
|
||||
if (!path) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
fp = fopen(path, "rb");
|
||||
if (!fp) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (fseek(fp, 0, SEEK_END) != 0) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sz = ftell(fp);
|
||||
if (sz <= 0) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (fseek(fp, 0, SEEK_SET) != 0) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
buf = (char *)malloc((size_t)sz + 1U);
|
||||
if (!buf) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
nread = fread(buf, 1U, (size_t)sz, fp);
|
||||
fclose(fp);
|
||||
if (nread != (size_t)sz) {
|
||||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
buf[nread] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
static int editor_run_command(const char *tmp_path, const char *editor_env) {
|
||||
pid_t pid;
|
||||
int status = 0;
|
||||
|
||||
if (!tmp_path) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
tui_cleanup();
|
||||
|
||||
pid = fork();
|
||||
if (pid < 0) {
|
||||
tui_init();
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
if (editor_env && editor_env[0] != '\0') {
|
||||
char command[2048];
|
||||
char *argv_sh[4];
|
||||
|
||||
if (snprintf(command, sizeof(command), "%s \"%s\"", editor_env, tmp_path) >= (int)sizeof(command)) {
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
argv_sh[0] = "sh";
|
||||
argv_sh[1] = "-c";
|
||||
argv_sh[2] = command;
|
||||
argv_sh[3] = NULL;
|
||||
execvp("sh", argv_sh);
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
{
|
||||
char command[4096];
|
||||
char *argv_sh[4];
|
||||
|
||||
if (snprintf(command,
|
||||
sizeof(command),
|
||||
"vipe < \"%s\" > \"%s.out\" && mv \"%s.out\" \"%s\"",
|
||||
tmp_path,
|
||||
tmp_path,
|
||||
tmp_path,
|
||||
tmp_path) >= (int)sizeof(command)) {
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
argv_sh[0] = "sh";
|
||||
argv_sh[1] = "-c";
|
||||
argv_sh[2] = command;
|
||||
argv_sh[3] = NULL;
|
||||
execvp("sh", argv_sh);
|
||||
}
|
||||
|
||||
{
|
||||
char *argv_vi[3];
|
||||
argv_vi[0] = "vi";
|
||||
argv_vi[1] = (char *)tmp_path;
|
||||
argv_vi[2] = NULL;
|
||||
execvp("vi", argv_vi);
|
||||
}
|
||||
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
while (waitpid(pid, &status, 0) < 0) {
|
||||
if (errno != EINTR) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tui_init();
|
||||
|
||||
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
char *editor_launch(const char *initial_content) {
|
||||
char template_path[] = "/tmp/nt_editor_XXXXXX";
|
||||
int fd;
|
||||
char *out = NULL;
|
||||
const char *editor_env;
|
||||
|
||||
fd = mkstemp(template_path);
|
||||
if (fd < 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (initial_content && initial_content[0] != '\0') {
|
||||
size_t len = strlen(initial_content);
|
||||
if (editor_write_all(fd, initial_content, len) != 0) {
|
||||
close(fd);
|
||||
unlink(template_path);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (close(fd) != 0) {
|
||||
unlink(template_path);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
editor_env = getenv("EDITOR");
|
||||
if (editor_run_command(template_path, editor_env) != 0) {
|
||||
unlink(template_path);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out = editor_read_file_malloc(template_path);
|
||||
unlink(template_path);
|
||||
return out;
|
||||
}
|
||||
106
src/main.c
106
src/main.c
@@ -10,8 +10,8 @@
|
||||
*/
|
||||
#define NT_VERSION_MAJOR 0
|
||||
#define NT_VERSION_MINOR 0
|
||||
#define NT_VERSION_PATCH 1
|
||||
#define NT_VERSION "v0.0.1"
|
||||
#define NT_VERSION_PATCH 8
|
||||
#define NT_VERSION "v0.0.8"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -22,6 +22,50 @@ void menu_profile(void);
|
||||
void menu_relays(void);
|
||||
void menu_tweet(void);
|
||||
void menu_follows(void);
|
||||
void menu_write(void);
|
||||
void menu_notifications(void);
|
||||
void menu_posts(void);
|
||||
void menu_live(void);
|
||||
void menu_dm(void);
|
||||
void menu_todo(void);
|
||||
void menu_diary(void);
|
||||
void menu_ai(void);
|
||||
void menu_ecash(void);
|
||||
|
||||
static void menu_show_loaded_events(void) {
|
||||
tui_clear_screen();
|
||||
tui_print("LOADED EVENTS\n");
|
||||
|
||||
tui_print("KIND 0 (metadata content):");
|
||||
tui_print("%s", g_state.kind0_json ? g_state.kind0_json : "(not loaded)");
|
||||
tui_print("");
|
||||
|
||||
tui_print("KIND 3 (follows event):");
|
||||
tui_print("%s", g_state.kind3_json ? g_state.kind3_json : "(not loaded)");
|
||||
tui_print("");
|
||||
|
||||
tui_print("KIND 10002 (relay list event):");
|
||||
tui_print("%s", g_state.kind10002_json ? g_state.kind10002_json : "(not loaded)");
|
||||
tui_print("");
|
||||
|
||||
tui_print("KIND 10096 (blossom event):");
|
||||
tui_print("%s", g_state.kind10096_json ? g_state.kind10096_json : "(not loaded)");
|
||||
tui_print("");
|
||||
|
||||
tui_print("KIND 17375 (wallet event):");
|
||||
tui_print("%s", g_state.kind17375_json ? g_state.kind17375_json : "(not loaded)");
|
||||
tui_print("");
|
||||
|
||||
tui_print("USER-SETTINGS (kind 30078, d=user-settings):");
|
||||
tui_print("%s", g_state.user_settings_json ? g_state.user_settings_json : "(not loaded)");
|
||||
tui_print("");
|
||||
|
||||
tui_print("\nPress Enter to return.");
|
||||
{
|
||||
char input[8];
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
}
|
||||
}
|
||||
|
||||
void menu_main(void) {
|
||||
char input[64];
|
||||
@@ -39,6 +83,15 @@ void menu_main(void) {
|
||||
tui_print("^_P^:rofile");
|
||||
tui_print("^_R^:elays");
|
||||
tui_print("^_F^:ollows");
|
||||
tui_print("^_K^:ind/event dump");
|
||||
tui_print("^_N^:otifications");
|
||||
tui_print("^_B^:logs/posts");
|
||||
tui_print("^_L^:ive feeds");
|
||||
tui_print("^_M^:essage");
|
||||
tui_print("To^_d^:o");
|
||||
tui_print("^_J^:ournal");
|
||||
tui_print("^_A^:i");
|
||||
tui_print("^_E^:cash");
|
||||
tui_print("^_Q^:uit.");
|
||||
}
|
||||
|
||||
@@ -57,14 +110,9 @@ void menu_main(void) {
|
||||
}
|
||||
|
||||
switch (input[0]) {
|
||||
case 'l':
|
||||
case 'L':
|
||||
menu_login();
|
||||
break;
|
||||
case 'w':
|
||||
case 'W':
|
||||
tui_print("WRITE placeholder");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
menu_write();
|
||||
break;
|
||||
case 't':
|
||||
case 'T':
|
||||
@@ -82,6 +130,48 @@ void menu_main(void) {
|
||||
case 'F':
|
||||
menu_follows();
|
||||
break;
|
||||
case 'n':
|
||||
case 'N':
|
||||
menu_notifications();
|
||||
break;
|
||||
case 'k':
|
||||
case 'K':
|
||||
menu_show_loaded_events();
|
||||
break;
|
||||
case 'b':
|
||||
case 'B':
|
||||
case 'o':
|
||||
case 'O':
|
||||
menu_posts();
|
||||
break;
|
||||
case 'l':
|
||||
case 'L':
|
||||
case 'v':
|
||||
case 'V':
|
||||
menu_live();
|
||||
break;
|
||||
case 'm':
|
||||
case 'M':
|
||||
menu_dm();
|
||||
break;
|
||||
case 'd':
|
||||
case 'D':
|
||||
menu_todo();
|
||||
break;
|
||||
case 'j':
|
||||
case 'J':
|
||||
case 'i':
|
||||
case 'I':
|
||||
menu_diary();
|
||||
break;
|
||||
case 'a':
|
||||
case 'A':
|
||||
menu_ai();
|
||||
break;
|
||||
case 'e':
|
||||
case 'E':
|
||||
menu_ecash();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
658
src/menu_ai.c
Normal file
658
src/menu_ai.c
Normal file
@@ -0,0 +1,658 @@
|
||||
#include "tui.h"
|
||||
#include "state.h"
|
||||
#include "publish.h"
|
||||
|
||||
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
typedef struct {
|
||||
char *endpoint;
|
||||
char *api_key;
|
||||
char *model;
|
||||
} ai_prefs_t;
|
||||
|
||||
typedef struct {
|
||||
char *data;
|
||||
size_t len;
|
||||
} nt_http_buffer_t;
|
||||
|
||||
static char *ai_strdup(const char *s) {
|
||||
size_t n;
|
||||
char *out;
|
||||
|
||||
if (!s) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
n = strlen(s);
|
||||
out = (char *)malloc(n + 1U);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
memcpy(out, s, n + 1U);
|
||||
return out;
|
||||
}
|
||||
|
||||
static void ai_set_string(char **dst, const char *src) {
|
||||
char *next;
|
||||
|
||||
if (!dst) {
|
||||
return;
|
||||
}
|
||||
|
||||
next = ai_strdup(src ? src : "");
|
||||
if (!next) {
|
||||
return;
|
||||
}
|
||||
|
||||
free(*dst);
|
||||
*dst = next;
|
||||
}
|
||||
|
||||
static void ai_prefs_init(ai_prefs_t *prefs) {
|
||||
if (!prefs) {
|
||||
return;
|
||||
}
|
||||
|
||||
memset(prefs, 0, sizeof(*prefs));
|
||||
prefs->endpoint = ai_strdup("https://api.venice.ai/api/v1/chat/completions");
|
||||
prefs->api_key = ai_strdup("");
|
||||
prefs->model = ai_strdup("llama-3.3-70b");
|
||||
}
|
||||
|
||||
static void ai_prefs_free(ai_prefs_t *prefs) {
|
||||
if (!prefs) {
|
||||
return;
|
||||
}
|
||||
|
||||
free(prefs->endpoint);
|
||||
free(prefs->api_key);
|
||||
free(prefs->model);
|
||||
memset(prefs, 0, sizeof(*prefs));
|
||||
}
|
||||
|
||||
static int ai_ends_with_chat_completions(const char *s) {
|
||||
static const char *suffix = "/chat/completions";
|
||||
size_t s_len;
|
||||
size_t suffix_len;
|
||||
|
||||
if (!s) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
s_len = strlen(s);
|
||||
suffix_len = strlen(suffix);
|
||||
if (s_len < suffix_len) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return strcmp(s + (s_len - suffix_len), suffix) == 0;
|
||||
}
|
||||
|
||||
static char *ai_join_chat_completions(const char *base_url) {
|
||||
size_t base_len;
|
||||
int needs_slash;
|
||||
char *out;
|
||||
|
||||
if (!base_url || base_url[0] == '\0') {
|
||||
return ai_strdup("https://api.venice.ai/api/v1/chat/completions");
|
||||
}
|
||||
|
||||
if (ai_ends_with_chat_completions(base_url)) {
|
||||
return ai_strdup(base_url);
|
||||
}
|
||||
|
||||
base_len = strlen(base_url);
|
||||
needs_slash = (base_len > 0U && base_url[base_len - 1U] != '/');
|
||||
out = (char *)malloc(base_len + (needs_slash ? 1U : 0U) + strlen("chat/completions") + 1U);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
snprintf(out,
|
||||
base_len + (needs_slash ? 1U : 0U) + strlen("chat/completions") + 1U,
|
||||
"%s%schat/completions",
|
||||
base_url,
|
||||
needs_slash ? "/" : "");
|
||||
return out;
|
||||
}
|
||||
|
||||
static char *ai_base_url_from_endpoint(const char *endpoint) {
|
||||
static const char *suffix = "/chat/completions";
|
||||
size_t endpoint_len;
|
||||
size_t suffix_len;
|
||||
char *out;
|
||||
|
||||
if (!endpoint || endpoint[0] == '\0') {
|
||||
return ai_strdup("");
|
||||
}
|
||||
|
||||
endpoint_len = strlen(endpoint);
|
||||
suffix_len = strlen(suffix);
|
||||
|
||||
if (endpoint_len >= suffix_len &&
|
||||
strcmp(endpoint + (endpoint_len - suffix_len), suffix) == 0) {
|
||||
out = (char *)malloc(endpoint_len - suffix_len + 1U);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
memcpy(out, endpoint, endpoint_len - suffix_len);
|
||||
out[endpoint_len - suffix_len] = '\0';
|
||||
return out;
|
||||
}
|
||||
|
||||
return ai_strdup(endpoint);
|
||||
}
|
||||
|
||||
static cJSON *ai_make_d_tag(const char *dval) {
|
||||
cJSON *tags = cJSON_CreateArray();
|
||||
cJSON *d_tag;
|
||||
|
||||
if (!tags) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
d_tag = cJSON_CreateArray();
|
||||
if (!d_tag) {
|
||||
cJSON_Delete(tags);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(d_tag, cJSON_CreateString("d"));
|
||||
cJSON_AddItemToArray(d_tag, cJSON_CreateString(dval ? dval : ""));
|
||||
cJSON_AddItemToArray(tags, d_tag);
|
||||
return tags;
|
||||
}
|
||||
|
||||
static size_t nt_http_write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) {
|
||||
nt_http_buffer_t *buf = (nt_http_buffer_t *)userdata;
|
||||
size_t total = size * nmemb;
|
||||
char *next;
|
||||
|
||||
if (!buf || !ptr || total == 0U) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
next = (char *)realloc(buf->data, buf->len + total + 1U);
|
||||
if (!next) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
buf->data = next;
|
||||
memcpy(buf->data + buf->len, ptr, total);
|
||||
buf->len += total;
|
||||
buf->data[buf->len] = '\0';
|
||||
return total;
|
||||
}
|
||||
|
||||
static void nt_configure_curl_tls(CURL *curl) {
|
||||
const char *cafile_env;
|
||||
const char *capath_env;
|
||||
static const char *cafile_candidates[] = {
|
||||
"/etc/ssl/certs/ca-certificates.crt",
|
||||
"/etc/ssl/cert.pem",
|
||||
"/etc/pki/tls/certs/ca-bundle.crt",
|
||||
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"
|
||||
};
|
||||
int i;
|
||||
|
||||
if (!curl) {
|
||||
return;
|
||||
}
|
||||
|
||||
cafile_env = getenv("SSL_CERT_FILE");
|
||||
if (!cafile_env || cafile_env[0] == '\0') {
|
||||
cafile_env = getenv("CURL_CA_BUNDLE");
|
||||
}
|
||||
if (cafile_env && cafile_env[0] != '\0') {
|
||||
curl_easy_setopt(curl, CURLOPT_CAINFO, cafile_env);
|
||||
return;
|
||||
}
|
||||
|
||||
capath_env = getenv("SSL_CERT_DIR");
|
||||
if (capath_env && capath_env[0] != '\0') {
|
||||
curl_easy_setopt(curl, CURLOPT_CAPATH, capath_env);
|
||||
}
|
||||
|
||||
for (i = 0; i < (int)(sizeof(cafile_candidates) / sizeof(cafile_candidates[0])); i++) {
|
||||
if (access(cafile_candidates[i], R_OK) == 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_CAINFO, cafile_candidates[i]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (access("/etc/ssl/certs", R_OK) == 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_CAPATH, "/etc/ssl/certs");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static int ai_load_prefs(ai_prefs_t *prefs) {
|
||||
cJSON *root;
|
||||
cJSON *llm;
|
||||
cJSON *api_key;
|
||||
cJSON *model;
|
||||
cJSON *base_url;
|
||||
cJSON *legacy_endpoint;
|
||||
char *endpoint;
|
||||
|
||||
if (!prefs || !g_state.user_settings_json) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
root = cJSON_Parse(g_state.user_settings_json);
|
||||
if (!root || !cJSON_IsObject(root)) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
llm = cJSON_GetObjectItemCaseSensitive(root, "global_llm");
|
||||
if (!cJSON_IsObject(llm)) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
api_key = cJSON_GetObjectItemCaseSensitive(llm, "api_key");
|
||||
model = cJSON_GetObjectItemCaseSensitive(llm, "model");
|
||||
base_url = cJSON_GetObjectItemCaseSensitive(llm, "base_url");
|
||||
legacy_endpoint = cJSON_GetObjectItemCaseSensitive(llm, "endpoint");
|
||||
|
||||
if (cJSON_IsString(api_key) && api_key->valuestring) {
|
||||
ai_set_string(&prefs->api_key, api_key->valuestring);
|
||||
}
|
||||
if (cJSON_IsString(model) && model->valuestring) {
|
||||
ai_set_string(&prefs->model, model->valuestring);
|
||||
}
|
||||
|
||||
endpoint = NULL;
|
||||
if (cJSON_IsString(base_url) && base_url->valuestring) {
|
||||
endpoint = ai_join_chat_completions(base_url->valuestring);
|
||||
} else if (cJSON_IsString(legacy_endpoint) && legacy_endpoint->valuestring) {
|
||||
endpoint = ai_strdup(legacy_endpoint->valuestring);
|
||||
}
|
||||
if (endpoint) {
|
||||
ai_set_string(&prefs->endpoint, endpoint);
|
||||
free(endpoint);
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int ai_save_prefs(const ai_prefs_t *prefs) {
|
||||
cJSON *root;
|
||||
cJSON *llm;
|
||||
cJSON *tags;
|
||||
char *new_json;
|
||||
char *cipher = NULL;
|
||||
char *base_url = NULL;
|
||||
int posted;
|
||||
|
||||
if (!prefs) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
root = g_state.user_settings_json
|
||||
? cJSON_Parse(g_state.user_settings_json)
|
||||
: cJSON_CreateObject();
|
||||
if (!root || !cJSON_IsObject(root)) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!cJSON_GetObjectItemCaseSensitive(root, "v")) {
|
||||
cJSON_AddNumberToObject(root, "v", 2);
|
||||
}
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(root, "updatedAt");
|
||||
cJSON_AddNumberToObject(root, "updatedAt", (double)time(NULL));
|
||||
|
||||
if (!cJSON_GetObjectItemCaseSensitive(root, "global_zaps")) {
|
||||
cJSON_AddObjectToObject(root, "global_zaps");
|
||||
}
|
||||
if (!cJSON_GetObjectItemCaseSensitive(root, "global_ui")) {
|
||||
cJSON_AddObjectToObject(root, "global_ui");
|
||||
}
|
||||
if (!cJSON_GetObjectItemCaseSensitive(root, "global_relays")) {
|
||||
cJSON_AddObjectToObject(root, "global_relays");
|
||||
}
|
||||
if (!cJSON_GetObjectItemCaseSensitive(root, "global_experimental")) {
|
||||
cJSON_AddObjectToObject(root, "global_experimental");
|
||||
}
|
||||
|
||||
llm = cJSON_GetObjectItemCaseSensitive(root, "global_llm");
|
||||
if (!cJSON_IsObject(llm)) {
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(root, "global_llm");
|
||||
llm = cJSON_AddObjectToObject(root, "global_llm");
|
||||
}
|
||||
if (!llm) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
base_url = ai_base_url_from_endpoint(prefs->endpoint ? prefs->endpoint : "");
|
||||
if (!base_url) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(llm, "api_key");
|
||||
cJSON_AddStringToObject(llm, "api_key", prefs->api_key ? prefs->api_key : "");
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(llm, "model");
|
||||
cJSON_AddStringToObject(llm, "model", prefs->model ? prefs->model : "");
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(llm, "base_url");
|
||||
cJSON_AddStringToObject(llm, "base_url", base_url);
|
||||
|
||||
new_json = cJSON_PrintUnformatted(root);
|
||||
cJSON_Delete(root);
|
||||
free(base_url);
|
||||
if (!new_json) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
free(g_state.user_settings_json);
|
||||
g_state.user_settings_json = new_json;
|
||||
|
||||
if (state_nip44_self_encrypt(new_json, &cipher) != 0 || !cipher) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
tags = ai_make_d_tag("user-settings");
|
||||
if (!tags) {
|
||||
free(cipher);
|
||||
return -1;
|
||||
}
|
||||
|
||||
posted = publish_signed_event_with_report("UserSettings", 30078, cipher, tags, 8000);
|
||||
cJSON_Delete(tags);
|
||||
free(cipher);
|
||||
|
||||
return (posted < 0) ? -1 : 0;
|
||||
}
|
||||
|
||||
static char *ai_http_post(const char *url, const char *auth_header, const char *json_body) {
|
||||
CURL *curl;
|
||||
CURLcode rc;
|
||||
long status = 0;
|
||||
nt_http_buffer_t body = {0};
|
||||
struct curl_slist *headers = NULL;
|
||||
char curl_err[CURL_ERROR_SIZE] = {0};
|
||||
int insecure_retry = 0;
|
||||
|
||||
if (!url || !json_body) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
curl_global_cleanup();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
if (auth_header && auth_header[0] != '\0') {
|
||||
headers = curl_slist_append(headers, auth_header);
|
||||
}
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_body);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)strlen(json_body));
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "nostr-terminal/0.0.1");
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 5000L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 30000L);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, nt_http_write_cb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&body);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
nt_configure_curl_tls(curl);
|
||||
if (headers) {
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
}
|
||||
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_err);
|
||||
|
||||
retry_request:
|
||||
rc = curl_easy_perform(curl);
|
||||
if (rc != CURLE_OK) {
|
||||
if (!insecure_retry &&
|
||||
(rc == CURLE_PEER_FAILED_VERIFICATION || rc == CURLE_SSL_CACERT_BADFILE)) {
|
||||
insecure_retry = 1;
|
||||
free(body.data);
|
||||
body.data = NULL;
|
||||
body.len = 0;
|
||||
curl_err[0] = '\0';
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
|
||||
goto retry_request;
|
||||
}
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
curl_global_cleanup();
|
||||
free(body.data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
|
||||
if (status < 200 || status >= 300) {
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
curl_global_cleanup();
|
||||
free(body.data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
curl_global_cleanup();
|
||||
return body.data;
|
||||
}
|
||||
|
||||
static char *ai_extract_response(const char *resp_json) {
|
||||
cJSON *root;
|
||||
cJSON *choices;
|
||||
cJSON *choice0;
|
||||
cJSON *message;
|
||||
cJSON *content;
|
||||
char *out;
|
||||
|
||||
if (!resp_json) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
root = cJSON_Parse(resp_json);
|
||||
if (!root) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
choices = cJSON_GetObjectItemCaseSensitive(root, "choices");
|
||||
choice0 = cJSON_IsArray(choices) ? cJSON_GetArrayItem(choices, 0) : NULL;
|
||||
message = cJSON_IsObject(choice0) ? cJSON_GetObjectItemCaseSensitive(choice0, "message") : NULL;
|
||||
content = cJSON_IsObject(message) ? cJSON_GetObjectItemCaseSensitive(message, "content") : NULL;
|
||||
|
||||
if (!cJSON_IsString(content) || !content->valuestring) {
|
||||
cJSON_Delete(root);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out = ai_strdup(content->valuestring);
|
||||
cJSON_Delete(root);
|
||||
return out;
|
||||
}
|
||||
|
||||
static void ai_chat(const ai_prefs_t *prefs) {
|
||||
char prompt[4096];
|
||||
cJSON *root;
|
||||
cJSON *messages;
|
||||
cJSON *msg;
|
||||
char *body;
|
||||
char auth[4096];
|
||||
char *resp_json;
|
||||
char *answer;
|
||||
char hold[16];
|
||||
|
||||
if (!prefs) {
|
||||
return;
|
||||
}
|
||||
|
||||
tui_get_line("prompt >", prompt, (int)sizeof(prompt));
|
||||
if (prompt[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!prefs->api_key || prefs->api_key[0] == '\0') {
|
||||
tui_print("AI API key is empty. Configure settings first.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
root = cJSON_CreateObject();
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
cJSON_AddStringToObject(root, "model", prefs->model ? prefs->model : "");
|
||||
|
||||
messages = cJSON_CreateArray();
|
||||
msg = cJSON_CreateObject();
|
||||
if (!messages || !msg) {
|
||||
cJSON_Delete(messages);
|
||||
cJSON_Delete(msg);
|
||||
cJSON_Delete(root);
|
||||
return;
|
||||
}
|
||||
cJSON_AddStringToObject(msg, "role", "user");
|
||||
cJSON_AddStringToObject(msg, "content", prompt);
|
||||
cJSON_AddItemToArray(messages, msg);
|
||||
cJSON_AddItemToObject(root, "messages", messages);
|
||||
|
||||
body = cJSON_PrintUnformatted(root);
|
||||
cJSON_Delete(root);
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(auth, sizeof(auth), "Authorization: Bearer %s", prefs->api_key ? prefs->api_key : "");
|
||||
resp_json = ai_http_post(prefs->endpoint ? prefs->endpoint : "", auth, body);
|
||||
free(body);
|
||||
|
||||
if (!resp_json) {
|
||||
tui_print("AI request failed.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
answer = ai_extract_response(resp_json);
|
||||
free(resp_json);
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("AI RESPONSE\n");
|
||||
if (answer) {
|
||||
tui_print("%s", answer);
|
||||
} else {
|
||||
tui_print("Failed to parse AI response.");
|
||||
}
|
||||
free(answer);
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
}
|
||||
|
||||
static void ai_choose_model(ai_prefs_t *prefs) {
|
||||
char model[256];
|
||||
|
||||
if (!prefs) {
|
||||
return;
|
||||
}
|
||||
|
||||
tui_get_line("model >", model, (int)sizeof(model));
|
||||
if (model[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
ai_set_string(&prefs->model, model);
|
||||
}
|
||||
|
||||
static void ai_settings(ai_prefs_t *prefs) {
|
||||
char endpoint[512];
|
||||
char key[1024];
|
||||
char model[256];
|
||||
char hold[16];
|
||||
|
||||
if (!prefs) {
|
||||
return;
|
||||
}
|
||||
|
||||
tui_print("Leave empty to keep current value.");
|
||||
|
||||
tui_print("Current endpoint: %s", prefs->endpoint ? prefs->endpoint : "");
|
||||
tui_get_line("new endpoint >", endpoint, (int)sizeof(endpoint));
|
||||
if (endpoint[0] != '\0') {
|
||||
ai_set_string(&prefs->endpoint, endpoint);
|
||||
}
|
||||
|
||||
tui_print("Current API key: %s", (prefs->api_key && prefs->api_key[0] != '\0') ? "(set)" : "(empty)");
|
||||
tui_get_line("new api key >", key, (int)sizeof(key));
|
||||
if (key[0] != '\0') {
|
||||
ai_set_string(&prefs->api_key, key);
|
||||
}
|
||||
|
||||
tui_print("Current model: %s", prefs->model ? prefs->model : "");
|
||||
tui_get_line("new model >", model, (int)sizeof(model));
|
||||
if (model[0] != '\0') {
|
||||
ai_set_string(&prefs->model, model);
|
||||
}
|
||||
|
||||
if (ai_save_prefs(prefs) != 0) {
|
||||
tui_print("Failed to save prefs.");
|
||||
} else {
|
||||
tui_print("Prefs saved.");
|
||||
}
|
||||
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
}
|
||||
|
||||
void menu_ai(void) {
|
||||
ai_prefs_t prefs;
|
||||
char input[16];
|
||||
|
||||
ai_prefs_init(&prefs);
|
||||
(void)ai_load_prefs(&prefs);
|
||||
|
||||
while (1) {
|
||||
tui_clear_screen();
|
||||
tui_print("AI\n");
|
||||
tui_print("Model: %s", prefs.model ? prefs.model : "");
|
||||
tui_print("Endpoint: %s", prefs.endpoint ? prefs.endpoint : "");
|
||||
tui_print("^_C^:hat");
|
||||
tui_print("^_M^:odel");
|
||||
tui_print("^_S^:ettings");
|
||||
tui_print("^_B^:ack");
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
|
||||
if (input[0] == 'b' || input[0] == 'B' ||
|
||||
input[0] == 'x' || input[0] == 'X' ||
|
||||
input[0] == 'q' || input[0] == 'Q') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (input[0] == 'c' || input[0] == 'C') {
|
||||
ai_chat(&prefs);
|
||||
} else if (input[0] == 'm' || input[0] == 'M') {
|
||||
ai_choose_model(&prefs);
|
||||
} else if (input[0] == 's' || input[0] == 'S') {
|
||||
ai_settings(&prefs);
|
||||
}
|
||||
}
|
||||
|
||||
ai_prefs_free(&prefs);
|
||||
}
|
||||
409
src/menu_diary.c
Normal file
409
src/menu_diary.c
Normal file
@@ -0,0 +1,409 @@
|
||||
#include "tui.h"
|
||||
#include "state.h"
|
||||
#include "net.h"
|
||||
#include "publish.h"
|
||||
#include "editor.h"
|
||||
#include "signer.h"
|
||||
|
||||
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/nip004.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/utils.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
typedef struct {
|
||||
char *date_d;
|
||||
long long created_at;
|
||||
char *content;
|
||||
} diary_entry_t;
|
||||
|
||||
static char *diary_strdup(const char *s) {
|
||||
size_t n;
|
||||
char *out;
|
||||
|
||||
if (!s) {
|
||||
return NULL;
|
||||
}
|
||||
n = strlen(s);
|
||||
out = (char *)malloc(n + 1U);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
memcpy(out, s, n + 1U);
|
||||
return out;
|
||||
}
|
||||
|
||||
static void diary_free_entries(diary_entry_t *entries, int count) {
|
||||
int i;
|
||||
|
||||
if (!entries) {
|
||||
return;
|
||||
}
|
||||
for (i = 0; i < count; i++) {
|
||||
free(entries[i].date_d);
|
||||
free(entries[i].content);
|
||||
}
|
||||
free(entries);
|
||||
}
|
||||
|
||||
static int diary_encrypt_for_self(const char *plaintext, char **cipher_out) {
|
||||
return signer_nip44_self_encrypt(plaintext, cipher_out);
|
||||
}
|
||||
|
||||
static char *diary_decrypt_from_pub(const char *sender_pub_hex, const char *ciphertext) {
|
||||
char *out = NULL;
|
||||
|
||||
if (signer_nip44_decrypt(sender_pub_hex, ciphertext, &out) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static cJSON *diary_make_d_tag(const char *d_value) {
|
||||
cJSON *tags = cJSON_CreateArray();
|
||||
cJSON *d_tag;
|
||||
|
||||
if (!tags) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
d_tag = cJSON_CreateArray();
|
||||
if (!d_tag) {
|
||||
cJSON_Delete(tags);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(d_tag, cJSON_CreateString("d"));
|
||||
cJSON_AddItemToArray(d_tag, cJSON_CreateString(d_value ? d_value : ""));
|
||||
cJSON_AddItemToArray(tags, d_tag);
|
||||
return tags;
|
||||
}
|
||||
|
||||
static int diary_get_today_d(char out[16]) {
|
||||
time_t now;
|
||||
struct tm tmv;
|
||||
|
||||
if (!out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
now = time(NULL);
|
||||
memset(&tmv, 0, sizeof(tmv));
|
||||
if (!localtime_r(&now, &tmv)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (strftime(out, 16, "%Y%m%d", &tmv) == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char *diary_find_d_tag(cJSON *tags) {
|
||||
int i;
|
||||
int n;
|
||||
|
||||
if (!cJSON_IsArray(tags)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
n = cJSON_GetArraySize(tags);
|
||||
for (i = 0; i < n; i++) {
|
||||
cJSON *tag = cJSON_GetArrayItem(tags, i);
|
||||
cJSON *t0;
|
||||
cJSON *t1;
|
||||
|
||||
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
t0 = cJSON_GetArrayItem(tag, 0);
|
||||
t1 = cJSON_GetArrayItem(tag, 1);
|
||||
if (cJSON_IsString(t0) && t0->valuestring && strcmp(t0->valuestring, "d") == 0 &&
|
||||
cJSON_IsString(t1) && t1->valuestring) {
|
||||
return diary_strdup(t1->valuestring);
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char *diary_load_entry_plain(const char *date_d) {
|
||||
char filter[256];
|
||||
const char **read_relays;
|
||||
int relay_count = 0;
|
||||
char *event_json = NULL;
|
||||
cJSON *event;
|
||||
cJSON *content_item;
|
||||
cJSON *pubkey_item;
|
||||
char *plain;
|
||||
|
||||
if (!date_d) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
read_relays = state_get_read_relays(&relay_count);
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"{\"authors\":[\"%s\"],\"kinds\":[30024],\"#d\":[\"%s\"],\"limit\":1}",
|
||||
g_state.npub_hex,
|
||||
date_d);
|
||||
|
||||
if (nt_get_first(filter, read_relays, relay_count, 7000, &event_json) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
if (!event_json) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
event = cJSON_Parse(event_json);
|
||||
free(event_json);
|
||||
if (!event) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
content_item = cJSON_GetObjectItemCaseSensitive(event, "content");
|
||||
pubkey_item = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
|
||||
if (!cJSON_IsString(content_item) || !content_item->valuestring ||
|
||||
!cJSON_IsString(pubkey_item) || !pubkey_item->valuestring) {
|
||||
cJSON_Delete(event);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
plain = diary_decrypt_from_pub(pubkey_item->valuestring, content_item->valuestring);
|
||||
cJSON_Delete(event);
|
||||
return plain;
|
||||
}
|
||||
|
||||
static int diary_save_entry(const char *date_d, const char *plain) {
|
||||
cJSON *tags;
|
||||
char *cipher = NULL;
|
||||
int posted;
|
||||
|
||||
if (!date_d || !plain) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (diary_encrypt_for_self(plain, &cipher) != 0) {
|
||||
tui_print("Failed to encrypt diary entry.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
tags = diary_make_d_tag(date_d);
|
||||
if (!tags) {
|
||||
tui_print("Failed to build diary tags.");
|
||||
free(cipher);
|
||||
return -1;
|
||||
}
|
||||
|
||||
posted = publish_signed_event_with_report("Diary", 30024, cipher, tags, 8000);
|
||||
cJSON_Delete(tags);
|
||||
free(cipher);
|
||||
|
||||
if (posted <= 0) {
|
||||
tui_print("Diary publish failed (accepted by 0 relays).");
|
||||
return -1;
|
||||
}
|
||||
|
||||
tui_print("Diary saved.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int diary_push_entry(diary_entry_t **entries, int *count, diary_entry_t *src) {
|
||||
diary_entry_t *next;
|
||||
|
||||
if (!entries || !count || !src) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
next = (diary_entry_t *)realloc(*entries, (size_t)(*count + 1) * sizeof(diary_entry_t));
|
||||
if (!next) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*entries = next;
|
||||
(*entries)[*count] = *src;
|
||||
(*count)++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int diary_cmp_desc_date(const void *a, const void *b) {
|
||||
const diary_entry_t *ea = (const diary_entry_t *)a;
|
||||
const diary_entry_t *eb = (const diary_entry_t *)b;
|
||||
|
||||
if (!ea->date_d && !eb->date_d) {
|
||||
return 0;
|
||||
}
|
||||
if (!ea->date_d) {
|
||||
return 1;
|
||||
}
|
||||
if (!eb->date_d) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return strcmp(eb->date_d, ea->date_d);
|
||||
}
|
||||
|
||||
static void diary_browse_past(void) {
|
||||
const char **read_relays;
|
||||
int relay_count = 0;
|
||||
char filter[256];
|
||||
char **events = NULL;
|
||||
int event_count = 0;
|
||||
diary_entry_t *entries = NULL;
|
||||
int entries_count = 0;
|
||||
int i;
|
||||
char input[64];
|
||||
|
||||
read_relays = state_get_read_relays(&relay_count);
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"{\"authors\":[\"%s\"],\"kinds\":[30024],\"limit\":300}",
|
||||
g_state.npub_hex);
|
||||
|
||||
if (nt_query_sync(filter, read_relays, relay_count, 7000, &events, &event_count) != 0) {
|
||||
tui_print("Failed to query diary entries.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < event_count; i++) {
|
||||
cJSON *ev = cJSON_Parse(events[i]);
|
||||
cJSON *tags_item;
|
||||
cJSON *created_item;
|
||||
cJSON *content_item;
|
||||
cJSON *pubkey_item;
|
||||
diary_entry_t e;
|
||||
|
||||
if (!ev) {
|
||||
continue;
|
||||
}
|
||||
|
||||
memset(&e, 0, sizeof(e));
|
||||
|
||||
tags_item = cJSON_GetObjectItemCaseSensitive(ev, "tags");
|
||||
created_item = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
|
||||
content_item = cJSON_GetObjectItemCaseSensitive(ev, "content");
|
||||
pubkey_item = cJSON_GetObjectItemCaseSensitive(ev, "pubkey");
|
||||
|
||||
e.date_d = diary_find_d_tag(tags_item);
|
||||
e.created_at = cJSON_IsNumber(created_item) ? (long long)created_item->valuedouble : 0;
|
||||
if (cJSON_IsString(content_item) && content_item->valuestring &&
|
||||
cJSON_IsString(pubkey_item) && pubkey_item->valuestring) {
|
||||
e.content = diary_decrypt_from_pub(pubkey_item->valuestring, content_item->valuestring);
|
||||
}
|
||||
if (!e.content) {
|
||||
e.content = diary_strdup("(decrypt failed)");
|
||||
}
|
||||
|
||||
if (!e.date_d || !e.content) {
|
||||
free(e.date_d);
|
||||
free(e.content);
|
||||
cJSON_Delete(ev);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (diary_push_entry(&entries, &entries_count, &e) != 0) {
|
||||
free(e.date_d);
|
||||
free(e.content);
|
||||
}
|
||||
|
||||
cJSON_Delete(ev);
|
||||
}
|
||||
|
||||
for (i = 0; i < event_count; i++) {
|
||||
free(events[i]);
|
||||
}
|
||||
free(events);
|
||||
|
||||
if (entries_count > 1) {
|
||||
qsort(entries, (size_t)entries_count, sizeof(diary_entry_t), diary_cmp_desc_date);
|
||||
}
|
||||
|
||||
while (1) {
|
||||
tui_clear_screen();
|
||||
tui_print("PAST DIARY ENTRIES\n");
|
||||
for (i = 0; i < entries_count; i++) {
|
||||
tui_print("%2d - %s", i + 1, entries[i].date_d ? entries[i].date_d : "(no d-tag)");
|
||||
}
|
||||
if (entries_count == 0) {
|
||||
tui_print("No diary entries found.");
|
||||
}
|
||||
tui_print("\nEnter number to view, x to back.");
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
|
||||
break;
|
||||
}
|
||||
|
||||
{
|
||||
int idx = atoi(input) - 1;
|
||||
if (idx < 0 || idx >= entries_count) {
|
||||
continue;
|
||||
}
|
||||
tui_clear_screen();
|
||||
tui_print("DIARY %s\n", entries[idx].date_d ? entries[idx].date_d : "");
|
||||
tui_print("%s", entries[idx].content ? entries[idx].content : "");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
}
|
||||
}
|
||||
|
||||
diary_free_entries(entries, entries_count);
|
||||
}
|
||||
|
||||
void menu_diary(void) {
|
||||
char today_d[16];
|
||||
char *existing = NULL;
|
||||
char *edited = NULL;
|
||||
char input[16];
|
||||
|
||||
if (diary_get_today_d(today_d) != 0) {
|
||||
tui_print("Failed to compute today's date.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
return;
|
||||
}
|
||||
|
||||
existing = diary_load_entry_plain(today_d);
|
||||
|
||||
while (1) {
|
||||
tui_clear_screen();
|
||||
tui_print("DIARY\n");
|
||||
tui_print("Today: %s", today_d);
|
||||
tui_print("Existing entry: %s", existing ? "yes" : "no");
|
||||
tui_print("^_E^:dit today's entry");
|
||||
tui_print("^_B^:rowse past entries");
|
||||
tui_print("^_Q^:uit");
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
|
||||
if (input[0] == 'q' || input[0] == 'Q' ||
|
||||
input[0] == 'x' || input[0] == 'X') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (input[0] == 'e' || input[0] == 'E') {
|
||||
edited = editor_launch(existing ? existing : "");
|
||||
if (!edited) {
|
||||
tui_print("Edit canceled.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (diary_save_entry(today_d, edited) == 0) {
|
||||
free(existing);
|
||||
existing = diary_strdup(edited);
|
||||
}
|
||||
free(edited);
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
} else if (input[0] == 'b' || input[0] == 'B') {
|
||||
diary_browse_past();
|
||||
}
|
||||
}
|
||||
|
||||
free(existing);
|
||||
}
|
||||
457
src/menu_dm.c
Normal file
457
src/menu_dm.c
Normal file
@@ -0,0 +1,457 @@
|
||||
#include "tui.h"
|
||||
#include "state.h"
|
||||
#include "net.h"
|
||||
#include "signer.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/nip004.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/nip017.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>
|
||||
|
||||
typedef struct {
|
||||
long long created_at;
|
||||
char *sender_pub;
|
||||
char *content;
|
||||
int kind;
|
||||
} dm_item_t;
|
||||
|
||||
static char *dm_strdup(const char *s) {
|
||||
size_t n;
|
||||
char *out;
|
||||
|
||||
if (!s) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
n = strlen(s);
|
||||
out = (char *)malloc(n + 1U);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
memcpy(out, s, n + 1U);
|
||||
return out;
|
||||
}
|
||||
|
||||
static void dm_free_items(dm_item_t *items, int count) {
|
||||
int i;
|
||||
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
free(items[i].sender_pub);
|
||||
free(items[i].content);
|
||||
}
|
||||
free(items);
|
||||
}
|
||||
|
||||
static int dm_cmp_desc_created(const void *a, const void *b) {
|
||||
const dm_item_t *ia = (const dm_item_t *)a;
|
||||
const dm_item_t *ib = (const dm_item_t *)b;
|
||||
|
||||
if (ia->created_at < ib->created_at) {
|
||||
return 1;
|
||||
}
|
||||
if (ia->created_at > ib->created_at) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const char *dm_shorten_hex(const char *hex, char *buf, size_t buf_sz) {
|
||||
size_t n;
|
||||
|
||||
if (!buf || buf_sz == 0U) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!hex || hex[0] == '\0') {
|
||||
snprintf(buf, buf_sz, "(unknown)");
|
||||
return buf;
|
||||
}
|
||||
|
||||
n = strlen(hex);
|
||||
if (n <= 12U) {
|
||||
snprintf(buf, buf_sz, "%s", hex);
|
||||
} else {
|
||||
snprintf(buf, buf_sz, "%.8s..%.4s", hex, hex + n - 4U);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
static int dm_push_item(dm_item_t **items, int *count, dm_item_t *src) {
|
||||
dm_item_t *next;
|
||||
|
||||
if (!items || !count || !src) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
next = (dm_item_t *)realloc(*items, (size_t)(*count + 1) * sizeof(dm_item_t));
|
||||
if (!next) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*items = next;
|
||||
(*items)[*count] = *src;
|
||||
(*count)++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int dm_parse_recipient_hex(const char *input, char out_hex[65]) {
|
||||
if (!input || !out_hex) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (strncmp(input, "npub", 4) == 0) {
|
||||
unsigned char pub[32];
|
||||
if (nostr_decode_npub(input, pub) != 0) {
|
||||
return -1;
|
||||
}
|
||||
nostr_bytes_to_hex(pub, 32, out_hex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strlen(input) == 64U) {
|
||||
unsigned char pub[32];
|
||||
if (nostr_hex_to_bytes(input, pub, 32) != 0) {
|
||||
return -1;
|
||||
}
|
||||
snprintf(out_hex, 65, "%s", input);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void menu_dm_send(void) {
|
||||
char who[256];
|
||||
char msg[4096];
|
||||
char recip_hex[65] = {0};
|
||||
const char *recips[1];
|
||||
unsigned char priv[32];
|
||||
cJSON *rumor = NULL;
|
||||
cJSON *gift_wraps[8] = {0};
|
||||
int wrap_count = 0;
|
||||
const char **write_relays;
|
||||
int relay_count = 0;
|
||||
int i;
|
||||
int any_ok = 0;
|
||||
char hold[16];
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("SEND DM\n");
|
||||
|
||||
tui_get_line("recipient (npub or hex) >", who, (int)sizeof(who));
|
||||
if (who[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dm_parse_recipient_hex(who, recip_hex) != 0) {
|
||||
tui_print("Invalid recipient. Use npub or 64-char hex pubkey.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
tui_get_line("message >", msg, (int)sizeof(msg));
|
||||
if (msg[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (signer_get_local_private_key(priv) != 0) {
|
||||
tui_print("Private key unavailable for DM send.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
recips[0] = recip_hex;
|
||||
rumor = nostr_nip17_create_chat_event(msg,
|
||||
recips,
|
||||
1,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
g_state.npub_hex);
|
||||
if (!rumor) {
|
||||
tui_print("Failed to build NIP-17 DM rumor.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
wrap_count = nostr_nip17_send_dm(rumor, recips, 1, priv, gift_wraps, 8, 0);
|
||||
cJSON_Delete(rumor);
|
||||
if (wrap_count <= 0) {
|
||||
tui_print("Failed to create NIP-17 gift wrap event(s).");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
write_relays = state_get_write_relays(&relay_count);
|
||||
tui_print("Publishing %d wrapped DM event(s) to %d relay(s)...", wrap_count, relay_count);
|
||||
|
||||
for (i = 0; i < wrap_count; i++) {
|
||||
char *event_json;
|
||||
int posted;
|
||||
|
||||
if (!gift_wraps[i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
event_json = cJSON_PrintUnformatted(gift_wraps[i]);
|
||||
cJSON_Delete(gift_wraps[i]);
|
||||
gift_wraps[i] = NULL;
|
||||
if (!event_json) {
|
||||
continue;
|
||||
}
|
||||
|
||||
posted = nt_publish(event_json, write_relays, relay_count, 8000);
|
||||
free(event_json);
|
||||
if (posted > 0) {
|
||||
any_ok = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (any_ok) {
|
||||
tui_print("DM sent.");
|
||||
} else {
|
||||
tui_print("DM publish failed.");
|
||||
}
|
||||
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
}
|
||||
|
||||
static char *dm_try_decrypt_kind4(const char *sender_pub_hex, const char *ciphertext) {
|
||||
char *out = NULL;
|
||||
if (!sender_pub_hex || !ciphertext) return NULL;
|
||||
if (signer_nip04_decrypt(sender_pub_hex, ciphertext, &out) != 0) return NULL;
|
||||
return out;
|
||||
}
|
||||
|
||||
static void dm_collect_legacy_kind4(dm_item_t **items, int *items_count) {
|
||||
const char **read_relays;
|
||||
int relay_count = 0;
|
||||
char filter[256];
|
||||
char **events = NULL;
|
||||
int event_count = 0;
|
||||
int i;
|
||||
|
||||
if (!items || !items_count) {
|
||||
return;
|
||||
}
|
||||
|
||||
read_relays = state_get_read_relays(&relay_count);
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"{\"#p\":[\"%s\"],\"kinds\":[4],\"limit\":200}",
|
||||
g_state.npub_hex);
|
||||
|
||||
if (nt_query_sync(filter, read_relays, relay_count, 7000, &events, &event_count) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < event_count; i++) {
|
||||
cJSON *ev = cJSON_Parse(events[i]);
|
||||
cJSON *created_item;
|
||||
cJSON *pubkey_item;
|
||||
cJSON *content_item;
|
||||
dm_item_t it;
|
||||
char *plain;
|
||||
|
||||
if (!ev) {
|
||||
continue;
|
||||
}
|
||||
|
||||
created_item = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
|
||||
pubkey_item = cJSON_GetObjectItemCaseSensitive(ev, "pubkey");
|
||||
content_item = cJSON_GetObjectItemCaseSensitive(ev, "content");
|
||||
if (!cJSON_IsNumber(created_item) ||
|
||||
!cJSON_IsString(pubkey_item) || !pubkey_item->valuestring ||
|
||||
!cJSON_IsString(content_item) || !content_item->valuestring) {
|
||||
cJSON_Delete(ev);
|
||||
continue;
|
||||
}
|
||||
|
||||
plain = dm_try_decrypt_kind4(pubkey_item->valuestring, content_item->valuestring);
|
||||
|
||||
memset(&it, 0, sizeof(it));
|
||||
it.kind = 4;
|
||||
it.created_at = (long long)created_item->valuedouble;
|
||||
it.sender_pub = dm_strdup(pubkey_item->valuestring);
|
||||
it.content = plain ? plain : dm_strdup("(NIP-04 decrypt failed)");
|
||||
|
||||
if (!it.sender_pub || !it.content) {
|
||||
free(it.sender_pub);
|
||||
free(it.content);
|
||||
cJSON_Delete(ev);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dm_push_item(items, items_count, &it) != 0) {
|
||||
free(it.sender_pub);
|
||||
free(it.content);
|
||||
}
|
||||
|
||||
cJSON_Delete(ev);
|
||||
}
|
||||
|
||||
for (i = 0; i < event_count; i++) {
|
||||
free(events[i]);
|
||||
}
|
||||
free(events);
|
||||
}
|
||||
|
||||
static void dm_collect_nip17_kind1059(dm_item_t **items, int *items_count) {
|
||||
const char **read_relays;
|
||||
int relay_count = 0;
|
||||
char filter[256];
|
||||
char **events = NULL;
|
||||
int event_count = 0;
|
||||
unsigned char priv[32];
|
||||
int i;
|
||||
|
||||
if (!items || !items_count) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (signer_get_local_private_key(priv) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
read_relays = state_get_read_relays(&relay_count);
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"{\"#p\":[\"%s\"],\"kinds\":[1059],\"limit\":200}",
|
||||
g_state.npub_hex);
|
||||
|
||||
if (nt_query_sync(filter, read_relays, relay_count, 7000, &events, &event_count) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < event_count; i++) {
|
||||
cJSON *gift = cJSON_Parse(events[i]);
|
||||
cJSON *created_item;
|
||||
cJSON *pubkey_item;
|
||||
cJSON *rumor;
|
||||
cJSON *rumor_content;
|
||||
cJSON *rumor_pubkey;
|
||||
dm_item_t it;
|
||||
|
||||
if (!gift) {
|
||||
continue;
|
||||
}
|
||||
|
||||
created_item = cJSON_GetObjectItemCaseSensitive(gift, "created_at");
|
||||
pubkey_item = cJSON_GetObjectItemCaseSensitive(gift, "pubkey");
|
||||
|
||||
rumor = nostr_nip17_receive_dm(gift, priv);
|
||||
memset(&it, 0, sizeof(it));
|
||||
it.kind = 1059;
|
||||
it.created_at = cJSON_IsNumber(created_item) ? (long long)created_item->valuedouble : 0;
|
||||
|
||||
if (rumor) {
|
||||
rumor_pubkey = cJSON_GetObjectItemCaseSensitive(rumor, "pubkey");
|
||||
rumor_content = cJSON_GetObjectItemCaseSensitive(rumor, "content");
|
||||
it.sender_pub = (cJSON_IsString(rumor_pubkey) && rumor_pubkey->valuestring)
|
||||
? dm_strdup(rumor_pubkey->valuestring)
|
||||
: dm_strdup((cJSON_IsString(pubkey_item) && pubkey_item->valuestring)
|
||||
? pubkey_item->valuestring
|
||||
: "");
|
||||
it.content = (cJSON_IsString(rumor_content) && rumor_content->valuestring)
|
||||
? dm_strdup(rumor_content->valuestring)
|
||||
: dm_strdup("(empty NIP-17 content)");
|
||||
cJSON_Delete(rumor);
|
||||
} else {
|
||||
it.sender_pub = dm_strdup((cJSON_IsString(pubkey_item) && pubkey_item->valuestring)
|
||||
? pubkey_item->valuestring
|
||||
: "");
|
||||
it.content = dm_strdup("(NIP-17 decrypt not available or failed)");
|
||||
}
|
||||
|
||||
if (!it.sender_pub || !it.content) {
|
||||
free(it.sender_pub);
|
||||
free(it.content);
|
||||
cJSON_Delete(gift);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dm_push_item(items, items_count, &it) != 0) {
|
||||
free(it.sender_pub);
|
||||
free(it.content);
|
||||
}
|
||||
|
||||
cJSON_Delete(gift);
|
||||
}
|
||||
|
||||
for (i = 0; i < event_count; i++) {
|
||||
free(events[i]);
|
||||
}
|
||||
free(events);
|
||||
}
|
||||
|
||||
static void menu_dm_read_inbox(void) {
|
||||
dm_item_t *items = NULL;
|
||||
int items_count = 0;
|
||||
int i;
|
||||
char hold[16];
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("READ INBOX\n");
|
||||
tui_print("Querying kind 1059 + kind 4...");
|
||||
|
||||
dm_collect_nip17_kind1059(&items, &items_count);
|
||||
dm_collect_legacy_kind4(&items, &items_count);
|
||||
|
||||
if (items_count > 1) {
|
||||
qsort(items, (size_t)items_count, sizeof(dm_item_t), dm_cmp_desc_created);
|
||||
}
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("INBOX\n");
|
||||
|
||||
for (i = 0; i < items_count; i++) {
|
||||
char sender_short[32];
|
||||
const char *sender = dm_shorten_hex(items[i].sender_pub, sender_short, sizeof(sender_short));
|
||||
tui_print("[%3d] (%d) %s: %s", i + 1, items[i].kind, sender, items[i].content ? items[i].content : "");
|
||||
}
|
||||
|
||||
if (items_count == 0) {
|
||||
tui_print("No DMs found.");
|
||||
}
|
||||
|
||||
dm_free_items(items, items_count);
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
}
|
||||
|
||||
void menu_dm(void) {
|
||||
char input[16];
|
||||
|
||||
while (1) {
|
||||
tui_clear_screen();
|
||||
tui_print("DIRECT MESSAGE\n");
|
||||
tui_print("^_S^:end DM (NIP-17)");
|
||||
tui_print("^_R^:ead inbox (kind 1059 + kind 4)");
|
||||
tui_print("^_B^:ack");
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
|
||||
if (input[0] == 'b' || input[0] == 'B' ||
|
||||
input[0] == 'x' || input[0] == 'X' ||
|
||||
input[0] == 'q' || input[0] == 'Q') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (input[0] == 's' || input[0] == 'S') {
|
||||
menu_dm_send();
|
||||
} else if (input[0] == 'r' || input[0] == 'R') {
|
||||
menu_dm_read_inbox();
|
||||
}
|
||||
}
|
||||
}
|
||||
804
src/menu_ecash.c
Normal file
804
src/menu_ecash.c
Normal file
@@ -0,0 +1,804 @@
|
||||
#include "tui.h"
|
||||
#include "state.h"
|
||||
#include "net.h"
|
||||
#include "publish.h"
|
||||
|
||||
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/cashu_mint.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/nip060.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
char **mints;
|
||||
int mint_count;
|
||||
nostr_cashu_proof_t *proofs;
|
||||
char **proof_mints;
|
||||
int proof_count;
|
||||
} ecash_wallet_t;
|
||||
|
||||
static char *ecash_strdup(const char *s) {
|
||||
size_t n;
|
||||
char *out;
|
||||
|
||||
if (!s) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
n = strlen(s);
|
||||
out = (char *)malloc(n + 1U);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
memcpy(out, s, n + 1U);
|
||||
return out;
|
||||
}
|
||||
|
||||
static int ecash_has_mint(const ecash_wallet_t *w, const char *mint_url) {
|
||||
int i;
|
||||
|
||||
if (!w || !mint_url) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (i = 0; i < w->mint_count; i++) {
|
||||
if (w->mints[i] && strcmp(w->mints[i], mint_url) == 0) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int ecash_add_mint(ecash_wallet_t *w, const char *mint_url) {
|
||||
char **next;
|
||||
char *copy;
|
||||
|
||||
if (!w || !mint_url || mint_url[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ecash_has_mint(w, mint_url)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
copy = ecash_strdup(mint_url);
|
||||
if (!copy) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
next = (char **)realloc(w->mints, (size_t)(w->mint_count + 1) * sizeof(char *));
|
||||
if (!next) {
|
||||
free(copy);
|
||||
return -1;
|
||||
}
|
||||
|
||||
w->mints = next;
|
||||
w->mints[w->mint_count] = copy;
|
||||
w->mint_count++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void ecash_free_proof(nostr_cashu_proof_t *p) {
|
||||
if (!p) {
|
||||
return;
|
||||
}
|
||||
free(p->secret);
|
||||
free(p->C);
|
||||
p->secret = NULL;
|
||||
p->C = NULL;
|
||||
}
|
||||
|
||||
static int ecash_copy_proof(nostr_cashu_proof_t *dst, const nostr_cashu_proof_t *src) {
|
||||
if (!dst || !src) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(dst, 0, sizeof(*dst));
|
||||
snprintf(dst->id, sizeof(dst->id), "%s", src->id);
|
||||
dst->amount = src->amount;
|
||||
dst->secret = ecash_strdup(src->secret ? src->secret : "");
|
||||
dst->C = ecash_strdup(src->C ? src->C : "");
|
||||
|
||||
if (!dst->secret || !dst->C) {
|
||||
ecash_free_proof(dst);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int ecash_push_proof(ecash_wallet_t *w, const char *mint_url, const nostr_cashu_proof_t *proof) {
|
||||
nostr_cashu_proof_t *next_proofs;
|
||||
char **next_mints;
|
||||
nostr_cashu_proof_t copy;
|
||||
char *mint_copy;
|
||||
|
||||
if (!w || !mint_url || !proof) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ecash_copy_proof(©, proof) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
mint_copy = ecash_strdup(mint_url);
|
||||
if (!mint_copy) {
|
||||
ecash_free_proof(©);
|
||||
return -1;
|
||||
}
|
||||
|
||||
next_proofs = (nostr_cashu_proof_t *)realloc(w->proofs, (size_t)(w->proof_count + 1) * sizeof(nostr_cashu_proof_t));
|
||||
if (!next_proofs) {
|
||||
ecash_free_proof(©);
|
||||
free(mint_copy);
|
||||
return -1;
|
||||
}
|
||||
|
||||
next_mints = (char **)realloc(w->proof_mints, (size_t)(w->proof_count + 1) * sizeof(char *));
|
||||
if (!next_mints) {
|
||||
ecash_free_proof(©);
|
||||
free(mint_copy);
|
||||
return -1;
|
||||
}
|
||||
|
||||
w->proofs = next_proofs;
|
||||
w->proof_mints = next_mints;
|
||||
w->proofs[w->proof_count] = copy;
|
||||
w->proof_mints[w->proof_count] = mint_copy;
|
||||
w->proof_count++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void ecash_wallet_free(ecash_wallet_t *w) {
|
||||
int i;
|
||||
|
||||
if (!w) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < w->mint_count; i++) {
|
||||
free(w->mints[i]);
|
||||
}
|
||||
free(w->mints);
|
||||
|
||||
for (i = 0; i < w->proof_count; i++) {
|
||||
ecash_free_proof(&w->proofs[i]);
|
||||
free(w->proof_mints[i]);
|
||||
}
|
||||
free(w->proofs);
|
||||
free(w->proof_mints);
|
||||
|
||||
memset(w, 0, sizeof(*w));
|
||||
}
|
||||
|
||||
static char *ecash_extract_wallet_content(const char *event_or_content_json) {
|
||||
cJSON *root;
|
||||
cJSON *content;
|
||||
char *out;
|
||||
|
||||
if (!event_or_content_json || event_or_content_json[0] == '\0') {
|
||||
return ecash_strdup("{}");
|
||||
}
|
||||
|
||||
root = cJSON_Parse(event_or_content_json);
|
||||
if (!root) {
|
||||
return ecash_strdup("{}");
|
||||
}
|
||||
|
||||
content = cJSON_GetObjectItemCaseSensitive(root, "content");
|
||||
if (cJSON_IsString(content) && content->valuestring) {
|
||||
out = ecash_strdup(content->valuestring);
|
||||
} else {
|
||||
out = ecash_strdup(event_or_content_json);
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return out ? out : ecash_strdup("{}");
|
||||
}
|
||||
|
||||
static int ecash_wallet_from_content_json(const char *content_json, ecash_wallet_t *w) {
|
||||
cJSON *root;
|
||||
cJSON *mints;
|
||||
cJSON *proofs;
|
||||
int i;
|
||||
|
||||
if (!w) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!content_json || content_json[0] == '\0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
root = cJSON_Parse(content_json);
|
||||
if (!root || !cJSON_IsObject(root)) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
mints = cJSON_GetObjectItemCaseSensitive(root, "mints");
|
||||
if (cJSON_IsArray(mints)) {
|
||||
int n = cJSON_GetArraySize(mints);
|
||||
for (i = 0; i < n; i++) {
|
||||
cJSON *mi = cJSON_GetArrayItem(mints, i);
|
||||
if (cJSON_IsString(mi) && mi->valuestring) {
|
||||
(void)ecash_add_mint(w, mi->valuestring);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proofs = cJSON_GetObjectItemCaseSensitive(root, "proofs");
|
||||
if (cJSON_IsArray(proofs)) {
|
||||
int n = cJSON_GetArraySize(proofs);
|
||||
for (i = 0; i < n; i++) {
|
||||
cJSON *pi = cJSON_GetArrayItem(proofs, i);
|
||||
cJSON *mint_item;
|
||||
cJSON *id_item;
|
||||
cJSON *amount_item;
|
||||
cJSON *secret_item;
|
||||
cJSON *c_item;
|
||||
nostr_cashu_proof_t p;
|
||||
const char *mint_url;
|
||||
|
||||
if (!cJSON_IsObject(pi)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
mint_item = cJSON_GetObjectItemCaseSensitive(pi, "mint");
|
||||
id_item = cJSON_GetObjectItemCaseSensitive(pi, "id");
|
||||
amount_item = cJSON_GetObjectItemCaseSensitive(pi, "amount");
|
||||
secret_item = cJSON_GetObjectItemCaseSensitive(pi, "secret");
|
||||
c_item = cJSON_GetObjectItemCaseSensitive(pi, "C");
|
||||
|
||||
if (!cJSON_IsString(mint_item) || !mint_item->valuestring ||
|
||||
!cJSON_IsString(id_item) || !id_item->valuestring ||
|
||||
!cJSON_IsNumber(amount_item) ||
|
||||
!cJSON_IsString(secret_item) || !secret_item->valuestring ||
|
||||
!cJSON_IsString(c_item) || !c_item->valuestring) {
|
||||
continue;
|
||||
}
|
||||
|
||||
memset(&p, 0, sizeof(p));
|
||||
snprintf(p.id, sizeof(p.id), "%s", id_item->valuestring);
|
||||
p.amount = (uint64_t)amount_item->valuedouble;
|
||||
p.secret = (char *)secret_item->valuestring;
|
||||
p.C = (char *)c_item->valuestring;
|
||||
mint_url = mint_item->valuestring;
|
||||
|
||||
(void)ecash_add_mint(w, mint_url);
|
||||
(void)ecash_push_proof(w, mint_url, &p);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int ecash_wallet_load(ecash_wallet_t *w) {
|
||||
const char **read_relays;
|
||||
int relay_count = 0;
|
||||
char filter[256];
|
||||
char *event_json = NULL;
|
||||
char *content_json;
|
||||
|
||||
if (!w) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(w, 0, sizeof(*w));
|
||||
|
||||
read_relays = state_get_read_relays(&relay_count);
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"{\"authors\":[\"%s\"],\"kinds\":[17375],\"limit\":1}",
|
||||
g_state.npub_hex);
|
||||
|
||||
if (nt_get_first(filter, read_relays, relay_count, 7000, &event_json) != 0) {
|
||||
event_json = NULL;
|
||||
}
|
||||
|
||||
if (!event_json && g_state.kind17375_json && g_state.kind17375_json[0] != '\0') {
|
||||
event_json = ecash_strdup(g_state.kind17375_json);
|
||||
}
|
||||
|
||||
content_json = ecash_extract_wallet_content(event_json ? event_json : "{}");
|
||||
free(event_json);
|
||||
if (!content_json) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ecash_wallet_from_content_json(content_json, w) != 0) {
|
||||
free(content_json);
|
||||
ecash_wallet_free(w);
|
||||
return -1;
|
||||
}
|
||||
|
||||
free(content_json);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char *ecash_wallet_to_content_json(const ecash_wallet_t *w) {
|
||||
cJSON *root;
|
||||
cJSON *mints;
|
||||
cJSON *proofs;
|
||||
int i;
|
||||
char *out;
|
||||
|
||||
if (!w) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
root = cJSON_CreateObject();
|
||||
if (!root) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
mints = cJSON_CreateArray();
|
||||
proofs = cJSON_CreateArray();
|
||||
if (!mints || !proofs) {
|
||||
cJSON_Delete(mints);
|
||||
cJSON_Delete(proofs);
|
||||
cJSON_Delete(root);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (i = 0; i < w->mint_count; i++) {
|
||||
cJSON_AddItemToArray(mints, cJSON_CreateString(w->mints[i] ? w->mints[i] : ""));
|
||||
}
|
||||
|
||||
for (i = 0; i < w->proof_count; i++) {
|
||||
cJSON *po = cJSON_CreateObject();
|
||||
if (!po) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON_AddStringToObject(po, "mint", w->proof_mints[i] ? w->proof_mints[i] : "");
|
||||
cJSON_AddStringToObject(po, "id", w->proofs[i].id);
|
||||
cJSON_AddNumberToObject(po, "amount", (double)w->proofs[i].amount);
|
||||
cJSON_AddStringToObject(po, "secret", w->proofs[i].secret ? w->proofs[i].secret : "");
|
||||
cJSON_AddStringToObject(po, "C", w->proofs[i].C ? w->proofs[i].C : "");
|
||||
cJSON_AddItemToArray(proofs, po);
|
||||
}
|
||||
|
||||
cJSON_AddItemToObject(root, "mints", mints);
|
||||
cJSON_AddItemToObject(root, "proofs", proofs);
|
||||
|
||||
out = cJSON_PrintUnformatted(root);
|
||||
cJSON_Delete(root);
|
||||
return out;
|
||||
}
|
||||
|
||||
static int ecash_publish_wallet(const ecash_wallet_t *w) {
|
||||
char *content;
|
||||
int posted;
|
||||
|
||||
if (!w) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
content = ecash_wallet_to_content_json(w);
|
||||
if (!content) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
posted = publish_signed_event_with_report("Wallet", NOSTR_NIP60_WALLET_KIND, content, NULL, 8000);
|
||||
free(content);
|
||||
|
||||
return (posted < 0) ? -1 : 0;
|
||||
}
|
||||
|
||||
static uint64_t ecash_wallet_total(const ecash_wallet_t *w) {
|
||||
int i;
|
||||
uint64_t total = 0;
|
||||
|
||||
if (!w) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (i = 0; i < w->proof_count; i++) {
|
||||
total += w->proofs[i].amount;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
static void ecash_show_balance(const ecash_wallet_t *w) {
|
||||
int i;
|
||||
char hold[16];
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("ECASH BALANCE\n");
|
||||
tui_print("Total: %llu sats", (unsigned long long)ecash_wallet_total(w));
|
||||
tui_print("");
|
||||
|
||||
for (i = 0; i < w->mint_count; i++) {
|
||||
int j;
|
||||
uint64_t mint_total = 0;
|
||||
|
||||
for (j = 0; j < w->proof_count; j++) {
|
||||
if (w->proof_mints[j] && w->mints[i] && strcmp(w->proof_mints[j], w->mints[i]) == 0) {
|
||||
mint_total += w->proofs[j].amount;
|
||||
}
|
||||
}
|
||||
|
||||
tui_print("%2d. %s", i + 1, w->mints[i]);
|
||||
tui_print(" balance: %llu sats", (unsigned long long)mint_total);
|
||||
}
|
||||
|
||||
if (w->mint_count == 0) {
|
||||
tui_print("No mints configured.");
|
||||
}
|
||||
|
||||
tui_print("\nProof count: %d", w->proof_count);
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
}
|
||||
|
||||
static void ecash_add_mint_menu(ecash_wallet_t *w) {
|
||||
char mint[512];
|
||||
char hold[16];
|
||||
|
||||
tui_get_line("mint url >", mint, (int)sizeof(mint));
|
||||
if (mint[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ecash_add_mint(w, mint) != 0) {
|
||||
tui_print("Failed to add mint.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
if (ecash_publish_wallet(w) != 0) {
|
||||
tui_print("Failed to publish wallet metadata.");
|
||||
} else {
|
||||
tui_print("Mint added.");
|
||||
}
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
}
|
||||
|
||||
static void ecash_remove_mint_menu(ecash_wallet_t *w) {
|
||||
char input[64];
|
||||
int idx;
|
||||
int i;
|
||||
char hold[16];
|
||||
|
||||
if (!w || w->mint_count <= 0) {
|
||||
tui_print("No mints to remove.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < w->mint_count; i++) {
|
||||
tui_print("%2d. %s", i + 1, w->mints[i]);
|
||||
}
|
||||
|
||||
tui_get_line("remove number >", input, (int)sizeof(input));
|
||||
idx = atoi(input) - 1;
|
||||
if (idx < 0 || idx >= w->mint_count) {
|
||||
tui_print("Invalid mint number.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
free(w->mints[idx]);
|
||||
for (i = idx; i < w->mint_count - 1; i++) {
|
||||
w->mints[i] = w->mints[i + 1];
|
||||
}
|
||||
w->mint_count--;
|
||||
|
||||
if (ecash_publish_wallet(w) != 0) {
|
||||
tui_print("Failed to publish wallet metadata.");
|
||||
} else {
|
||||
tui_print("Mint removed.");
|
||||
}
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
}
|
||||
|
||||
static void ecash_receive_token_menu(ecash_wallet_t *w) {
|
||||
char token[8192];
|
||||
cashu_decoded_token_t decoded;
|
||||
int i;
|
||||
char hold[16];
|
||||
|
||||
if (!w) {
|
||||
return;
|
||||
}
|
||||
|
||||
memset(&decoded, 0, sizeof(decoded));
|
||||
tui_get_line("paste cashu token >", token, (int)sizeof(token));
|
||||
if (token[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cashu_decode_token(token, &decoded) != 0) {
|
||||
tui_print("Failed to decode token.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!decoded.mint_url || decoded.proof_count <= 0 || !decoded.proofs) {
|
||||
cashu_free_decoded_token(&decoded);
|
||||
tui_print("Token did not contain usable proofs.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
(void)ecash_add_mint(w, decoded.mint_url);
|
||||
for (i = 0; i < decoded.proof_count; i++) {
|
||||
(void)ecash_push_proof(w, decoded.mint_url, &decoded.proofs[i]);
|
||||
}
|
||||
|
||||
cashu_free_decoded_token(&decoded);
|
||||
|
||||
if (ecash_publish_wallet(w) != 0) {
|
||||
tui_print("Token imported locally, but publish failed.");
|
||||
} else {
|
||||
tui_print("Token received and wallet updated.");
|
||||
}
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
}
|
||||
|
||||
static void ecash_remove_proofs_by_global_indices(ecash_wallet_t *w, const int *indices, int nidx) {
|
||||
char *remove_flags;
|
||||
int i;
|
||||
int out_i;
|
||||
|
||||
if (!w || !indices || nidx <= 0 || w->proof_count <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
remove_flags = (char *)calloc((size_t)w->proof_count, 1U);
|
||||
if (!remove_flags) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < nidx; i++) {
|
||||
if (indices[i] >= 0 && indices[i] < w->proof_count) {
|
||||
remove_flags[indices[i]] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
out_i = 0;
|
||||
for (i = 0; i < w->proof_count; i++) {
|
||||
if (remove_flags[i]) {
|
||||
ecash_free_proof(&w->proofs[i]);
|
||||
free(w->proof_mints[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (out_i != i) {
|
||||
w->proofs[out_i] = w->proofs[i];
|
||||
w->proof_mints[out_i] = w->proof_mints[i];
|
||||
}
|
||||
out_i++;
|
||||
}
|
||||
|
||||
w->proof_count = out_i;
|
||||
free(remove_flags);
|
||||
}
|
||||
|
||||
static void ecash_send_token_menu(ecash_wallet_t *w) {
|
||||
char input[128];
|
||||
int mint_idx;
|
||||
uint64_t target_amount;
|
||||
int i;
|
||||
nostr_cashu_proof_t *mint_proofs = NULL;
|
||||
int *mint_global_indices = NULL;
|
||||
int mint_proof_count = 0;
|
||||
int *selected_local = NULL;
|
||||
int selected_count;
|
||||
uint64_t selected_total = 0;
|
||||
cashu_decoded_token_t token;
|
||||
char *encoded = NULL;
|
||||
int *selected_global = NULL;
|
||||
char hold[16];
|
||||
|
||||
if (!w) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (w->mint_count <= 0) {
|
||||
tui_print("No mints configured.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < w->mint_count; i++) {
|
||||
tui_print("%2d. %s", i + 1, w->mints[i]);
|
||||
}
|
||||
|
||||
tui_get_line("mint number >", input, (int)sizeof(input));
|
||||
mint_idx = atoi(input) - 1;
|
||||
if (mint_idx < 0 || mint_idx >= w->mint_count) {
|
||||
tui_print("Invalid mint number.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
tui_get_line("amount (sats) >", input, (int)sizeof(input));
|
||||
target_amount = (uint64_t)strtoull(input, NULL, 10);
|
||||
if (target_amount == 0) {
|
||||
tui_print("Invalid amount.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < w->proof_count; i++) {
|
||||
if (w->proof_mints[i] && strcmp(w->proof_mints[i], w->mints[mint_idx]) == 0) {
|
||||
nostr_cashu_proof_t *next_proofs;
|
||||
int *next_idx;
|
||||
|
||||
next_proofs = (nostr_cashu_proof_t *)realloc(mint_proofs, (size_t)(mint_proof_count + 1) * sizeof(nostr_cashu_proof_t));
|
||||
if (!next_proofs) {
|
||||
free(mint_proofs);
|
||||
free(mint_global_indices);
|
||||
tui_print("Memory error.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
mint_proofs = next_proofs;
|
||||
|
||||
next_idx = (int *)realloc(mint_global_indices, (size_t)(mint_proof_count + 1) * sizeof(int));
|
||||
if (!next_idx) {
|
||||
free(mint_proofs);
|
||||
free(mint_global_indices);
|
||||
tui_print("Memory error.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
mint_global_indices = next_idx;
|
||||
mint_proofs[mint_proof_count] = w->proofs[i];
|
||||
mint_global_indices[mint_proof_count] = i;
|
||||
mint_proof_count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (mint_proof_count <= 0) {
|
||||
free(mint_proofs);
|
||||
free(mint_global_indices);
|
||||
tui_print("No proofs available for selected mint.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
selected_local = (int *)malloc((size_t)mint_proof_count * sizeof(int));
|
||||
if (!selected_local) {
|
||||
free(mint_proofs);
|
||||
free(mint_global_indices);
|
||||
tui_print("Memory error.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
selected_count = cashu_mint_select_proofs_for_amount(mint_proofs,
|
||||
mint_proof_count,
|
||||
target_amount,
|
||||
selected_local,
|
||||
mint_proof_count,
|
||||
&selected_total);
|
||||
if (selected_count <= 0) {
|
||||
free(selected_local);
|
||||
free(mint_proofs);
|
||||
free(mint_global_indices);
|
||||
tui_print("Could not select proofs for that amount.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
memset(&token, 0, sizeof(token));
|
||||
token.mint_url = ecash_strdup(w->mints[mint_idx]);
|
||||
token.proofs = (nostr_cashu_proof_t *)calloc((size_t)selected_count, sizeof(nostr_cashu_proof_t));
|
||||
token.proof_count = selected_count;
|
||||
selected_global = (int *)malloc((size_t)selected_count * sizeof(int));
|
||||
|
||||
if (!token.mint_url || !token.proofs || !selected_global) {
|
||||
free(selected_local);
|
||||
free(mint_proofs);
|
||||
free(mint_global_indices);
|
||||
free(selected_global);
|
||||
cashu_free_decoded_token(&token);
|
||||
tui_print("Memory error.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < selected_count; i++) {
|
||||
int local_idx = selected_local[i];
|
||||
int global_idx;
|
||||
|
||||
if (local_idx < 0 || local_idx >= mint_proof_count ||
|
||||
ecash_copy_proof(&token.proofs[i], &mint_proofs[local_idx]) != 0) {
|
||||
free(selected_local);
|
||||
free(mint_proofs);
|
||||
free(mint_global_indices);
|
||||
free(selected_global);
|
||||
cashu_free_decoded_token(&token);
|
||||
tui_print("Failed to assemble token proofs.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
global_idx = mint_global_indices[local_idx];
|
||||
selected_global[i] = global_idx;
|
||||
}
|
||||
|
||||
if (cashu_encode_token(&token, CASHU_TOKEN_FORMAT_B, &encoded) != 0 || !encoded) {
|
||||
free(selected_local);
|
||||
free(mint_proofs);
|
||||
free(mint_global_indices);
|
||||
free(selected_global);
|
||||
cashu_free_decoded_token(&token);
|
||||
tui_print("Failed to encode token.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
return;
|
||||
}
|
||||
|
||||
ecash_remove_proofs_by_global_indices(w, selected_global, selected_count);
|
||||
(void)ecash_publish_wallet(w);
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("SEND TOKEN\n");
|
||||
tui_print("Requested: %llu sats", (unsigned long long)target_amount);
|
||||
tui_print("Selected: %llu sats", (unsigned long long)selected_total);
|
||||
tui_print("Token:");
|
||||
tui_print("%s", encoded);
|
||||
tui_print("");
|
||||
tui_print("Note: Mint HTTP swap/melt flows are not yet implemented in this MVP.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
|
||||
free(encoded);
|
||||
free(selected_local);
|
||||
free(mint_proofs);
|
||||
free(mint_global_indices);
|
||||
free(selected_global);
|
||||
cashu_free_decoded_token(&token);
|
||||
}
|
||||
|
||||
void menu_ecash(void) {
|
||||
ecash_wallet_t wallet;
|
||||
char input[16];
|
||||
|
||||
if (ecash_wallet_load(&wallet) != 0) {
|
||||
memset(&wallet, 0, sizeof(wallet));
|
||||
}
|
||||
|
||||
while (1) {
|
||||
tui_clear_screen();
|
||||
tui_print("ECASH WALLET\n");
|
||||
tui_print("Mints: %d", wallet.mint_count);
|
||||
tui_print("Proofs: %d", wallet.proof_count);
|
||||
tui_print("^_B^:alance");
|
||||
tui_print("^_A^:dd mint");
|
||||
tui_print("^_R^:emove mint");
|
||||
tui_print("^_I^:mport token");
|
||||
tui_print("^_S^:end token");
|
||||
tui_print("E^_x^:it");
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
|
||||
if (input[0] == 'x' || input[0] == 'X' ||
|
||||
input[0] == 'q' || input[0] == 'Q') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (input[0] == 'b' || input[0] == 'B') {
|
||||
ecash_show_balance(&wallet);
|
||||
} else if (input[0] == 'a' || input[0] == 'A') {
|
||||
ecash_add_mint_menu(&wallet);
|
||||
} else if (input[0] == 'r' || input[0] == 'R') {
|
||||
ecash_remove_mint_menu(&wallet);
|
||||
} else if (input[0] == 'i' || input[0] == 'I') {
|
||||
ecash_receive_token_menu(&wallet);
|
||||
} else if (input[0] == 's' || input[0] == 'S') {
|
||||
ecash_send_token_menu(&wallet);
|
||||
}
|
||||
}
|
||||
|
||||
ecash_wallet_free(&wallet);
|
||||
}
|
||||
@@ -88,7 +88,7 @@ void menu_follows(void) {
|
||||
}
|
||||
|
||||
tui_print("\n\n");
|
||||
tui_print("P^_r^:ofile");
|
||||
tui_print("^_R^:efresh profile");
|
||||
tui_print("^_A^:dd follow");
|
||||
tui_print("^_D^:elete follow");
|
||||
tui_print("^_P^:ost changes and exit.");
|
||||
@@ -261,7 +261,8 @@ void menu_follows(void) {
|
||||
}
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
break;
|
||||
} else if (input[0] == 'x' || input[0] == 'X') {
|
||||
} else if (input[0] == 'x' || input[0] == 'X' ||
|
||||
input[0] == 'q' || input[0] == 'Q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
330
src/menu_live.c
Normal file
330
src/menu_live.c
Normal file
@@ -0,0 +1,330 @@
|
||||
#include "tui.h"
|
||||
#include "state.h"
|
||||
#include "net.h"
|
||||
|
||||
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
int term_width;
|
||||
} live_feed_ctx_t;
|
||||
|
||||
static char *live_strdup(const char *s) {
|
||||
size_t n;
|
||||
char *out;
|
||||
|
||||
if (!s) {
|
||||
return NULL;
|
||||
}
|
||||
n = strlen(s);
|
||||
out = (char *)malloc(n + 1U);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
memcpy(out, s, n + 1U);
|
||||
return out;
|
||||
}
|
||||
|
||||
static const char *live_shorten_hex(const char *hex, char *buf, size_t buf_size) {
|
||||
size_t n;
|
||||
|
||||
if (!buf || buf_size == 0U) {
|
||||
return "";
|
||||
}
|
||||
buf[0] = '\0';
|
||||
|
||||
if (!hex || hex[0] == '\0') {
|
||||
snprintf(buf, buf_size, "(unknown)");
|
||||
return buf;
|
||||
}
|
||||
|
||||
n = strlen(hex);
|
||||
if (n <= 12U) {
|
||||
snprintf(buf, buf_size, "%s", hex);
|
||||
} else {
|
||||
snprintf(buf, buf_size, "%.8s..%.4s", hex, hex + n - 4U);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
static void live_preview_text(const char *text, int max_len, char *buf, size_t buf_size) {
|
||||
size_t i;
|
||||
size_t j = 0;
|
||||
|
||||
if (!buf || buf_size == 0U) {
|
||||
return;
|
||||
}
|
||||
buf[0] = '\0';
|
||||
|
||||
if (!text || text[0] == '\0' || max_len <= 0) {
|
||||
snprintf(buf, buf_size, "");
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; text[i] != '\0' && j + 1U < buf_size; i++) {
|
||||
char ch = text[i];
|
||||
if (ch == '\n' || ch == '\r' || ch == '\t') {
|
||||
ch = ' ';
|
||||
}
|
||||
buf[j++] = ch;
|
||||
if ((int)j >= max_len) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
buf[j] = '\0';
|
||||
|
||||
if (text[i] != '\0' && j + 4U < buf_size) {
|
||||
buf[j++] = '.';
|
||||
buf[j++] = '.';
|
||||
buf[j++] = '.';
|
||||
buf[j] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static void live_event_print_cb(const char *event_json, void *userdata) {
|
||||
cJSON *ev;
|
||||
cJSON *pubkey_item;
|
||||
cJSON *content_item;
|
||||
const char *pubkey = NULL;
|
||||
const char *content = "";
|
||||
char pub_short[32];
|
||||
char preview[1024];
|
||||
int max_preview = 64;
|
||||
live_feed_ctx_t *ctx = (live_feed_ctx_t *)userdata;
|
||||
|
||||
if (!event_json) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev = cJSON_Parse(event_json);
|
||||
if (!ev) {
|
||||
return;
|
||||
}
|
||||
|
||||
pubkey_item = cJSON_GetObjectItemCaseSensitive(ev, "pubkey");
|
||||
content_item = cJSON_GetObjectItemCaseSensitive(ev, "content");
|
||||
|
||||
if (cJSON_IsString(pubkey_item) && pubkey_item->valuestring) {
|
||||
pubkey = pubkey_item->valuestring;
|
||||
}
|
||||
if (cJSON_IsString(content_item) && content_item->valuestring) {
|
||||
content = content_item->valuestring;
|
||||
}
|
||||
|
||||
if (ctx && ctx->term_width > 24) {
|
||||
max_preview = ctx->term_width - 22;
|
||||
}
|
||||
if (max_preview > (int)sizeof(preview) - 1) {
|
||||
max_preview = (int)sizeof(preview) - 1;
|
||||
}
|
||||
|
||||
live_preview_text(content, max_preview, preview, sizeof(preview));
|
||||
tui_print("%s: %s", live_shorten_hex(pubkey, pub_short, sizeof(pub_short)), preview);
|
||||
|
||||
cJSON_Delete(ev);
|
||||
}
|
||||
|
||||
static int live_extract_follow_authors(char ***authors_out, int *count_out) {
|
||||
cJSON *kind3;
|
||||
cJSON *tags;
|
||||
int i;
|
||||
int n;
|
||||
char **authors = NULL;
|
||||
int count = 0;
|
||||
|
||||
if (!authors_out || !count_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*authors_out = NULL;
|
||||
*count_out = 0;
|
||||
|
||||
if (!g_state.kind3_json) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
kind3 = cJSON_Parse(g_state.kind3_json);
|
||||
if (!kind3) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
tags = cJSON_GetObjectItemCaseSensitive(kind3, "tags");
|
||||
if (!cJSON_IsArray(tags)) {
|
||||
cJSON_Delete(kind3);
|
||||
return 0;
|
||||
}
|
||||
|
||||
n = cJSON_GetArraySize(tags);
|
||||
for (i = 0; i < n; i++) {
|
||||
cJSON *tag = cJSON_GetArrayItem(tags, i);
|
||||
cJSON *t0;
|
||||
cJSON *t1;
|
||||
char **next;
|
||||
|
||||
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
t0 = cJSON_GetArrayItem(tag, 0);
|
||||
t1 = cJSON_GetArrayItem(tag, 1);
|
||||
if (!cJSON_IsString(t0) || !t0->valuestring || strcmp(t0->valuestring, "p") != 0 ||
|
||||
!cJSON_IsString(t1) || !t1->valuestring || strlen(t1->valuestring) != 64U) {
|
||||
continue;
|
||||
}
|
||||
|
||||
next = (char **)realloc(authors, (size_t)(count + 1) * sizeof(char *));
|
||||
if (!next) {
|
||||
int k;
|
||||
for (k = 0; k < count; k++) {
|
||||
free(authors[k]);
|
||||
}
|
||||
free(authors);
|
||||
cJSON_Delete(kind3);
|
||||
return -1;
|
||||
}
|
||||
authors = next;
|
||||
authors[count] = live_strdup(t1->valuestring);
|
||||
if (!authors[count]) {
|
||||
int k;
|
||||
for (k = 0; k < count; k++) {
|
||||
free(authors[k]);
|
||||
}
|
||||
free(authors);
|
||||
cJSON_Delete(kind3);
|
||||
return -1;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
|
||||
cJSON_Delete(kind3);
|
||||
*authors_out = authors;
|
||||
*count_out = count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void live_free_authors(char **authors, int count) {
|
||||
int i;
|
||||
|
||||
if (!authors) {
|
||||
return;
|
||||
}
|
||||
for (i = 0; i < count; i++) {
|
||||
free(authors[i]);
|
||||
}
|
||||
free(authors);
|
||||
}
|
||||
|
||||
static void menu_live_run_filter(const char *title, const char *filter_json) {
|
||||
const char **read_relays;
|
||||
int relay_count = 0;
|
||||
live_feed_ctx_t ctx;
|
||||
int events_received;
|
||||
char input[8];
|
||||
|
||||
read_relays = state_get_read_relays(&relay_count);
|
||||
tui_clear_screen();
|
||||
tui_print("%s\n", title);
|
||||
tui_print("Press any key to stop...");
|
||||
|
||||
memset(&ctx, 0, sizeof(ctx));
|
||||
tui_get_terminal_size(&ctx.term_width, NULL);
|
||||
|
||||
events_received = nt_live_feed(filter_json,
|
||||
read_relays,
|
||||
relay_count,
|
||||
live_event_print_cb,
|
||||
&ctx);
|
||||
|
||||
tui_print("");
|
||||
if (events_received < 0) {
|
||||
tui_print("Feed stopped (failed to connect to all relays).");
|
||||
} else {
|
||||
tui_print("Feed stopped. %d events received.", events_received);
|
||||
}
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
}
|
||||
|
||||
void menu_live(void) {
|
||||
char input[16];
|
||||
|
||||
while (1) {
|
||||
tui_clear_screen();
|
||||
tui_print("LIVE FEEDS\n");
|
||||
tui_print("^_F^:ollows feed");
|
||||
tui_print("^_M^:entions");
|
||||
tui_print("^_G^:lobal firehose");
|
||||
tui_print("^_B^:ack");
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
|
||||
if (input[0] == 'b' || input[0] == 'B' ||
|
||||
input[0] == 'x' || input[0] == 'X' ||
|
||||
input[0] == 'q' || input[0] == 'Q') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (input[0] == 'f' || input[0] == 'F') {
|
||||
char **authors = NULL;
|
||||
int authors_count = 0;
|
||||
cJSON *filter_arr = NULL;
|
||||
cJSON *filter = NULL;
|
||||
cJSON *authors_json = NULL;
|
||||
cJSON *kinds_json = NULL;
|
||||
char *filter_str = NULL;
|
||||
int i;
|
||||
|
||||
if (live_extract_follow_authors(&authors, &authors_count) != 0 || authors_count <= 0) {
|
||||
tui_print("No follows found in kind3 list.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
live_free_authors(authors, authors_count);
|
||||
continue;
|
||||
}
|
||||
|
||||
filter_arr = cJSON_CreateArray();
|
||||
filter = cJSON_CreateObject();
|
||||
authors_json = cJSON_CreateArray();
|
||||
kinds_json = cJSON_CreateArray();
|
||||
if (!filter_arr || !filter || !authors_json || !kinds_json) {
|
||||
cJSON_Delete(filter_arr);
|
||||
cJSON_Delete(filter);
|
||||
cJSON_Delete(authors_json);
|
||||
cJSON_Delete(kinds_json);
|
||||
live_free_authors(authors, authors_count);
|
||||
tui_print("Failed to build follows filter.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (i = 0; i < authors_count; i++) {
|
||||
cJSON_AddItemToArray(authors_json, cJSON_CreateString(authors[i]));
|
||||
}
|
||||
cJSON_AddItemToArray(kinds_json, cJSON_CreateNumber(1));
|
||||
cJSON_AddItemToObject(filter, "authors", authors_json);
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds_json);
|
||||
cJSON_AddItemToArray(filter_arr, filter);
|
||||
|
||||
filter_str = cJSON_PrintUnformatted(filter_arr);
|
||||
if (filter_str) {
|
||||
menu_live_run_filter("FOLLOWS FEED", filter_str);
|
||||
free(filter_str);
|
||||
}
|
||||
|
||||
cJSON_Delete(filter_arr);
|
||||
live_free_authors(authors, authors_count);
|
||||
} else if (input[0] == 'm' || input[0] == 'M') {
|
||||
char filter[512];
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"[{\"kinds\":[1],\"#p\":[\"%s\"],\"limit\":500}]",
|
||||
g_state.npub_hex);
|
||||
menu_live_run_filter("MENTIONS", filter);
|
||||
} else if (input[0] == 'g' || input[0] == 'G') {
|
||||
char filter[128];
|
||||
snprintf(filter, sizeof(filter), "[{\"kinds\":[1],\"limit\":500}]");
|
||||
menu_live_run_filter("FIREHOSE", filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
226
src/menu_login.c
226
src/menu_login.c
@@ -1,8 +1,11 @@
|
||||
#include "tui.h"
|
||||
#include "state.h"
|
||||
#include "net.h"
|
||||
#include "db.h"
|
||||
|
||||
#include "publish.h"
|
||||
#include "signer.h"
|
||||
#include "nsigner_client.h"
|
||||
|
||||
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/nip006.h"
|
||||
@@ -158,7 +161,14 @@ void menu_login(void) {
|
||||
unsigned char pub[32];
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("LOGIN");
|
||||
tui_print("LOGIN\n");
|
||||
tui_print("^_E^:nter test/dev seed fallback");
|
||||
tui_print("^_P^:rivate key (nsec or 64-hex)");
|
||||
tui_print("^_M^:nemonic (12 words)");
|
||||
tui_print("^_S^:igner local (same qube)");
|
||||
tui_print("^_U^:RL signer (FIPS/web address)");
|
||||
tui_print("^_N^:ew account");
|
||||
tui_print("^_Q^:uit");
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
|
||||
@@ -167,6 +177,218 @@ void menu_login(void) {
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (input[0] == 'e' || input[0] == 'E') {
|
||||
input[0] = '\0';
|
||||
} else if (input[0] == 'p' || input[0] == 'P') {
|
||||
tui_get_line("Private key (nsec or 64-hex) >", input, (int)sizeof(input));
|
||||
if (input[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
} else if (input[0] == 'm' || input[0] == 'M') {
|
||||
tui_get_line("Seed phrase (12 words) >", input, (int)sizeof(input));
|
||||
if (input[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (strcmp(input, "s") == 0 || strcmp(input, "S") == 0) {
|
||||
char **names = NULL;
|
||||
int name_count = 0;
|
||||
char pubkey_hex[65] = {0};
|
||||
int selected = 0;
|
||||
|
||||
tui_print("Searching for running n_signer instances...");
|
||||
|
||||
if (nsigner_list(&names, &name_count) != 0 || name_count == 0) {
|
||||
tui_print("No running n_signer found. Start `nsigner` and try again.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name_count == 1) {
|
||||
tui_print("Found: @%s", names[0]);
|
||||
selected = 0;
|
||||
} else {
|
||||
int idx;
|
||||
char idxbuf[16];
|
||||
tui_print("Found %d n_signer instances:", name_count);
|
||||
for (idx = 0; idx < name_count; idx++) {
|
||||
tui_print(" [%d] @%s", idx, names[idx]);
|
||||
}
|
||||
tui_get_line("Select (0) >", idxbuf, (int)sizeof(idxbuf));
|
||||
selected = (idxbuf[0] != '\0') ? atoi(idxbuf) : 0;
|
||||
if (selected < 0 || selected >= name_count) {
|
||||
selected = 0;
|
||||
}
|
||||
}
|
||||
|
||||
tui_print("Waiting for n_signer approval...");
|
||||
if (nsigner_get_public_key(names[selected], "main", pubkey_hex) != 0) {
|
||||
int i;
|
||||
tui_print("Failed to get public key from n_signer (denied or error).");
|
||||
for (i = 0; i < name_count; i++) {
|
||||
free(names[i]);
|
||||
}
|
||||
free(names);
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
continue;
|
||||
}
|
||||
|
||||
signer_init_nsigner(names[selected]);
|
||||
|
||||
snprintf(g_state.npub_hex, sizeof(g_state.npub_hex), "%s", pubkey_hex);
|
||||
{
|
||||
unsigned char pub_bytes[32];
|
||||
nostr_hex_to_bytes(pubkey_hex, pub_bytes, 32);
|
||||
nostr_key_to_bech32(pub_bytes, "npub", g_state.npub_bech32);
|
||||
}
|
||||
g_state.nsec_hex[0] = '\0';
|
||||
g_state.nsec_bech32[0] = '\0';
|
||||
g_state.seed_phrase[0] = '\0';
|
||||
g_state.logged_in = 1;
|
||||
|
||||
tui_print("Signed in via n_signer (@%s)", names[selected]);
|
||||
tui_print("npub: %s", g_state.npub_bech32);
|
||||
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < name_count; i++) {
|
||||
free(names[i]);
|
||||
}
|
||||
free(names);
|
||||
}
|
||||
|
||||
(void)state_load_user_info();
|
||||
|
||||
if (g_state.kind0_json && strcmp(g_state.kind0_json, "{}") == 0) {
|
||||
tui_print("No profile found on relays. Setting up new user...");
|
||||
menu_add_new_user();
|
||||
(void)state_load_user_info();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(input, "u") == 0 || strcmp(input, "U") == 0) {
|
||||
char pubkey_hex[65] = {0};
|
||||
char endpoint_url[256];
|
||||
const char *saved_endpoint = db_get_local("nsigner_url_endpoint");
|
||||
|
||||
snprintf(endpoint_url, sizeof(endpoint_url), "%s", (saved_endpoint && saved_endpoint[0] != '\0') ? saved_endpoint : "");
|
||||
tui_get_line("Signer URL (http://<npub>.fips:8080) >", endpoint_url, (int)sizeof(endpoint_url));
|
||||
if (endpoint_url[0] == '\0') {
|
||||
if (saved_endpoint && saved_endpoint[0] != '\0') {
|
||||
snprintf(endpoint_url, sizeof(endpoint_url), "%s", saved_endpoint);
|
||||
} else {
|
||||
tui_print("No URL provided.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
tui_print("Connecting to n_signer URL: %s", endpoint_url);
|
||||
tui_print("Waiting for n_signer approval...");
|
||||
|
||||
if (nsigner_get_public_key_url(endpoint_url, "main", pubkey_hex) != 0) {
|
||||
tui_print("Failed to get public key from n_signer URL (denied or error).");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (signer_init_nsigner_url(endpoint_url) != 0) {
|
||||
tui_print("Failed to initialize URL signer mode.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
continue;
|
||||
}
|
||||
|
||||
(void)db_set_local("nsigner_url_endpoint", endpoint_url);
|
||||
|
||||
snprintf(g_state.npub_hex, sizeof(g_state.npub_hex), "%s", pubkey_hex);
|
||||
{
|
||||
unsigned char pub_bytes[32];
|
||||
nostr_hex_to_bytes(pubkey_hex, pub_bytes, 32);
|
||||
nostr_key_to_bech32(pub_bytes, "npub", g_state.npub_bech32);
|
||||
}
|
||||
g_state.nsec_hex[0] = '\0';
|
||||
g_state.nsec_bech32[0] = '\0';
|
||||
g_state.seed_phrase[0] = '\0';
|
||||
g_state.logged_in = 1;
|
||||
|
||||
tui_print("Signed in via n_signer URL (%s)", endpoint_url);
|
||||
tui_print("npub: %s", g_state.npub_bech32);
|
||||
|
||||
(void)state_load_user_info();
|
||||
|
||||
if (g_state.kind0_json && strcmp(g_state.kind0_json, "{}") == 0) {
|
||||
tui_print("No profile found on relays. Setting up new user...");
|
||||
menu_add_new_user();
|
||||
(void)state_load_user_info();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(input, "S") == 0) {
|
||||
char pubkey_hex[65] = {0};
|
||||
char target_qube[128];
|
||||
char service_name[128];
|
||||
const char *saved_target = db_get_local("nsigner_qrexec_target");
|
||||
|
||||
snprintf(target_qube, sizeof(target_qube), "%s", (saved_target && saved_target[0] != '\0') ? saved_target : "nsigner-vault");
|
||||
snprintf(service_name, sizeof(service_name), "%s", "qubes.NsignerRpc");
|
||||
|
||||
tui_get_line("Target signer qube (nsigner-vault) >", target_qube, (int)sizeof(target_qube));
|
||||
if (target_qube[0] == '\0') {
|
||||
snprintf(target_qube, sizeof(target_qube), "%s", (saved_target && saved_target[0] != '\0') ? saved_target : "nsigner-vault");
|
||||
}
|
||||
|
||||
tui_get_line("Qrexec service (qubes.NsignerRpc) >", service_name, (int)sizeof(service_name));
|
||||
if (service_name[0] == '\0') {
|
||||
snprintf(service_name, sizeof(service_name), "%s", "qubes.NsignerRpc");
|
||||
}
|
||||
|
||||
tui_print("Calling qrexec service %s in %s...", service_name, target_qube);
|
||||
tui_print("Waiting for n_signer approval...");
|
||||
|
||||
if (nsigner_qrexec_get_public_key(target_qube, service_name, "main", pubkey_hex) != 0) {
|
||||
tui_print("Failed to get public key via qrexec (denied or error).");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (signer_init_nsigner_qrexec(target_qube, service_name) != 0) {
|
||||
tui_print("Failed to initialize qrexec signer mode.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
continue;
|
||||
}
|
||||
|
||||
(void)db_set_local("nsigner_qrexec_target", target_qube);
|
||||
|
||||
snprintf(g_state.npub_hex, sizeof(g_state.npub_hex), "%s", pubkey_hex);
|
||||
{
|
||||
unsigned char pub_bytes[32];
|
||||
nostr_hex_to_bytes(pubkey_hex, pub_bytes, 32);
|
||||
nostr_key_to_bech32(pub_bytes, "npub", g_state.npub_bech32);
|
||||
}
|
||||
g_state.nsec_hex[0] = '\0';
|
||||
g_state.nsec_bech32[0] = '\0';
|
||||
g_state.seed_phrase[0] = '\0';
|
||||
g_state.logged_in = 1;
|
||||
|
||||
tui_print("Signed in via qrexec n_signer (%s:%s)", target_qube, service_name);
|
||||
tui_print("npub: %s", g_state.npub_bech32);
|
||||
|
||||
(void)state_load_user_info();
|
||||
|
||||
if (g_state.kind0_json && strcmp(g_state.kind0_json, "{}") == 0) {
|
||||
tui_print("No profile found on relays. Setting up new user...");
|
||||
menu_add_new_user();
|
||||
(void)state_load_user_info();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(input, "n") == 0) {
|
||||
char mnemonic[256];
|
||||
char idxbuf[32];
|
||||
@@ -302,7 +524,7 @@ void menu_login(void) {
|
||||
}
|
||||
}
|
||||
|
||||
tui_print("Invalid login input. Enter 12 words, nsec, 64-hex, [n], or [q].");
|
||||
tui_print("Invalid login input. Choose a menu command or enter valid key/mnemonic data.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
}
|
||||
}
|
||||
|
||||
309
src/menu_notifications.c
Normal file
309
src/menu_notifications.c
Normal file
@@ -0,0 +1,309 @@
|
||||
#include "tui.h"
|
||||
#include "state.h"
|
||||
#include "net.h"
|
||||
|
||||
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
typedef struct {
|
||||
int kind;
|
||||
long long created_at;
|
||||
char *pubkey;
|
||||
char *event_ref;
|
||||
char *content;
|
||||
} notif_item_t;
|
||||
|
||||
static char *notif_strdup(const char *s) {
|
||||
size_t n;
|
||||
char *out;
|
||||
|
||||
if (!s) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
n = strlen(s);
|
||||
out = (char *)malloc(n + 1U);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
memcpy(out, s, n + 1U);
|
||||
return out;
|
||||
}
|
||||
|
||||
static void notif_free_items(notif_item_t *items, int count) {
|
||||
int i;
|
||||
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
free(items[i].pubkey);
|
||||
free(items[i].event_ref);
|
||||
free(items[i].content);
|
||||
}
|
||||
|
||||
free(items);
|
||||
}
|
||||
|
||||
static const char *notif_shorten_hex(const char *hex, char *buf, size_t buf_size) {
|
||||
size_t n;
|
||||
|
||||
if (!buf || buf_size == 0U) {
|
||||
return "";
|
||||
}
|
||||
|
||||
buf[0] = '\0';
|
||||
if (!hex || hex[0] == '\0') {
|
||||
return "(unknown)";
|
||||
}
|
||||
|
||||
n = strlen(hex);
|
||||
if (n <= 16U) {
|
||||
snprintf(buf, buf_size, "%s", hex);
|
||||
return buf;
|
||||
}
|
||||
|
||||
snprintf(buf, buf_size, "%.8s..%.8s", hex, hex + (n - 8U));
|
||||
return buf;
|
||||
}
|
||||
|
||||
static const char *notif_preview_text(const char *text, char *buf, size_t buf_size) {
|
||||
size_t i;
|
||||
size_t j = 0;
|
||||
|
||||
if (!buf || buf_size == 0U) {
|
||||
return "";
|
||||
}
|
||||
|
||||
buf[0] = '\0';
|
||||
if (!text || text[0] == '\0') {
|
||||
snprintf(buf, buf_size, "(no content)");
|
||||
return buf;
|
||||
}
|
||||
|
||||
for (i = 0; text[i] != '\0' && j + 1U < buf_size; i++) {
|
||||
char ch = text[i];
|
||||
if (ch == '\n' || ch == '\r' || ch == '\t') {
|
||||
ch = ' ';
|
||||
}
|
||||
buf[j++] = ch;
|
||||
if (j >= 120U) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
buf[j] = '\0';
|
||||
|
||||
if (text[i] != '\0' && j + 4U < buf_size) {
|
||||
buf[j++] = '.';
|
||||
buf[j++] = '.';
|
||||
buf[j++] = '.';
|
||||
buf[j] = '\0';
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
static int notif_push_item(notif_item_t **items, int *count, notif_item_t *src) {
|
||||
notif_item_t *next;
|
||||
|
||||
if (!items || !count || !src) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
next = (notif_item_t *)realloc(*items, (size_t)(*count + 1) * sizeof(notif_item_t));
|
||||
if (!next) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*items = next;
|
||||
(*items)[*count] = *src;
|
||||
(*count)++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char *notif_extract_e_tag_ref(cJSON *tags) {
|
||||
int i;
|
||||
int n;
|
||||
|
||||
if (!cJSON_IsArray(tags)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
n = cJSON_GetArraySize(tags);
|
||||
for (i = 0; i < n; i++) {
|
||||
cJSON *tag = cJSON_GetArrayItem(tags, i);
|
||||
cJSON *t0;
|
||||
cJSON *t1;
|
||||
|
||||
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
t0 = cJSON_GetArrayItem(tag, 0);
|
||||
t1 = cJSON_GetArrayItem(tag, 1);
|
||||
if (cJSON_IsString(t0) && t0->valuestring && strcmp(t0->valuestring, "e") == 0 &&
|
||||
cJSON_IsString(t1) && t1->valuestring && t1->valuestring[0] != '\0') {
|
||||
return notif_strdup(t1->valuestring);
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int notif_cmp_desc_created(const void *a, const void *b) {
|
||||
const notif_item_t *ia = (const notif_item_t *)a;
|
||||
const notif_item_t *ib = (const notif_item_t *)b;
|
||||
|
||||
if (ia->created_at < ib->created_at) {
|
||||
return 1;
|
||||
}
|
||||
if (ia->created_at > ib->created_at) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void menu_notifications(void) {
|
||||
const char **read_relays;
|
||||
int relay_count = 0;
|
||||
time_t now;
|
||||
long long since;
|
||||
char filter[512];
|
||||
char **events = NULL;
|
||||
int event_count = 0;
|
||||
notif_item_t *items = NULL;
|
||||
int items_count = 0;
|
||||
int reactions = 0;
|
||||
int replies = 0;
|
||||
int i;
|
||||
char input[32];
|
||||
|
||||
read_relays = state_get_read_relays(&relay_count);
|
||||
|
||||
now = time(NULL);
|
||||
since = (long long)now - (3LL * 24LL * 60LL * 60LL);
|
||||
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"{\"#p\":[\"%s\"],\"since\":%lld,\"kinds\":[1,7],\"limit\":300}",
|
||||
g_state.npub_hex,
|
||||
since);
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("NOTIFICATIONS\n");
|
||||
tui_print("Querying %d read relay(s)...", relay_count);
|
||||
|
||||
if (nt_query_sync(filter, read_relays, relay_count, 7000, &events, &event_count) != 0) {
|
||||
tui_print("Failed to query notifications (all relays failed).");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < event_count; i++) {
|
||||
cJSON *ev = cJSON_Parse(events[i]);
|
||||
cJSON *kind_item;
|
||||
cJSON *created_item;
|
||||
cJSON *pubkey_item;
|
||||
cJSON *content_item;
|
||||
cJSON *tags_item;
|
||||
notif_item_t it;
|
||||
|
||||
if (!ev) {
|
||||
continue;
|
||||
}
|
||||
|
||||
kind_item = cJSON_GetObjectItemCaseSensitive(ev, "kind");
|
||||
created_item = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
|
||||
pubkey_item = cJSON_GetObjectItemCaseSensitive(ev, "pubkey");
|
||||
content_item = cJSON_GetObjectItemCaseSensitive(ev, "content");
|
||||
tags_item = cJSON_GetObjectItemCaseSensitive(ev, "tags");
|
||||
|
||||
if (!cJSON_IsNumber(kind_item) || !cJSON_IsNumber(created_item) ||
|
||||
!cJSON_IsString(pubkey_item) || !pubkey_item->valuestring) {
|
||||
cJSON_Delete(ev);
|
||||
continue;
|
||||
}
|
||||
|
||||
memset(&it, 0, sizeof(it));
|
||||
it.kind = (int)kind_item->valuedouble;
|
||||
it.created_at = (long long)created_item->valuedouble;
|
||||
it.pubkey = notif_strdup(pubkey_item->valuestring);
|
||||
it.content = (cJSON_IsString(content_item) && content_item->valuestring)
|
||||
? notif_strdup(content_item->valuestring)
|
||||
: notif_strdup("");
|
||||
it.event_ref = notif_extract_e_tag_ref(tags_item);
|
||||
|
||||
if (!it.pubkey || !it.content) {
|
||||
free(it.pubkey);
|
||||
free(it.content);
|
||||
free(it.event_ref);
|
||||
cJSON_Delete(ev);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (it.kind == 7) {
|
||||
reactions++;
|
||||
} else if (it.kind == 1) {
|
||||
replies++;
|
||||
}
|
||||
|
||||
if (it.kind == 7 || it.kind == 1) {
|
||||
if (notif_push_item(&items, &items_count, &it) != 0) {
|
||||
free(it.pubkey);
|
||||
free(it.content);
|
||||
free(it.event_ref);
|
||||
}
|
||||
} else {
|
||||
free(it.pubkey);
|
||||
free(it.content);
|
||||
free(it.event_ref);
|
||||
}
|
||||
|
||||
cJSON_Delete(ev);
|
||||
}
|
||||
|
||||
if (items_count > 1) {
|
||||
qsort(items, (size_t)items_count, sizeof(notif_item_t), notif_cmp_desc_created);
|
||||
}
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("NOTIFICATIONS\n");
|
||||
tui_print("%d reactions, %d replies in last 3 days", reactions, replies);
|
||||
tui_print("");
|
||||
|
||||
for (i = 0; i < items_count; i++) {
|
||||
char pub_short[32];
|
||||
|
||||
if (items[i].kind == 7) {
|
||||
char ev_short[32];
|
||||
tui_print("♥ %s reacted to %s",
|
||||
notif_shorten_hex(items[i].pubkey, pub_short, sizeof(pub_short)),
|
||||
notif_shorten_hex(items[i].event_ref ? items[i].event_ref : "(unknown)",
|
||||
ev_short,
|
||||
sizeof(ev_short)));
|
||||
} else if (items[i].kind == 1) {
|
||||
char preview[160];
|
||||
tui_print("↩ %s replied: %s",
|
||||
notif_shorten_hex(items[i].pubkey, pub_short, sizeof(pub_short)),
|
||||
notif_preview_text(items[i].content, preview, sizeof(preview)));
|
||||
}
|
||||
}
|
||||
|
||||
if (items_count == 0) {
|
||||
tui_print("No notifications found.");
|
||||
}
|
||||
|
||||
for (i = 0; i < event_count; i++) {
|
||||
free(events[i]);
|
||||
}
|
||||
free(events);
|
||||
|
||||
notif_free_items(items, items_count);
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
}
|
||||
551
src/menu_posts.c
Normal file
551
src/menu_posts.c
Normal file
@@ -0,0 +1,551 @@
|
||||
#include "tui.h"
|
||||
#include "state.h"
|
||||
#include "net.h"
|
||||
#include "publish.h"
|
||||
#include "editor.h"
|
||||
#include "signer.h"
|
||||
|
||||
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/nip004.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/utils.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
typedef struct {
|
||||
char *event_json;
|
||||
char *id;
|
||||
int kind;
|
||||
long long created_at;
|
||||
char *pubkey;
|
||||
char *content;
|
||||
cJSON *tags;
|
||||
char *title;
|
||||
char *dtag;
|
||||
} post_item_t;
|
||||
|
||||
static char *posts_strdup(const char *s) {
|
||||
size_t n;
|
||||
char *out;
|
||||
|
||||
if (!s) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
n = strlen(s);
|
||||
out = (char *)malloc(n + 1U);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
memcpy(out, s, n + 1U);
|
||||
return out;
|
||||
}
|
||||
|
||||
static void posts_free_items(post_item_t *items, int count) {
|
||||
int i;
|
||||
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
free(items[i].event_json);
|
||||
free(items[i].id);
|
||||
free(items[i].pubkey);
|
||||
free(items[i].content);
|
||||
cJSON_Delete(items[i].tags);
|
||||
free(items[i].title);
|
||||
free(items[i].dtag);
|
||||
}
|
||||
|
||||
free(items);
|
||||
}
|
||||
|
||||
static int posts_cmp_desc_created(const void *a, const void *b) {
|
||||
const post_item_t *ia = (const post_item_t *)a;
|
||||
const post_item_t *ib = (const post_item_t *)b;
|
||||
|
||||
if (ia->created_at < ib->created_at) {
|
||||
return 1;
|
||||
}
|
||||
if (ia->created_at > ib->created_at) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char *posts_find_tag_value(cJSON *tags, const char *tag_name) {
|
||||
int i;
|
||||
int n;
|
||||
|
||||
if (!cJSON_IsArray(tags) || !tag_name) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
n = cJSON_GetArraySize(tags);
|
||||
for (i = 0; i < n; i++) {
|
||||
cJSON *tag = cJSON_GetArrayItem(tags, i);
|
||||
cJSON *t0;
|
||||
cJSON *t1;
|
||||
|
||||
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
t0 = cJSON_GetArrayItem(tag, 0);
|
||||
t1 = cJSON_GetArrayItem(tag, 1);
|
||||
if (cJSON_IsString(t0) && t0->valuestring && strcmp(t0->valuestring, tag_name) == 0 &&
|
||||
cJSON_IsString(t1) && t1->valuestring) {
|
||||
return posts_strdup(t1->valuestring);
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char *posts_try_decrypt_nip04(const char *sender_pub_hex, const char *ciphertext) {
|
||||
char *out = NULL;
|
||||
|
||||
if (signer_nip04_decrypt(sender_pub_hex, ciphertext, &out) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static int posts_encrypt_nip04_for_self(const char *plaintext, char **cipher_out) {
|
||||
return signer_nip04_encrypt(g_state.npub_hex, plaintext, cipher_out);
|
||||
}
|
||||
|
||||
static int posts_push_item(post_item_t **items, int *count, post_item_t *src) {
|
||||
post_item_t *next;
|
||||
|
||||
if (!items || !count || !src) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
next = (post_item_t *)realloc(*items, (size_t)(*count + 1) * sizeof(post_item_t));
|
||||
if (!next) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*items = next;
|
||||
(*items)[*count] = *src;
|
||||
(*count)++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void posts_format_date(long long created_at, char *buf, size_t buf_sz) {
|
||||
time_t t;
|
||||
struct tm tmv;
|
||||
|
||||
if (!buf || buf_sz == 0U) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (created_at <= 0) {
|
||||
snprintf(buf, buf_sz, "(no date)");
|
||||
return;
|
||||
}
|
||||
|
||||
t = (time_t)created_at;
|
||||
memset(&tmv, 0, sizeof(tmv));
|
||||
if (!localtime_r(&t, &tmv)) {
|
||||
snprintf(buf, buf_sz, "%lld", created_at);
|
||||
return;
|
||||
}
|
||||
|
||||
if (strftime(buf, buf_sz, "%Y-%m-%d %H:%M", &tmv) == 0) {
|
||||
snprintf(buf, buf_sz, "%lld", created_at);
|
||||
}
|
||||
}
|
||||
|
||||
static int posts_build_list(const char *filter_json, post_item_t **items_out, int *count_out) {
|
||||
const char **read_relays;
|
||||
int relay_count = 0;
|
||||
char **events = NULL;
|
||||
int event_count = 0;
|
||||
post_item_t *items = NULL;
|
||||
int items_count = 0;
|
||||
int i;
|
||||
|
||||
if (!filter_json || !items_out || !count_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*items_out = NULL;
|
||||
*count_out = 0;
|
||||
|
||||
read_relays = state_get_read_relays(&relay_count);
|
||||
if (nt_query_sync(filter_json, read_relays, relay_count, 7000, &events, &event_count) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (i = 0; i < event_count; i++) {
|
||||
cJSON *ev = cJSON_Parse(events[i]);
|
||||
cJSON *id_item;
|
||||
cJSON *kind_item;
|
||||
cJSON *created_item;
|
||||
cJSON *pubkey_item;
|
||||
cJSON *content_item;
|
||||
cJSON *tags_item;
|
||||
post_item_t it;
|
||||
|
||||
if (!ev) {
|
||||
continue;
|
||||
}
|
||||
|
||||
memset(&it, 0, sizeof(it));
|
||||
|
||||
id_item = cJSON_GetObjectItemCaseSensitive(ev, "id");
|
||||
kind_item = cJSON_GetObjectItemCaseSensitive(ev, "kind");
|
||||
created_item = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
|
||||
pubkey_item = cJSON_GetObjectItemCaseSensitive(ev, "pubkey");
|
||||
content_item = cJSON_GetObjectItemCaseSensitive(ev, "content");
|
||||
tags_item = cJSON_GetObjectItemCaseSensitive(ev, "tags");
|
||||
|
||||
if (!cJSON_IsString(id_item) || !id_item->valuestring ||
|
||||
!cJSON_IsNumber(kind_item) ||
|
||||
!cJSON_IsNumber(created_item) ||
|
||||
!cJSON_IsString(pubkey_item) || !pubkey_item->valuestring) {
|
||||
cJSON_Delete(ev);
|
||||
continue;
|
||||
}
|
||||
|
||||
it.event_json = posts_strdup(events[i]);
|
||||
it.id = posts_strdup(id_item->valuestring);
|
||||
it.kind = (int)kind_item->valuedouble;
|
||||
it.created_at = (long long)created_item->valuedouble;
|
||||
it.pubkey = posts_strdup(pubkey_item->valuestring);
|
||||
it.content = (cJSON_IsString(content_item) && content_item->valuestring)
|
||||
? posts_strdup(content_item->valuestring)
|
||||
: posts_strdup("");
|
||||
it.tags = cJSON_IsArray(tags_item) ? cJSON_Duplicate(tags_item, 1) : cJSON_CreateArray();
|
||||
it.dtag = posts_find_tag_value(it.tags, "d");
|
||||
it.title = posts_find_tag_value(it.tags, "title");
|
||||
|
||||
if (!it.event_json || !it.id || !it.pubkey || !it.content || !it.tags) {
|
||||
free(it.event_json);
|
||||
free(it.id);
|
||||
free(it.pubkey);
|
||||
free(it.content);
|
||||
cJSON_Delete(it.tags);
|
||||
free(it.title);
|
||||
free(it.dtag);
|
||||
cJSON_Delete(ev);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (posts_push_item(&items, &items_count, &it) != 0) {
|
||||
free(it.event_json);
|
||||
free(it.id);
|
||||
free(it.pubkey);
|
||||
free(it.content);
|
||||
cJSON_Delete(it.tags);
|
||||
free(it.title);
|
||||
free(it.dtag);
|
||||
}
|
||||
|
||||
cJSON_Delete(ev);
|
||||
}
|
||||
|
||||
for (i = 0; i < event_count; i++) {
|
||||
free(events[i]);
|
||||
}
|
||||
free(events);
|
||||
|
||||
if (items_count > 1) {
|
||||
qsort(items, (size_t)items_count, sizeof(post_item_t), posts_cmp_desc_created);
|
||||
}
|
||||
|
||||
*items_out = items;
|
||||
*count_out = items_count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const char *posts_kind_label(int kind) {
|
||||
if (kind == 30023) {
|
||||
return "public";
|
||||
}
|
||||
if (kind == 30024) {
|
||||
return "private";
|
||||
}
|
||||
if (kind == 30078) {
|
||||
return "encrypted";
|
||||
}
|
||||
return "post";
|
||||
}
|
||||
|
||||
static void posts_view_item(const post_item_t *it) {
|
||||
char date_buf[64];
|
||||
char input[16];
|
||||
char *decrypted = NULL;
|
||||
|
||||
if (!it) {
|
||||
return;
|
||||
}
|
||||
|
||||
posts_format_date(it->created_at, date_buf, sizeof(date_buf));
|
||||
|
||||
if (it->kind == 30024 || it->kind == 30078) {
|
||||
decrypted = posts_try_decrypt_nip04(it->pubkey, it->content);
|
||||
}
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("POST VIEW\n");
|
||||
tui_print("id: %s", it->id);
|
||||
tui_print("kind: %d (%s)", it->kind, posts_kind_label(it->kind));
|
||||
tui_print("date: %s", date_buf);
|
||||
tui_print("d: %s", (it->dtag && it->dtag[0] != '\0') ? it->dtag : "");
|
||||
tui_print("title: %s", (it->title && it->title[0] != '\0') ? it->title : "");
|
||||
tui_print("");
|
||||
|
||||
if (decrypted) {
|
||||
tui_print("%s", decrypted);
|
||||
} else if (it->kind == 30024 || it->kind == 30078) {
|
||||
tui_print("(decrypt failed; showing raw content)");
|
||||
tui_print("%s", it->content);
|
||||
} else {
|
||||
tui_print("%s", it->content);
|
||||
}
|
||||
|
||||
free(decrypted);
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
}
|
||||
|
||||
static void posts_delete_item(const post_item_t *it) {
|
||||
cJSON *tags;
|
||||
cJSON *e_tag;
|
||||
char input[16];
|
||||
int posted;
|
||||
|
||||
if (!it || !it->id) {
|
||||
return;
|
||||
}
|
||||
|
||||
tags = cJSON_CreateArray();
|
||||
if (!tags) {
|
||||
return;
|
||||
}
|
||||
|
||||
e_tag = cJSON_CreateArray();
|
||||
if (!e_tag) {
|
||||
cJSON_Delete(tags);
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(e_tag, cJSON_CreateString("e"));
|
||||
cJSON_AddItemToArray(e_tag, cJSON_CreateString(it->id));
|
||||
cJSON_AddItemToArray(tags, e_tag);
|
||||
|
||||
posted = publish_signed_event_with_report("Delete", 5, "", tags, 8000);
|
||||
cJSON_Delete(tags);
|
||||
|
||||
if (posted < 0) {
|
||||
tui_print("Delete publish failed.");
|
||||
} else {
|
||||
tui_print("Deletion published.");
|
||||
}
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
}
|
||||
|
||||
static void posts_edit_item(const post_item_t *it) {
|
||||
char *initial = NULL;
|
||||
char *edited = NULL;
|
||||
char *out_content = NULL;
|
||||
cJSON *tags_copy = NULL;
|
||||
int posted;
|
||||
char input[16];
|
||||
|
||||
if (!it) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (it->kind == 30024 || it->kind == 30078) {
|
||||
initial = posts_try_decrypt_nip04(it->pubkey, it->content);
|
||||
if (!initial) {
|
||||
initial = posts_strdup("");
|
||||
}
|
||||
} else {
|
||||
initial = posts_strdup(it->content ? it->content : "");
|
||||
}
|
||||
|
||||
if (!initial) {
|
||||
tui_print("Failed to prepare editor content.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
return;
|
||||
}
|
||||
|
||||
edited = editor_launch(initial);
|
||||
free(initial);
|
||||
|
||||
if (!edited) {
|
||||
tui_print("Edit canceled.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
return;
|
||||
}
|
||||
|
||||
if (it->kind == 30024 || it->kind == 30078) {
|
||||
if (posts_encrypt_nip04_for_self(edited, &out_content) != 0) {
|
||||
tui_print("Failed to encrypt updated content.");
|
||||
free(edited);
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
out_content = posts_strdup(edited);
|
||||
if (!out_content) {
|
||||
free(edited);
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
tags_copy = it->tags ? cJSON_Duplicate(it->tags, 1) : cJSON_CreateArray();
|
||||
if (!tags_copy) {
|
||||
free(edited);
|
||||
free(out_content);
|
||||
tui_print("Failed to duplicate tags.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
return;
|
||||
}
|
||||
|
||||
posted = publish_signed_event_with_report("Edit post", it->kind, out_content, tags_copy, 8000);
|
||||
cJSON_Delete(tags_copy);
|
||||
free(edited);
|
||||
free(out_content);
|
||||
|
||||
if (posted < 0) {
|
||||
tui_print("Post update failed.");
|
||||
} else {
|
||||
tui_print("Post updated.");
|
||||
}
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
}
|
||||
|
||||
static void posts_kind_menu(const char *title, const char *filter_json) {
|
||||
post_item_t *items = NULL;
|
||||
int items_count = 0;
|
||||
char input[32];
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("%s\n", title);
|
||||
|
||||
if (posts_build_list(filter_json, &items, &items_count) != 0) {
|
||||
tui_print("Failed to query posts (all relays failed).");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
return;
|
||||
}
|
||||
|
||||
while (1) {
|
||||
int i;
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("%s\n", title);
|
||||
for (i = 0; i < items_count; i++) {
|
||||
char date_buf[64];
|
||||
const char *label;
|
||||
|
||||
posts_format_date(items[i].created_at, date_buf, sizeof(date_buf));
|
||||
label = (items[i].title && items[i].title[0] != '\0')
|
||||
? items[i].title
|
||||
: ((items[i].dtag && items[i].dtag[0] != '\0') ? items[i].dtag : "(untitled)");
|
||||
|
||||
tui_print("[%2d] (%d/%s) %s %s",
|
||||
i + 1,
|
||||
items[i].kind,
|
||||
posts_kind_label(items[i].kind),
|
||||
label,
|
||||
date_buf);
|
||||
}
|
||||
|
||||
if (items_count == 0) {
|
||||
tui_print("No posts found.");
|
||||
}
|
||||
|
||||
tui_print("");
|
||||
tui_print("^_V^:iew <n>");
|
||||
tui_print("^_E^:dit <n>");
|
||||
tui_print("^_D^:elete <n>");
|
||||
tui_print("^_B^:ack");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
|
||||
if (input[0] == 'b' || input[0] == 'B' ||
|
||||
input[0] == 'x' || input[0] == 'X' ||
|
||||
input[0] == 'q' || input[0] == 'Q') {
|
||||
break;
|
||||
}
|
||||
|
||||
if ((input[0] == 'v' || input[0] == 'V' ||
|
||||
input[0] == 'e' || input[0] == 'E' ||
|
||||
input[0] == 'd' || input[0] == 'D') && input[1] != '\0') {
|
||||
int idx = atoi(input + 1) - 1;
|
||||
if (idx < 0 || idx >= items_count) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (input[0] == 'v' || input[0] == 'V') {
|
||||
posts_view_item(&items[idx]);
|
||||
} else if (input[0] == 'e' || input[0] == 'E') {
|
||||
posts_edit_item(&items[idx]);
|
||||
} else if (input[0] == 'd' || input[0] == 'D') {
|
||||
posts_delete_item(&items[idx]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
posts_free_items(items, items_count);
|
||||
}
|
||||
|
||||
void menu_posts(void) {
|
||||
char input[32];
|
||||
char filter[512];
|
||||
|
||||
while (1) {
|
||||
tui_clear_screen();
|
||||
tui_print("POSTS\n");
|
||||
tui_print("^_S^:ecret blog (kind 30024)");
|
||||
tui_print("^_P^:ublic blog (kind 30023)");
|
||||
tui_print("^_E^:ncrypted data (kind 30078)");
|
||||
tui_print("^_A^:ll posts");
|
||||
tui_print("^_B^:ack");
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
|
||||
if (input[0] == 'b' || input[0] == 'B' ||
|
||||
input[0] == 'x' || input[0] == 'X' ||
|
||||
input[0] == 'q' || input[0] == 'Q') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (input[0] == 's' || input[0] == 'S') {
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"{\"authors\":[\"%s\"],\"kinds\":[30024],\"limit\":200}",
|
||||
g_state.npub_hex);
|
||||
posts_kind_menu("PRIVATE BLOG", filter);
|
||||
} else if (input[0] == 'p' || input[0] == 'P') {
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"{\"authors\":[\"%s\"],\"kinds\":[30023],\"limit\":200}",
|
||||
g_state.npub_hex);
|
||||
posts_kind_menu("PUBLIC BLOG", filter);
|
||||
} else if (input[0] == 'e' || input[0] == 'E') {
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"{\"authors\":[\"%s\"],\"kinds\":[30078],\"limit\":200}",
|
||||
g_state.npub_hex);
|
||||
posts_kind_menu("ENCRYPTED DATA", filter);
|
||||
} else if (input[0] == 'a' || input[0] == 'A') {
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"{\"authors\":[\"%s\"],\"kinds\":[30023,30024,30078],\"limit\":300}",
|
||||
g_state.npub_hex);
|
||||
posts_kind_menu("ALL POSTS", filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "tui.h"
|
||||
#include "state.h"
|
||||
#include "net.h"
|
||||
#include "signer.h"
|
||||
|
||||
#include "publish.h"
|
||||
|
||||
@@ -26,8 +27,19 @@ void menu_profile(void) {
|
||||
tui_clear_screen();
|
||||
tui_print("PROFILE\n");
|
||||
|
||||
tui_print("nsecHex: %s", g_state.nsec_hex);
|
||||
tui_print("nsec: %s", g_state.nsec_bech32);
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER) {
|
||||
tui_print("Signer: n_signer (@%s)", g_signer.socket_name);
|
||||
} else if (g_signer.kind == NT_SIGNER_NSIGNER_QREXEC) {
|
||||
tui_print("Signer: n_signer qrexec");
|
||||
tui_print("Signer target: %s", g_signer.qrexec_target);
|
||||
tui_print("Signer service: %s", g_signer.qrexec_service);
|
||||
} else if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
|
||||
tui_print("Signer: n_signer URL");
|
||||
tui_print("Signer endpoint: %s", g_signer.endpoint_url);
|
||||
} else {
|
||||
tui_print("nsecHex: %s", g_state.nsec_hex);
|
||||
tui_print("nsec: %s", g_state.nsec_bech32);
|
||||
}
|
||||
tui_print("npubHex: %s", g_state.npub_hex);
|
||||
tui_print("npub: %s", g_state.npub_bech32);
|
||||
tui_print("");
|
||||
|
||||
@@ -27,6 +27,74 @@ static int relays_publish_kind10002(cJSON *event_obj) {
|
||||
8000);
|
||||
}
|
||||
|
||||
static void relays_print_nip11_summary(const char *nip11_json) {
|
||||
cJSON *doc;
|
||||
cJSON *name;
|
||||
cJSON *description;
|
||||
cJSON *software;
|
||||
cJSON *version;
|
||||
cJSON *pubkey;
|
||||
cJSON *contact;
|
||||
cJSON *supported_nips;
|
||||
cJSON *limitation;
|
||||
|
||||
if (!nip11_json || nip11_json[0] == '\0') {
|
||||
tui_print("NIP-11: (empty response)");
|
||||
return;
|
||||
}
|
||||
|
||||
doc = cJSON_Parse(nip11_json);
|
||||
if (!doc || !cJSON_IsObject(doc)) {
|
||||
tui_print("NIP-11 (raw): %s", nip11_json);
|
||||
cJSON_Delete(doc);
|
||||
return;
|
||||
}
|
||||
|
||||
name = cJSON_GetObjectItemCaseSensitive(doc, "name");
|
||||
description = cJSON_GetObjectItemCaseSensitive(doc, "description");
|
||||
software = cJSON_GetObjectItemCaseSensitive(doc, "software");
|
||||
version = cJSON_GetObjectItemCaseSensitive(doc, "version");
|
||||
pubkey = cJSON_GetObjectItemCaseSensitive(doc, "pubkey");
|
||||
contact = cJSON_GetObjectItemCaseSensitive(doc, "contact");
|
||||
supported_nips = cJSON_GetObjectItemCaseSensitive(doc, "supported_nips");
|
||||
limitation = cJSON_GetObjectItemCaseSensitive(doc, "limitation");
|
||||
|
||||
tui_print("NIP-11 relay profile:");
|
||||
tui_print(" Name: %s", (cJSON_IsString(name) && name->valuestring) ? name->valuestring : "(n/a)");
|
||||
tui_print(" Description: %s", (cJSON_IsString(description) && description->valuestring) ? description->valuestring : "(n/a)");
|
||||
tui_print(" Software: %s", (cJSON_IsString(software) && software->valuestring) ? software->valuestring : "(n/a)");
|
||||
tui_print(" Version: %s", (cJSON_IsString(version) && version->valuestring) ? version->valuestring : "(n/a)");
|
||||
tui_print(" Contact: %s", (cJSON_IsString(contact) && contact->valuestring) ? contact->valuestring : "(n/a)");
|
||||
tui_print(" Pubkey: %s", (cJSON_IsString(pubkey) && pubkey->valuestring) ? pubkey->valuestring : "(n/a)");
|
||||
|
||||
if (cJSON_IsArray(supported_nips)) {
|
||||
char *nips_json = cJSON_PrintUnformatted(supported_nips);
|
||||
if (nips_json) {
|
||||
tui_print(" Supported NIPs: %s", nips_json);
|
||||
free(nips_json);
|
||||
}
|
||||
}
|
||||
|
||||
if (cJSON_IsObject(limitation)) {
|
||||
cJSON *max_subscriptions = cJSON_GetObjectItemCaseSensitive(limitation, "max_subscriptions");
|
||||
cJSON *max_message_length = cJSON_GetObjectItemCaseSensitive(limitation, "max_message_length");
|
||||
cJSON *max_limit = cJSON_GetObjectItemCaseSensitive(limitation, "max_limit");
|
||||
|
||||
tui_print(" Limits:");
|
||||
if (cJSON_IsNumber(max_subscriptions)) {
|
||||
tui_print(" max_subscriptions: %d", max_subscriptions->valueint);
|
||||
}
|
||||
if (cJSON_IsNumber(max_message_length)) {
|
||||
tui_print(" max_message_length: %d", max_message_length->valueint);
|
||||
}
|
||||
if (cJSON_IsNumber(max_limit)) {
|
||||
tui_print(" max_limit: %d", max_limit->valueint);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(doc);
|
||||
}
|
||||
|
||||
void menu_relays(void) {
|
||||
cJSON *working = NULL;
|
||||
cJSON *tags = NULL;
|
||||
@@ -97,7 +165,7 @@ void menu_relays(void) {
|
||||
|
||||
tui_print("Gathering relay data ...");
|
||||
if (nt_fetch_nip11(relay_url, &nip11) == 0 && nip11) {
|
||||
tui_print("NIP-11: %s", nip11);
|
||||
relays_print_nip11_summary(nip11);
|
||||
free(nip11);
|
||||
} else {
|
||||
tui_print("NIP-11 fetch failed.");
|
||||
@@ -189,7 +257,8 @@ void menu_relays(void) {
|
||||
}
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
break;
|
||||
} else if (input[0] == 'x' || input[0] == 'X') {
|
||||
} else if (input[0] == 'x' || input[0] == 'X' ||
|
||||
input[0] == 'q' || input[0] == 'Q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
435
src/menu_todo.c
Normal file
435
src/menu_todo.c
Normal file
@@ -0,0 +1,435 @@
|
||||
#include "tui.h"
|
||||
#include "state.h"
|
||||
#include "net.h"
|
||||
#include "publish.h"
|
||||
#include "signer.h"
|
||||
|
||||
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/nip004.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/utils.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
char *text;
|
||||
int done;
|
||||
} todo_item_t;
|
||||
|
||||
static char *todo_strdup(const char *s) {
|
||||
size_t n;
|
||||
char *out;
|
||||
|
||||
if (!s) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
n = strlen(s);
|
||||
out = (char *)malloc(n + 1U);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
memcpy(out, s, n + 1U);
|
||||
return out;
|
||||
}
|
||||
|
||||
static void todo_free_items(todo_item_t *items, int count) {
|
||||
int i;
|
||||
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
free(items[i].text);
|
||||
}
|
||||
free(items);
|
||||
}
|
||||
|
||||
static int todo_push_item(todo_item_t **items, int *count, const char *text, int done) {
|
||||
todo_item_t *next;
|
||||
|
||||
if (!items || !count || !text) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
next = (todo_item_t *)realloc(*items, (size_t)(*count + 1) * sizeof(todo_item_t));
|
||||
if (!next) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*items = next;
|
||||
(*items)[*count].text = todo_strdup(text);
|
||||
(*items)[*count].done = done ? 1 : 0;
|
||||
if (!(*items)[*count].text) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
(*count)++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int todo_parse_index_1based(const char *input, int count) {
|
||||
int idx;
|
||||
|
||||
if (!input || count <= 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
idx = atoi(input);
|
||||
if (idx <= 0 || idx > count) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return idx - 1;
|
||||
}
|
||||
|
||||
static int todo_encrypt_for_self(const char *plaintext, char **cipher_out) {
|
||||
return signer_nip44_self_encrypt(plaintext, cipher_out);
|
||||
}
|
||||
|
||||
static char *todo_decrypt_from_pub(const char *sender_pub_hex, const char *ciphertext) {
|
||||
char *out = NULL;
|
||||
|
||||
if (signer_nip44_decrypt(sender_pub_hex, ciphertext, &out) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static cJSON *todo_make_d_tag(void) {
|
||||
cJSON *tags = cJSON_CreateArray();
|
||||
cJSON *d_tag;
|
||||
|
||||
if (!tags) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
d_tag = cJSON_CreateArray();
|
||||
if (!d_tag) {
|
||||
cJSON_Delete(tags);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(d_tag, cJSON_CreateString("d"));
|
||||
cJSON_AddItemToArray(d_tag, cJSON_CreateString("todo"));
|
||||
cJSON_AddItemToArray(tags, d_tag);
|
||||
return tags;
|
||||
}
|
||||
|
||||
static char *todo_items_to_json(const todo_item_t *items, int count) {
|
||||
cJSON *arr = cJSON_CreateArray();
|
||||
char *out;
|
||||
int i;
|
||||
|
||||
if (!arr) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
cJSON *obj = cJSON_CreateObject();
|
||||
if (!obj) {
|
||||
cJSON_Delete(arr);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddStringToObject(obj, "text", items[i].text ? items[i].text : "");
|
||||
cJSON_AddBoolToObject(obj, "done", items[i].done ? 1 : 0);
|
||||
cJSON_AddItemToArray(arr, obj);
|
||||
}
|
||||
|
||||
out = cJSON_PrintUnformatted(arr);
|
||||
cJSON_Delete(arr);
|
||||
return out;
|
||||
}
|
||||
|
||||
static int todo_items_from_json(const char *json, todo_item_t **items_out, int *count_out) {
|
||||
cJSON *arr;
|
||||
int i;
|
||||
int n;
|
||||
todo_item_t *items = NULL;
|
||||
int count = 0;
|
||||
|
||||
if (!items_out || !count_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*items_out = NULL;
|
||||
*count_out = 0;
|
||||
|
||||
if (!json || json[0] == '\0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
arr = cJSON_Parse(json);
|
||||
if (!arr || !cJSON_IsArray(arr)) {
|
||||
cJSON_Delete(arr);
|
||||
return -1;
|
||||
}
|
||||
|
||||
n = cJSON_GetArraySize(arr);
|
||||
for (i = 0; i < n; i++) {
|
||||
cJSON *obj = cJSON_GetArrayItem(arr, i);
|
||||
cJSON *text;
|
||||
cJSON *done;
|
||||
|
||||
if (!cJSON_IsObject(obj)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
text = cJSON_GetObjectItemCaseSensitive(obj, "text");
|
||||
done = cJSON_GetObjectItemCaseSensitive(obj, "done");
|
||||
if (!cJSON_IsString(text) || !text->valuestring) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (todo_push_item(&items, &count, text->valuestring, cJSON_IsBool(done) && cJSON_IsTrue(done)) != 0) {
|
||||
todo_free_items(items, count);
|
||||
cJSON_Delete(arr);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(arr);
|
||||
*items_out = items;
|
||||
*count_out = count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int todo_load_remote(todo_item_t **items_out, int *count_out) {
|
||||
char filter[256];
|
||||
const char **read_relays;
|
||||
int relay_count = 0;
|
||||
char *event_json = NULL;
|
||||
cJSON *event;
|
||||
cJSON *content_item;
|
||||
cJSON *pubkey_item;
|
||||
char *plain = NULL;
|
||||
int rc;
|
||||
|
||||
if (!items_out || !count_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*items_out = NULL;
|
||||
*count_out = 0;
|
||||
|
||||
read_relays = state_get_read_relays(&relay_count);
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"{\"authors\":[\"%s\"],\"kinds\":[30078],\"#d\":[\"todo\"],\"limit\":1}",
|
||||
g_state.npub_hex);
|
||||
|
||||
if (nt_get_first(filter, read_relays, relay_count, 7000, &event_json) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!event_json) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
event = cJSON_Parse(event_json);
|
||||
free(event_json);
|
||||
if (!event) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
content_item = cJSON_GetObjectItemCaseSensitive(event, "content");
|
||||
pubkey_item = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
|
||||
if (!cJSON_IsString(content_item) || !content_item->valuestring ||
|
||||
!cJSON_IsString(pubkey_item) || !pubkey_item->valuestring) {
|
||||
cJSON_Delete(event);
|
||||
return -1;
|
||||
}
|
||||
|
||||
plain = todo_decrypt_from_pub(pubkey_item->valuestring, content_item->valuestring);
|
||||
cJSON_Delete(event);
|
||||
if (!plain) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
rc = todo_items_from_json(plain, items_out, count_out);
|
||||
free(plain);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static void todo_show_list(const todo_item_t *items, int count) {
|
||||
int i;
|
||||
|
||||
tui_print("TODO LIST\n");
|
||||
for (i = 0; i < count; i++) {
|
||||
tui_print("%2d. [%c] %s", i + 1, items[i].done ? 'x' : ' ', items[i].text ? items[i].text : "");
|
||||
}
|
||||
if (count == 0) {
|
||||
tui_print("(empty)");
|
||||
}
|
||||
tui_print("");
|
||||
}
|
||||
|
||||
static void todo_add_item(todo_item_t **items, int *count) {
|
||||
char input[1024];
|
||||
|
||||
tui_get_line("new item >", input, (int)sizeof(input));
|
||||
if (input[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (todo_push_item(items, count, input, 0) != 0) {
|
||||
tui_print("Failed to add item.");
|
||||
}
|
||||
}
|
||||
|
||||
static void todo_toggle_item(todo_item_t *items, int count) {
|
||||
char input[64];
|
||||
int idx;
|
||||
|
||||
if (!items || count <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
tui_get_line("toggle number >", input, (int)sizeof(input));
|
||||
idx = todo_parse_index_1based(input, count);
|
||||
if (idx < 0) {
|
||||
tui_print("Invalid number.");
|
||||
return;
|
||||
}
|
||||
|
||||
items[idx].done = !items[idx].done;
|
||||
}
|
||||
|
||||
static void todo_delete_item(todo_item_t *items, int *count) {
|
||||
char input[64];
|
||||
int idx;
|
||||
int i;
|
||||
|
||||
if (!items || !count || *count <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
tui_get_line("delete number >", input, (int)sizeof(input));
|
||||
idx = todo_parse_index_1based(input, *count);
|
||||
if (idx < 0) {
|
||||
tui_print("Invalid number.");
|
||||
return;
|
||||
}
|
||||
|
||||
free(items[idx].text);
|
||||
for (i = idx; i < (*count - 1); i++) {
|
||||
items[i] = items[i + 1];
|
||||
}
|
||||
(*count)--;
|
||||
}
|
||||
|
||||
static void todo_reorder_swap(todo_item_t *items, int count) {
|
||||
char input_a[64];
|
||||
char input_b[64];
|
||||
int a;
|
||||
int b;
|
||||
todo_item_t tmp;
|
||||
|
||||
if (!items || count < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
tui_get_line("first number >", input_a, (int)sizeof(input_a));
|
||||
tui_get_line("second number >", input_b, (int)sizeof(input_b));
|
||||
|
||||
a = todo_parse_index_1based(input_a, count);
|
||||
b = todo_parse_index_1based(input_b, count);
|
||||
if (a < 0 || b < 0 || a == b) {
|
||||
tui_print("Invalid numbers.");
|
||||
return;
|
||||
}
|
||||
|
||||
tmp = items[a];
|
||||
items[a] = items[b];
|
||||
items[b] = tmp;
|
||||
}
|
||||
|
||||
static int todo_publish(const todo_item_t *items, int count) {
|
||||
char *json = NULL;
|
||||
char *cipher = NULL;
|
||||
cJSON *tags = NULL;
|
||||
int posted;
|
||||
|
||||
json = todo_items_to_json(items, count);
|
||||
if (!json) {
|
||||
tui_print("Failed to serialize todo list.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (todo_encrypt_for_self(json, &cipher) != 0) {
|
||||
tui_print("Failed to encrypt todo list.");
|
||||
free(json);
|
||||
return -1;
|
||||
}
|
||||
|
||||
tags = todo_make_d_tag();
|
||||
if (!tags) {
|
||||
tui_print("Failed to build todo tags.");
|
||||
free(json);
|
||||
free(cipher);
|
||||
return -1;
|
||||
}
|
||||
|
||||
posted = publish_signed_event_with_report("Todo", 30078, cipher, tags, 8000);
|
||||
cJSON_Delete(tags);
|
||||
free(json);
|
||||
free(cipher);
|
||||
|
||||
if (posted < 0) {
|
||||
tui_print("Todo publish failed.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
tui_print("Todo saved.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void menu_todo(void) {
|
||||
todo_item_t *items = NULL;
|
||||
int count = 0;
|
||||
char input[32];
|
||||
|
||||
if (todo_load_remote(&items, &count) != 0) {
|
||||
tui_print("Failed to load remote todo list; starting empty.");
|
||||
items = NULL;
|
||||
count = 0;
|
||||
}
|
||||
|
||||
while (1) {
|
||||
tui_clear_screen();
|
||||
todo_show_list(items, count);
|
||||
tui_print("^_A^:dd item");
|
||||
tui_print("^_C^:omplete/toggle item");
|
||||
tui_print("^_D^:elete item");
|
||||
tui_print("^_R^:eorder (swap two)");
|
||||
tui_print("^_P^:ost/save changes");
|
||||
tui_print("E^_x^:it without saving");
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
|
||||
if (input[0] == 'x' || input[0] == 'X' ||
|
||||
input[0] == 'q' || input[0] == 'Q') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (input[0] == 'a' || input[0] == 'A') {
|
||||
todo_add_item(&items, &count);
|
||||
} else if (input[0] == 'c' || input[0] == 'C') {
|
||||
todo_toggle_item(items, count);
|
||||
} else if (input[0] == 'd' || input[0] == 'D') {
|
||||
todo_delete_item(items, &count);
|
||||
} else if (input[0] == 'r' || input[0] == 'R') {
|
||||
todo_reorder_swap(items, count);
|
||||
} else if (input[0] == 'p' || input[0] == 'P') {
|
||||
char hold[8];
|
||||
(void)todo_publish(items, count);
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
}
|
||||
}
|
||||
|
||||
todo_free_items(items, count);
|
||||
}
|
||||
135
src/menu_write.c
Normal file
135
src/menu_write.c
Normal file
@@ -0,0 +1,135 @@
|
||||
#include "tui.h"
|
||||
#include "publish.h"
|
||||
#include "editor.h"
|
||||
|
||||
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
static cJSON *write_make_blog_tags(const char *d_tag_value, const char *title_opt) {
|
||||
cJSON *tags = cJSON_CreateArray();
|
||||
cJSON *d_tag;
|
||||
|
||||
if (!tags) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
d_tag = cJSON_CreateArray();
|
||||
if (!d_tag) {
|
||||
cJSON_Delete(tags);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddItemToArray(d_tag, cJSON_CreateString("d"));
|
||||
cJSON_AddItemToArray(d_tag, cJSON_CreateString(d_tag_value ? d_tag_value : ""));
|
||||
cJSON_AddItemToArray(tags, d_tag);
|
||||
|
||||
if (title_opt && title_opt[0] != '\0') {
|
||||
cJSON *title_tag = cJSON_CreateArray();
|
||||
if (title_tag) {
|
||||
cJSON_AddItemToArray(title_tag, cJSON_CreateString("title"));
|
||||
cJSON_AddItemToArray(title_tag, cJSON_CreateString(title_opt));
|
||||
cJSON_AddItemToArray(tags, title_tag);
|
||||
}
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
void menu_write(void) {
|
||||
char *content;
|
||||
char input[64];
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("WRITE\n");
|
||||
tui_print("Launching external editor...");
|
||||
|
||||
content = editor_launch(NULL);
|
||||
if (!content) {
|
||||
tui_print("No content created.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
return;
|
||||
}
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("WRITE RESULT\n");
|
||||
tui_print("Content length: %d bytes", (int)strlen(content));
|
||||
tui_print("\n");
|
||||
tui_print("^_T^:weet it");
|
||||
tui_print("^_B^:log post (kind 30023)");
|
||||
tui_print("^_D^:iary entry (kind 30024)");
|
||||
tui_print("^_C^:ancel/discard");
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
|
||||
if (input[0] == 't' || input[0] == 'T') {
|
||||
int posted = publish_signed_event_with_report("Tweet", 1, content, NULL, 8000);
|
||||
if (posted < 0) {
|
||||
tui_print("Tweet publish failed.");
|
||||
}
|
||||
} else if (input[0] == 'b' || input[0] == 'B') {
|
||||
char title[256];
|
||||
char slug[256];
|
||||
const char *d_val;
|
||||
cJSON *tags;
|
||||
int posted;
|
||||
|
||||
tui_get_line("title >", title, (int)sizeof(title));
|
||||
tui_get_line("slug (d tag) [empty uses title] >", slug, (int)sizeof(slug));
|
||||
|
||||
d_val = (slug[0] != '\0') ? slug : title;
|
||||
tags = write_make_blog_tags(d_val, title);
|
||||
if (!tags) {
|
||||
tui_print("Failed to build blog tags.");
|
||||
} else {
|
||||
posted = publish_signed_event_with_report("Blog post", 30023, content, tags, 8000);
|
||||
cJSON_Delete(tags);
|
||||
if (posted < 0) {
|
||||
tui_print("Blog publish failed.");
|
||||
}
|
||||
}
|
||||
} else if (input[0] == 'd' || input[0] == 'D') {
|
||||
char date_d[16];
|
||||
time_t now = time(NULL);
|
||||
struct tm tmv;
|
||||
cJSON *tags;
|
||||
int posted;
|
||||
|
||||
memset(&tmv, 0, sizeof(tmv));
|
||||
if (!localtime_r(&now, &tmv)) {
|
||||
tui_print("Failed to get local date.");
|
||||
free(content);
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
return;
|
||||
}
|
||||
|
||||
if (strftime(date_d, sizeof(date_d), "%Y%m%d", &tmv) == 0) {
|
||||
tui_print("Failed to format diary date.");
|
||||
free(content);
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
return;
|
||||
}
|
||||
|
||||
tags = write_make_blog_tags(date_d, NULL);
|
||||
if (!tags) {
|
||||
tui_print("Failed to build diary tags.");
|
||||
} else {
|
||||
posted = publish_signed_event_with_report("Diary", 30024, content, tags, 8000);
|
||||
cJSON_Delete(tags);
|
||||
if (posted < 0) {
|
||||
tui_print("Diary publish failed.");
|
||||
}
|
||||
}
|
||||
} else if (input[0] == 'c' || input[0] == 'C' ||
|
||||
input[0] == 'x' || input[0] == 'X' ||
|
||||
input[0] == 'q' || input[0] == 'Q') {
|
||||
tui_print("Discarded.");
|
||||
} else {
|
||||
tui_print("Discarded.");
|
||||
}
|
||||
|
||||
free(content);
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
}
|
||||
516
src/net.c
516
src/net.c
@@ -1,14 +1,22 @@
|
||||
#include "net.h"
|
||||
|
||||
#include "db.h"
|
||||
#include "state.h"
|
||||
#include "signer.h"
|
||||
|
||||
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/nip042.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/utils.h"
|
||||
#include "../resources/nostr_core_lib/nostr_websocket/nostr_websocket_tls.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define NT_SUB_ID "sub1"
|
||||
#define NT_WS_BUFFER_SIZE 65536
|
||||
@@ -152,15 +160,139 @@ static char *nt_ws_to_http_url(const char *relay_url) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Wait for an OK message for a publish and return whether relay accepted. */
|
||||
typedef struct {
|
||||
char *data;
|
||||
size_t len;
|
||||
} nt_http_buffer_t;
|
||||
|
||||
static size_t nt_http_write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) {
|
||||
nt_http_buffer_t *buf = (nt_http_buffer_t *)userdata;
|
||||
size_t total = size * nmemb;
|
||||
char *next;
|
||||
|
||||
if (!buf || !ptr || total == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
next = (char *)realloc(buf->data, buf->len + total + 1U);
|
||||
if (!next) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
buf->data = next;
|
||||
memcpy(buf->data + buf->len, ptr, total);
|
||||
buf->len += total;
|
||||
buf->data[buf->len] = '\0';
|
||||
return total;
|
||||
}
|
||||
|
||||
static void nt_configure_curl_tls(CURL *curl) {
|
||||
const char *cafile_env;
|
||||
const char *capath_env;
|
||||
static const char *cafile_candidates[] = {
|
||||
"/etc/ssl/certs/ca-certificates.crt", /* Debian/Ubuntu */
|
||||
"/etc/ssl/cert.pem", /* Alpine */
|
||||
"/etc/pki/tls/certs/ca-bundle.crt", /* RHEL/CentOS */
|
||||
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" /* Fedora */
|
||||
};
|
||||
int i;
|
||||
|
||||
if (!curl) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Respect explicit user overrides first. */
|
||||
cafile_env = getenv("SSL_CERT_FILE");
|
||||
if (!cafile_env || cafile_env[0] == '\0') {
|
||||
cafile_env = getenv("CURL_CA_BUNDLE");
|
||||
}
|
||||
if (cafile_env && cafile_env[0] != '\0') {
|
||||
curl_easy_setopt(curl, CURLOPT_CAINFO, cafile_env);
|
||||
return;
|
||||
}
|
||||
|
||||
capath_env = getenv("SSL_CERT_DIR");
|
||||
if (capath_env && capath_env[0] != '\0') {
|
||||
curl_easy_setopt(curl, CURLOPT_CAPATH, capath_env);
|
||||
}
|
||||
|
||||
for (i = 0; i < (int)(sizeof(cafile_candidates) / sizeof(cafile_candidates[0])); i++) {
|
||||
if (access(cafile_candidates[i], R_OK) == 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_CAINFO, cafile_candidates[i]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* Last resort: common cert directory if bundle file isn't found. */
|
||||
if (access("/etc/ssl/certs", R_OK) == 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_CAPATH, "/etc/ssl/certs");
|
||||
}
|
||||
}
|
||||
|
||||
static int nt_send_nip42_auth(nostr_ws_client_t *client,
|
||||
const char *relay_url,
|
||||
const char *challenge,
|
||||
char **auth_event_id_out) {
|
||||
unsigned char private_key[32];
|
||||
cJSON *auth_event = NULL;
|
||||
char *auth_message = NULL;
|
||||
char *auth_event_id = NULL;
|
||||
int send_rc;
|
||||
|
||||
if (!client || !relay_url || !challenge || !auth_event_id_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*auth_event_id_out = NULL;
|
||||
|
||||
if (signer_get_local_private_key(private_key) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
auth_event = nostr_nip42_create_auth_event(challenge, relay_url, private_key, 0);
|
||||
if (!auth_event) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
auth_event_id = nt_extract_event_id(cJSON_PrintUnformatted(auth_event));
|
||||
if (!auth_event_id) {
|
||||
cJSON *id = cJSON_GetObjectItemCaseSensitive(auth_event, "id");
|
||||
if (cJSON_IsString(id) && id->valuestring) {
|
||||
auth_event_id = nt_strdup_local(id->valuestring);
|
||||
}
|
||||
}
|
||||
|
||||
auth_message = nostr_nip42_create_auth_message(auth_event);
|
||||
cJSON_Delete(auth_event);
|
||||
if (!auth_message) {
|
||||
free(auth_event_id);
|
||||
return -1;
|
||||
}
|
||||
|
||||
send_rc = nostr_ws_send_text(client, auth_message);
|
||||
free(auth_message);
|
||||
if (send_rc < 0) {
|
||||
free(auth_event_id);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*auth_event_id_out = auth_event_id;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Wait for an OK message for publish. Handles one NIP-42 AUTH round-trip if challenged. */
|
||||
static int nt_wait_publish_ok(nostr_ws_client_t *client,
|
||||
cJSON *event,
|
||||
const char *relay_url,
|
||||
int timeout_ms,
|
||||
int *posted_out,
|
||||
char **response_out) {
|
||||
char buffer[NT_WS_BUFFER_SIZE];
|
||||
long long deadline_ms;
|
||||
int auth_attempted = 0;
|
||||
char *auth_event_id = NULL;
|
||||
|
||||
if (!client || !posted_out || !response_out) {
|
||||
if (!client || !event || !relay_url || !posted_out || !response_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -173,10 +305,10 @@ static int nt_wait_publish_ok(nostr_ws_client_t *client,
|
||||
int recv_len;
|
||||
char *message_type = NULL;
|
||||
cJSON *parsed = NULL;
|
||||
cJSON *ok_item;
|
||||
|
||||
recv_len = nostr_ws_receive(client, buffer, sizeof(buffer), nt_ms_remaining(deadline_ms));
|
||||
if (recv_len <= 0) {
|
||||
free(auth_event_id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -184,10 +316,71 @@ static int nt_wait_publish_ok(nostr_ws_client_t *client,
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message_type && strcmp(message_type, "AUTH") == 0) {
|
||||
char challenge[NOSTR_NIP42_MAX_CHALLENGE_LENGTH];
|
||||
|
||||
if (auth_attempted) {
|
||||
free(message_type);
|
||||
cJSON_Delete(parsed);
|
||||
continue;
|
||||
}
|
||||
|
||||
memset(challenge, 0, sizeof(challenge));
|
||||
if (nostr_nip42_parse_auth_challenge(buffer, challenge, sizeof(challenge)) != NOSTR_SUCCESS ||
|
||||
challenge[0] == '\0') {
|
||||
free(message_type);
|
||||
cJSON_Delete(parsed);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nt_send_nip42_auth(client, relay_url, challenge, &auth_event_id) != 0) {
|
||||
*response_out = nt_strdup_local("auth_send_failed");
|
||||
free(message_type);
|
||||
cJSON_Delete(parsed);
|
||||
return 0;
|
||||
}
|
||||
|
||||
auth_attempted = 1;
|
||||
free(message_type);
|
||||
cJSON_Delete(parsed);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message_type && strcmp(message_type, "OK") == 0) {
|
||||
ok_item = cJSON_GetArrayItem(parsed, 2);
|
||||
*posted_out = cJSON_IsBool(ok_item) && cJSON_IsTrue(ok_item);
|
||||
cJSON *id_item = cJSON_GetArrayItem(parsed, 1);
|
||||
cJSON *ok_item = cJSON_GetArrayItem(parsed, 2);
|
||||
const char *ok_id = (cJSON_IsString(id_item) && id_item->valuestring) ? id_item->valuestring : NULL;
|
||||
int ok = cJSON_IsBool(ok_item) && cJSON_IsTrue(ok_item);
|
||||
|
||||
if (auth_event_id && ok_id && strcmp(auth_event_id, ok_id) == 0) {
|
||||
if (!ok) {
|
||||
*posted_out = 0;
|
||||
*response_out = nt_strdup_local(buffer);
|
||||
free(auth_event_id);
|
||||
free(message_type);
|
||||
cJSON_Delete(parsed);
|
||||
return 0;
|
||||
}
|
||||
|
||||
free(auth_event_id);
|
||||
auth_event_id = NULL;
|
||||
|
||||
if (nostr_relay_send_event(client, event) != NOSTR_WS_SUCCESS) {
|
||||
*posted_out = 0;
|
||||
*response_out = nt_strdup_local("send_failed_after_auth");
|
||||
free(message_type);
|
||||
cJSON_Delete(parsed);
|
||||
return 0;
|
||||
}
|
||||
|
||||
free(message_type);
|
||||
cJSON_Delete(parsed);
|
||||
continue;
|
||||
}
|
||||
|
||||
*posted_out = ok;
|
||||
*response_out = nt_strdup_local(buffer);
|
||||
free(auth_event_id);
|
||||
free(message_type);
|
||||
cJSON_Delete(parsed);
|
||||
return 0;
|
||||
@@ -197,6 +390,7 @@ static int nt_wait_publish_ok(nostr_ws_client_t *client,
|
||||
cJSON_Delete(parsed);
|
||||
}
|
||||
|
||||
free(auth_event_id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -278,7 +472,7 @@ int nt_publish_detailed(const char *event_json,
|
||||
continue;
|
||||
}
|
||||
|
||||
(void)nt_wait_publish_ok(client, timeout_ms, &posted, &response);
|
||||
(void)nt_wait_publish_ok(client, event, relay, timeout_ms, &posted, &response);
|
||||
if (!response) {
|
||||
response = nt_strdup_local("timeout_or_no_ok");
|
||||
}
|
||||
@@ -338,6 +532,8 @@ void nt_publish_results_free(nt_publish_result_t *results, int result_count) {
|
||||
static int nt_query_one_relay(const char *relay_url,
|
||||
cJSON *filter,
|
||||
int timeout_ms,
|
||||
int verbose,
|
||||
const char *phase_label,
|
||||
char ***events,
|
||||
int *event_count,
|
||||
int *event_cap,
|
||||
@@ -364,6 +560,25 @@ static int nt_query_one_relay(const char *relay_url,
|
||||
|
||||
(void)nostr_ws_set_timeout(client, timeout_ms > 0 ? timeout_ms : 5000);
|
||||
|
||||
if (verbose) {
|
||||
cJSON *req_msg = cJSON_CreateArray();
|
||||
if (req_msg) {
|
||||
cJSON_AddItemToArray(req_msg, cJSON_CreateString("REQ"));
|
||||
cJSON_AddItemToArray(req_msg, cJSON_CreateString(NT_SUB_ID));
|
||||
cJSON_AddItemToArray(req_msg, cJSON_Duplicate(filter, 1));
|
||||
{
|
||||
char *req_json = cJSON_PrintUnformatted(req_msg);
|
||||
fprintf(stderr,
|
||||
"%s: request to %s: %s\n",
|
||||
phase_label ? phase_label : "query",
|
||||
relay_url ? relay_url : "(unknown relay)",
|
||||
req_json ? req_json : "(serialize_failed)");
|
||||
free(req_json);
|
||||
}
|
||||
cJSON_Delete(req_msg);
|
||||
}
|
||||
}
|
||||
|
||||
if (nostr_relay_send_req(client, NT_SUB_ID, filter) != NOSTR_WS_SUCCESS) {
|
||||
if (reason_out) {
|
||||
*reason_out = "send_req_failed";
|
||||
@@ -384,6 +599,14 @@ static int nt_query_one_relay(const char *relay_url,
|
||||
break;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
fprintf(stderr,
|
||||
"%s: response from %s: %s\n",
|
||||
phase_label ? phase_label : "query",
|
||||
relay_url ? relay_url : "(unknown relay)",
|
||||
buffer);
|
||||
}
|
||||
|
||||
if (nostr_parse_relay_message(buffer, &message_type, &parsed) != NOSTR_WS_SUCCESS) {
|
||||
continue;
|
||||
}
|
||||
@@ -393,6 +616,16 @@ static int nt_query_one_relay(const char *relay_url,
|
||||
cJSON *id_item = NULL;
|
||||
|
||||
if (cJSON_IsObject(event_obj)) {
|
||||
if (verbose) {
|
||||
char *received_event = cJSON_PrintUnformatted(event_obj);
|
||||
fprintf(stderr,
|
||||
"%s: event from %s: %s\n",
|
||||
phase_label ? phase_label : "query",
|
||||
relay_url ? relay_url : "(unknown relay)",
|
||||
received_event ? received_event : "(serialize_failed)");
|
||||
free(received_event);
|
||||
}
|
||||
|
||||
id_item = cJSON_GetObjectItemCaseSensitive(event_obj, "id");
|
||||
if (cJSON_IsString(id_item) && id_item->valuestring) {
|
||||
if (!nt_seen_id(*seen_ids, *seen_count, id_item->valuestring)) {
|
||||
@@ -491,6 +724,8 @@ int nt_query_sync_verbose(const char *filter_json,
|
||||
rc = nt_query_one_relay(relay,
|
||||
filter,
|
||||
timeout_ms,
|
||||
verbose,
|
||||
phase_label,
|
||||
&events,
|
||||
&events_n,
|
||||
&events_cap,
|
||||
@@ -629,14 +864,184 @@ int nt_get_first(const char *filter_json,
|
||||
event_out);
|
||||
}
|
||||
|
||||
static int nt_set_stdin_nonblocking(int *old_flags_out) {
|
||||
int flags;
|
||||
|
||||
if (!old_flags_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*old_flags_out = -1;
|
||||
flags = fcntl(STDIN_FILENO, F_GETFL, 0);
|
||||
if (flags < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*old_flags_out = flags;
|
||||
if ((flags & O_NONBLOCK) == 0) {
|
||||
if (fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK) != 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void nt_restore_stdin_flags(int old_flags) {
|
||||
if (old_flags >= 0) {
|
||||
(void)fcntl(STDIN_FILENO, F_SETFL, old_flags);
|
||||
}
|
||||
}
|
||||
|
||||
int nt_live_feed(const char *filter_json,
|
||||
const char **relay_urls,
|
||||
int relay_count,
|
||||
nt_event_callback_t on_event,
|
||||
void *userdata) {
|
||||
typedef struct {
|
||||
nostr_ws_client_t *client;
|
||||
const char *relay;
|
||||
} nt_live_relay_t;
|
||||
|
||||
cJSON *filter = NULL;
|
||||
nt_live_relay_t *relays = NULL;
|
||||
int connected = 0;
|
||||
int event_count = 0;
|
||||
int old_stdin_flags = -1;
|
||||
int i;
|
||||
int saw_stdin_key = 0;
|
||||
|
||||
if (!filter_json || !relay_urls || relay_count <= 0 || !on_event) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
filter = cJSON_Parse(filter_json);
|
||||
if (!filter) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
relays = (nt_live_relay_t *)calloc((size_t)relay_count, sizeof(nt_live_relay_t));
|
||||
if (!relays) {
|
||||
cJSON_Delete(filter);
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (i = 0; i < relay_count; i++) {
|
||||
nostr_ws_client_t *client;
|
||||
|
||||
relays[i].relay = relay_urls[i];
|
||||
if (!relay_urls[i] || relay_urls[i][0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
client = nostr_ws_connect(relay_urls[i]);
|
||||
if (!client) {
|
||||
continue;
|
||||
}
|
||||
|
||||
(void)nostr_ws_set_timeout(client, 1000);
|
||||
if (nostr_relay_send_req(client, NT_SUB_ID, filter) != NOSTR_WS_SUCCESS) {
|
||||
nostr_ws_close(client);
|
||||
continue;
|
||||
}
|
||||
|
||||
relays[i].client = client;
|
||||
connected++;
|
||||
}
|
||||
|
||||
if (connected == 0) {
|
||||
free(relays);
|
||||
cJSON_Delete(filter);
|
||||
return -1;
|
||||
}
|
||||
|
||||
(void)nt_set_stdin_nonblocking(&old_stdin_flags);
|
||||
|
||||
while (!saw_stdin_key) {
|
||||
int active = 0;
|
||||
|
||||
{
|
||||
unsigned char keybuf[8];
|
||||
ssize_t read_rc = read(STDIN_FILENO, keybuf, sizeof(keybuf));
|
||||
if (read_rc > 0) {
|
||||
saw_stdin_key = 1;
|
||||
break;
|
||||
}
|
||||
if (read_rc < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < relay_count; i++) {
|
||||
char buffer[NT_WS_BUFFER_SIZE];
|
||||
int recv_len;
|
||||
char *message_type = NULL;
|
||||
cJSON *parsed = NULL;
|
||||
|
||||
if (!relays[i].client) {
|
||||
continue;
|
||||
}
|
||||
active++;
|
||||
|
||||
recv_len = nostr_ws_receive(relays[i].client, buffer, sizeof(buffer), 100);
|
||||
if (recv_len <= 0) {
|
||||
continue;
|
||||
}
|
||||
buffer[sizeof(buffer) - 1U] = '\0';
|
||||
|
||||
if (nostr_parse_relay_message(buffer, &message_type, &parsed) != NOSTR_WS_SUCCESS) {
|
||||
free(message_type);
|
||||
cJSON_Delete(parsed);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message_type && strcmp(message_type, "EVENT") == 0) {
|
||||
cJSON *event_obj = cJSON_GetArrayItem(parsed, 2);
|
||||
if (cJSON_IsObject(event_obj)) {
|
||||
char *event_json = cJSON_PrintUnformatted(event_obj);
|
||||
if (event_json) {
|
||||
on_event(event_json, userdata);
|
||||
event_count++;
|
||||
free(event_json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(message_type);
|
||||
cJSON_Delete(parsed);
|
||||
}
|
||||
|
||||
if (active == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < relay_count; i++) {
|
||||
if (relays[i].client) {
|
||||
(void)nostr_relay_send_close(relays[i].client, NT_SUB_ID);
|
||||
nostr_ws_close(relays[i].client);
|
||||
relays[i].client = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
nt_restore_stdin_flags(old_stdin_flags);
|
||||
free(relays);
|
||||
cJSON_Delete(filter);
|
||||
return event_count;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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 *normalized_url;
|
||||
CURL *curl;
|
||||
CURLcode rc;
|
||||
long status = 0;
|
||||
nt_http_buffer_t body = {0};
|
||||
struct curl_slist *headers = NULL;
|
||||
char curl_err[CURL_ERROR_SIZE] = {0};
|
||||
int insecure_retry = 0;
|
||||
|
||||
if (!relay_url || !info_json_out) {
|
||||
fprintf(stderr, "nt_fetch_nip11: invalid arguments\n");
|
||||
@@ -651,7 +1056,96 @@ int nt_fetch_nip11(const char *relay_url, char **info_json_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) {
|
||||
fprintf(stderr, "nt_fetch_nip11: curl_global_init failed\n");
|
||||
free(normalized_url);
|
||||
return -1;
|
||||
}
|
||||
|
||||
curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
fprintf(stderr, "nt_fetch_nip11: curl_easy_init failed\n");
|
||||
free(normalized_url);
|
||||
curl_global_cleanup();
|
||||
return -1;
|
||||
}
|
||||
|
||||
headers = curl_slist_append(headers, "Accept: application/nostr+json");
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, normalized_url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "nostr-terminal/0.0.1");
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 4000L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 8000L);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, nt_http_write_cb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&body);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
nt_configure_curl_tls(curl);
|
||||
if (headers) {
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
}
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_err);
|
||||
|
||||
retry_request:
|
||||
rc = curl_easy_perform(curl);
|
||||
if (rc != CURLE_OK) {
|
||||
if (!insecure_retry &&
|
||||
(rc == CURLE_PEER_FAILED_VERIFICATION || rc == CURLE_SSL_CACERT_BADFILE)) {
|
||||
fprintf(stderr,
|
||||
"nt_fetch_nip11: TLS validation failed for %s (%s). Retrying insecurely.\n",
|
||||
normalized_url,
|
||||
(curl_err[0] != '\0') ? curl_err : curl_easy_strerror(rc));
|
||||
insecure_retry = 1;
|
||||
free(body.data);
|
||||
body.data = NULL;
|
||||
body.len = 0;
|
||||
curl_err[0] = '\0';
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
|
||||
goto retry_request;
|
||||
}
|
||||
|
||||
fprintf(stderr,
|
||||
"nt_fetch_nip11: request failed for %s: %s\n",
|
||||
normalized_url,
|
||||
(curl_err[0] != '\0') ? curl_err : curl_easy_strerror(rc));
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
curl_global_cleanup();
|
||||
free(normalized_url);
|
||||
free(body.data);
|
||||
return -1;
|
||||
}
|
||||
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
|
||||
if (status < 200 || status >= 300) {
|
||||
fprintf(stderr, "nt_fetch_nip11: HTTP %ld from %s\n", status, normalized_url);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
curl_global_cleanup();
|
||||
free(normalized_url);
|
||||
free(body.data);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!body.data || body.len == 0) {
|
||||
fprintf(stderr, "nt_fetch_nip11: empty response from %s\n", normalized_url);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
curl_global_cleanup();
|
||||
free(normalized_url);
|
||||
free(body.data);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*info_json_out = body.data;
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
curl_global_cleanup();
|
||||
free(normalized_url);
|
||||
fprintf(stderr, "nt_fetch_nip11: disabled (no HTTP client linked in static build)\n");
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
1089
src/nsigner_client.c
Normal file
1089
src/nsigner_client.c
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,7 @@
|
||||
#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 "signer.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -16,9 +14,6 @@ int publish_signed_event_with_report(const char *label,
|
||||
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;
|
||||
@@ -31,37 +26,16 @@ int publish_signed_event_with_report(const char *label,
|
||||
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) {
|
||||
if (signer_create_and_sign(kind, content ? content : "", tags, time(NULL), &event_json) != 0 ||
|
||||
!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("Event JSON:");
|
||||
tui_print("%s", event_json);
|
||||
tui_print("Posting to %d relay(s)...", relay_count);
|
||||
|
||||
accepted_count = nt_publish_detailed(event_json,
|
||||
|
||||
624
src/signer.c
Normal file
624
src/signer.c
Normal file
@@ -0,0 +1,624 @@
|
||||
#include "signer.h"
|
||||
|
||||
#include "state.h"
|
||||
#include "nsigner_client.h"
|
||||
|
||||
#include "../resources/nostr_core_lib/nostr_core/nip001.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/nip004.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/nip044.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/utils.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
nt_signer_t g_signer = {
|
||||
.kind = NT_SIGNER_LOCAL,
|
||||
.socket_name = {0},
|
||||
.endpoint_url = {0},
|
||||
.role = "main",
|
||||
};
|
||||
|
||||
static int signer_local_private_key(unsigned char out_priv[32]) {
|
||||
if (!out_priv) {
|
||||
return -1;
|
||||
}
|
||||
if (nostr_hex_to_bytes(g_state.nsec_hex, out_priv, 32) != 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void signer_init(void) {
|
||||
g_signer.kind = NT_SIGNER_LOCAL;
|
||||
g_signer.socket_name[0] = '\0';
|
||||
g_signer.qrexec_target[0] = '\0';
|
||||
g_signer.qrexec_service[0] = '\0';
|
||||
g_signer.endpoint_url[0] = '\0';
|
||||
snprintf(g_signer.role, sizeof(g_signer.role), "%s", "main");
|
||||
}
|
||||
|
||||
int signer_init_local(void) {
|
||||
signer_init();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int signer_init_nsigner(const char *socket_name) {
|
||||
if (!socket_name || socket_name[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
g_signer.kind = NT_SIGNER_NSIGNER;
|
||||
snprintf(g_signer.socket_name, sizeof(g_signer.socket_name), "%s", socket_name);
|
||||
g_signer.qrexec_target[0] = '\0';
|
||||
g_signer.qrexec_service[0] = '\0';
|
||||
g_signer.endpoint_url[0] = '\0';
|
||||
snprintf(g_signer.role, sizeof(g_signer.role), "%s", "main");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int signer_init_nsigner_qrexec(const char *target_qube, const char *service_name) {
|
||||
if (!target_qube || target_qube[0] == '\0' || !service_name || service_name[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
g_signer.kind = NT_SIGNER_NSIGNER_QREXEC;
|
||||
g_signer.socket_name[0] = '\0';
|
||||
snprintf(g_signer.qrexec_target, sizeof(g_signer.qrexec_target), "%s", target_qube);
|
||||
snprintf(g_signer.qrexec_service, sizeof(g_signer.qrexec_service), "%s", service_name);
|
||||
g_signer.endpoint_url[0] = '\0';
|
||||
snprintf(g_signer.role, sizeof(g_signer.role), "%s", "main");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int signer_init_nsigner_url(const char *endpoint_url) {
|
||||
if (!endpoint_url || endpoint_url[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
g_signer.kind = NT_SIGNER_NSIGNER_URL;
|
||||
g_signer.socket_name[0] = '\0';
|
||||
g_signer.qrexec_target[0] = '\0';
|
||||
g_signer.qrexec_service[0] = '\0';
|
||||
snprintf(g_signer.endpoint_url, sizeof(g_signer.endpoint_url), "%s", endpoint_url);
|
||||
snprintf(g_signer.role, sizeof(g_signer.role), "%s", "main");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int signer_get_local_private_key(unsigned char out_priv[32]) {
|
||||
if (!out_priv) {
|
||||
return -1;
|
||||
}
|
||||
if (g_signer.kind != NT_SIGNER_LOCAL) {
|
||||
return -1;
|
||||
}
|
||||
return signer_local_private_key(out_priv);
|
||||
}
|
||||
|
||||
int signer_create_and_sign(int kind,
|
||||
const char *content,
|
||||
cJSON *tags,
|
||||
time_t timestamp,
|
||||
char **signed_event_json_out) {
|
||||
unsigned char priv[32];
|
||||
cJSON *tags_copy;
|
||||
cJSON *event;
|
||||
|
||||
if (!signed_event_json_out) {
|
||||
return -1;
|
||||
}
|
||||
*signed_event_json_out = NULL;
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER) {
|
||||
cJSON *unsigned_event = NULL;
|
||||
cJSON *tags_copy = NULL;
|
||||
char *unsigned_json = NULL;
|
||||
int rc = -1;
|
||||
|
||||
unsigned_event = cJSON_CreateObject();
|
||||
if (!unsigned_event) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!cJSON_AddNumberToObject(unsigned_event, "kind", kind) ||
|
||||
!cJSON_AddStringToObject(unsigned_event, "content", content ? content : "")) {
|
||||
cJSON_Delete(unsigned_event);
|
||||
return -1;
|
||||
}
|
||||
|
||||
tags_copy = tags ? cJSON_Duplicate(tags, 1) : cJSON_CreateArray();
|
||||
if (!tags_copy) {
|
||||
cJSON_Delete(unsigned_event);
|
||||
return -1;
|
||||
}
|
||||
if (!cJSON_AddItemToObject(unsigned_event, "tags", tags_copy)) {
|
||||
cJSON_Delete(tags_copy);
|
||||
cJSON_Delete(unsigned_event);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!cJSON_AddNumberToObject(unsigned_event, "created_at", (double)timestamp) ||
|
||||
!cJSON_AddStringToObject(unsigned_event, "pubkey", g_state.npub_hex)) {
|
||||
cJSON_Delete(unsigned_event);
|
||||
return -1;
|
||||
}
|
||||
|
||||
unsigned_json = cJSON_PrintUnformatted(unsigned_event);
|
||||
if (!unsigned_json) {
|
||||
cJSON_Delete(unsigned_event);
|
||||
return -1;
|
||||
}
|
||||
|
||||
rc = nsigner_sign_event(g_signer.socket_name,
|
||||
g_signer.role,
|
||||
unsigned_json,
|
||||
signed_event_json_out);
|
||||
|
||||
free(unsigned_json);
|
||||
cJSON_Delete(unsigned_event);
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER_QREXEC) {
|
||||
cJSON *unsigned_event = NULL;
|
||||
cJSON *tags_copy = NULL;
|
||||
char *unsigned_json = NULL;
|
||||
int rc = -1;
|
||||
|
||||
unsigned_event = cJSON_CreateObject();
|
||||
if (!unsigned_event) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!cJSON_AddNumberToObject(unsigned_event, "kind", kind) ||
|
||||
!cJSON_AddStringToObject(unsigned_event, "content", content ? content : "")) {
|
||||
cJSON_Delete(unsigned_event);
|
||||
return -1;
|
||||
}
|
||||
|
||||
tags_copy = tags ? cJSON_Duplicate(tags, 1) : cJSON_CreateArray();
|
||||
if (!tags_copy) {
|
||||
cJSON_Delete(unsigned_event);
|
||||
return -1;
|
||||
}
|
||||
if (!cJSON_AddItemToObject(unsigned_event, "tags", tags_copy)) {
|
||||
cJSON_Delete(tags_copy);
|
||||
cJSON_Delete(unsigned_event);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!cJSON_AddNumberToObject(unsigned_event, "created_at", (double)timestamp) ||
|
||||
!cJSON_AddStringToObject(unsigned_event, "pubkey", g_state.npub_hex)) {
|
||||
cJSON_Delete(unsigned_event);
|
||||
return -1;
|
||||
}
|
||||
|
||||
unsigned_json = cJSON_PrintUnformatted(unsigned_event);
|
||||
if (!unsigned_json) {
|
||||
cJSON_Delete(unsigned_event);
|
||||
return -1;
|
||||
}
|
||||
|
||||
rc = nsigner_qrexec_sign_event(g_signer.qrexec_target,
|
||||
g_signer.qrexec_service,
|
||||
g_signer.role,
|
||||
unsigned_json,
|
||||
signed_event_json_out);
|
||||
|
||||
free(unsigned_json);
|
||||
cJSON_Delete(unsigned_event);
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
|
||||
cJSON *unsigned_event = NULL;
|
||||
cJSON *tags_copy = NULL;
|
||||
char *unsigned_json = NULL;
|
||||
int rc = -1;
|
||||
|
||||
unsigned_event = cJSON_CreateObject();
|
||||
if (!unsigned_event) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!cJSON_AddNumberToObject(unsigned_event, "kind", kind) ||
|
||||
!cJSON_AddStringToObject(unsigned_event, "content", content ? content : "")) {
|
||||
cJSON_Delete(unsigned_event);
|
||||
return -1;
|
||||
}
|
||||
|
||||
tags_copy = tags ? cJSON_Duplicate(tags, 1) : cJSON_CreateArray();
|
||||
if (!tags_copy) {
|
||||
cJSON_Delete(unsigned_event);
|
||||
return -1;
|
||||
}
|
||||
if (!cJSON_AddItemToObject(unsigned_event, "tags", tags_copy)) {
|
||||
cJSON_Delete(tags_copy);
|
||||
cJSON_Delete(unsigned_event);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!cJSON_AddNumberToObject(unsigned_event, "created_at", (double)timestamp) ||
|
||||
!cJSON_AddStringToObject(unsigned_event, "pubkey", g_state.npub_hex)) {
|
||||
cJSON_Delete(unsigned_event);
|
||||
return -1;
|
||||
}
|
||||
|
||||
unsigned_json = cJSON_PrintUnformatted(unsigned_event);
|
||||
if (!unsigned_json) {
|
||||
cJSON_Delete(unsigned_event);
|
||||
return -1;
|
||||
}
|
||||
|
||||
rc = nsigner_sign_event_url(g_signer.endpoint_url,
|
||||
g_signer.role,
|
||||
unsigned_json,
|
||||
signed_event_json_out);
|
||||
|
||||
free(unsigned_json);
|
||||
cJSON_Delete(unsigned_event);
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (g_signer.kind != NT_SIGNER_LOCAL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (signer_local_private_key(priv) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
tags_copy = tags ? cJSON_Duplicate(tags, 1) : cJSON_CreateArray();
|
||||
if (!tags_copy) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
event = nostr_create_and_sign_event(kind,
|
||||
content ? content : "",
|
||||
tags_copy,
|
||||
priv,
|
||||
timestamp);
|
||||
cJSON_Delete(tags_copy);
|
||||
if (!event) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*signed_event_json_out = cJSON_PrintUnformatted(event);
|
||||
cJSON_Delete(event);
|
||||
|
||||
return *signed_event_json_out ? 0 : -1;
|
||||
}
|
||||
|
||||
int signer_sign_event_json(const char *unsigned_event_json, char **signed_event_json_out) {
|
||||
unsigned char priv[32];
|
||||
cJSON *unsigned_event;
|
||||
cJSON *kind_item;
|
||||
cJSON *content_item;
|
||||
cJSON *tags_item;
|
||||
cJSON *created_at_item;
|
||||
cJSON *tags_copy;
|
||||
cJSON *signed_event;
|
||||
int kind;
|
||||
const char *content;
|
||||
time_t created_at;
|
||||
|
||||
if (!unsigned_event_json || !signed_event_json_out) {
|
||||
return -1;
|
||||
}
|
||||
*signed_event_json_out = NULL;
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER) {
|
||||
return nsigner_sign_event(g_signer.socket_name,
|
||||
g_signer.role,
|
||||
unsigned_event_json,
|
||||
signed_event_json_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER_QREXEC) {
|
||||
return nsigner_qrexec_sign_event(g_signer.qrexec_target,
|
||||
g_signer.qrexec_service,
|
||||
g_signer.role,
|
||||
unsigned_event_json,
|
||||
signed_event_json_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
|
||||
return nsigner_sign_event_url(g_signer.endpoint_url,
|
||||
g_signer.role,
|
||||
unsigned_event_json,
|
||||
signed_event_json_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind != NT_SIGNER_LOCAL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (signer_local_private_key(priv) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
unsigned_event = cJSON_Parse(unsigned_event_json);
|
||||
if (!unsigned_event || !cJSON_IsObject(unsigned_event)) {
|
||||
cJSON_Delete(unsigned_event);
|
||||
return -1;
|
||||
}
|
||||
|
||||
kind_item = cJSON_GetObjectItemCaseSensitive(unsigned_event, "kind");
|
||||
content_item = cJSON_GetObjectItemCaseSensitive(unsigned_event, "content");
|
||||
tags_item = cJSON_GetObjectItemCaseSensitive(unsigned_event, "tags");
|
||||
created_at_item = cJSON_GetObjectItemCaseSensitive(unsigned_event, "created_at");
|
||||
|
||||
if (!cJSON_IsNumber(kind_item) || !cJSON_IsArray(tags_item) || !cJSON_IsNumber(created_at_item)) {
|
||||
cJSON_Delete(unsigned_event);
|
||||
return -1;
|
||||
}
|
||||
|
||||
kind = (int)kind_item->valuedouble;
|
||||
content = (cJSON_IsString(content_item) && content_item->valuestring) ? content_item->valuestring : "";
|
||||
created_at = (time_t)created_at_item->valuedouble;
|
||||
|
||||
tags_copy = cJSON_Duplicate(tags_item, 1);
|
||||
cJSON_Delete(unsigned_event);
|
||||
if (!tags_copy) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
signed_event = nostr_create_and_sign_event(kind, content, tags_copy, priv, created_at);
|
||||
cJSON_Delete(tags_copy);
|
||||
if (!signed_event) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*signed_event_json_out = cJSON_PrintUnformatted(signed_event);
|
||||
cJSON_Delete(signed_event);
|
||||
|
||||
return *signed_event_json_out ? 0 : -1;
|
||||
}
|
||||
|
||||
int signer_nip04_encrypt(const char *peer_pub_hex, const char *plaintext, char **cipher_out) {
|
||||
unsigned char priv[32];
|
||||
unsigned char peer_pub[32];
|
||||
size_t out_sz;
|
||||
|
||||
if (!peer_pub_hex || !plaintext || !cipher_out) {
|
||||
return -1;
|
||||
}
|
||||
*cipher_out = NULL;
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER) {
|
||||
return nsigner_nip04_encrypt(g_signer.socket_name,
|
||||
g_signer.role,
|
||||
peer_pub_hex,
|
||||
plaintext,
|
||||
cipher_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER_QREXEC) {
|
||||
return nsigner_qrexec_nip04_encrypt(g_signer.qrexec_target,
|
||||
g_signer.qrexec_service,
|
||||
g_signer.role,
|
||||
peer_pub_hex,
|
||||
plaintext,
|
||||
cipher_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
|
||||
return nsigner_nip04_encrypt_url(g_signer.endpoint_url,
|
||||
g_signer.role,
|
||||
peer_pub_hex,
|
||||
plaintext,
|
||||
cipher_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind != NT_SIGNER_LOCAL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (signer_local_private_key(priv) != 0 ||
|
||||
nostr_hex_to_bytes(peer_pub_hex, peer_pub, 32) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out_sz = (strlen(plaintext) * 2U) + 1024U;
|
||||
*cipher_out = (char *)malloc(out_sz);
|
||||
if (!*cipher_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (nostr_nip04_encrypt(priv, peer_pub, plaintext, *cipher_out, out_sz) != 0) {
|
||||
free(*cipher_out);
|
||||
*cipher_out = NULL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int signer_nip04_decrypt(const char *peer_pub_hex, const char *ciphertext, char **plain_out) {
|
||||
unsigned char priv[32];
|
||||
unsigned char peer_pub[32];
|
||||
size_t out_sz;
|
||||
|
||||
if (!peer_pub_hex || !ciphertext || !plain_out) {
|
||||
return -1;
|
||||
}
|
||||
*plain_out = NULL;
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER) {
|
||||
return nsigner_nip04_decrypt(g_signer.socket_name,
|
||||
g_signer.role,
|
||||
peer_pub_hex,
|
||||
ciphertext,
|
||||
plain_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER_QREXEC) {
|
||||
return nsigner_qrexec_nip04_decrypt(g_signer.qrexec_target,
|
||||
g_signer.qrexec_service,
|
||||
g_signer.role,
|
||||
peer_pub_hex,
|
||||
ciphertext,
|
||||
plain_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
|
||||
return nsigner_nip04_decrypt_url(g_signer.endpoint_url,
|
||||
g_signer.role,
|
||||
peer_pub_hex,
|
||||
ciphertext,
|
||||
plain_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind != NT_SIGNER_LOCAL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (signer_local_private_key(priv) != 0 ||
|
||||
nostr_hex_to_bytes(peer_pub_hex, peer_pub, 32) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out_sz = strlen(ciphertext) + 1024U;
|
||||
*plain_out = (char *)malloc(out_sz);
|
||||
if (!*plain_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (nostr_nip04_decrypt(priv, peer_pub, ciphertext, *plain_out, out_sz) != 0) {
|
||||
free(*plain_out);
|
||||
*plain_out = NULL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int signer_nip44_encrypt(const char *peer_pub_hex, const char *plaintext, char **cipher_out) {
|
||||
unsigned char priv[32];
|
||||
unsigned char peer_pub[32];
|
||||
size_t out_sz;
|
||||
|
||||
if (!peer_pub_hex || !plaintext || !cipher_out) {
|
||||
return -1;
|
||||
}
|
||||
*cipher_out = NULL;
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER) {
|
||||
return nsigner_nip44_encrypt(g_signer.socket_name,
|
||||
g_signer.role,
|
||||
peer_pub_hex,
|
||||
plaintext,
|
||||
cipher_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER_QREXEC) {
|
||||
return nsigner_qrexec_nip44_encrypt(g_signer.qrexec_target,
|
||||
g_signer.qrexec_service,
|
||||
g_signer.role,
|
||||
peer_pub_hex,
|
||||
plaintext,
|
||||
cipher_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
|
||||
return nsigner_nip44_encrypt_url(g_signer.endpoint_url,
|
||||
g_signer.role,
|
||||
peer_pub_hex,
|
||||
plaintext,
|
||||
cipher_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind != NT_SIGNER_LOCAL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (signer_local_private_key(priv) != 0 ||
|
||||
nostr_hex_to_bytes(peer_pub_hex, peer_pub, 32) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out_sz = (strlen(plaintext) * 2U) + 2048U;
|
||||
*cipher_out = (char *)malloc(out_sz);
|
||||
if (!*cipher_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (nostr_nip44_encrypt(priv, peer_pub, plaintext, *cipher_out, out_sz) != 0) {
|
||||
free(*cipher_out);
|
||||
*cipher_out = NULL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int signer_nip44_decrypt(const char *peer_pub_hex, const char *ciphertext, char **plain_out) {
|
||||
unsigned char priv[32];
|
||||
unsigned char peer_pub[32];
|
||||
size_t out_sz;
|
||||
|
||||
if (!peer_pub_hex || !ciphertext || !plain_out) {
|
||||
return -1;
|
||||
}
|
||||
*plain_out = NULL;
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER) {
|
||||
return nsigner_nip44_decrypt(g_signer.socket_name,
|
||||
g_signer.role,
|
||||
peer_pub_hex,
|
||||
ciphertext,
|
||||
plain_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER_QREXEC) {
|
||||
return nsigner_qrexec_nip44_decrypt(g_signer.qrexec_target,
|
||||
g_signer.qrexec_service,
|
||||
g_signer.role,
|
||||
peer_pub_hex,
|
||||
ciphertext,
|
||||
plain_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
|
||||
return nsigner_nip44_decrypt_url(g_signer.endpoint_url,
|
||||
g_signer.role,
|
||||
peer_pub_hex,
|
||||
ciphertext,
|
||||
plain_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind != NT_SIGNER_LOCAL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (signer_local_private_key(priv) != 0 ||
|
||||
nostr_hex_to_bytes(peer_pub_hex, peer_pub, 32) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out_sz = strlen(ciphertext) + 4096U;
|
||||
*plain_out = (char *)malloc(out_sz);
|
||||
if (!*plain_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (nostr_nip44_decrypt(priv, peer_pub, ciphertext, *plain_out, out_sz) != 0) {
|
||||
free(*plain_out);
|
||||
*plain_out = NULL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int signer_nip44_self_encrypt(const char *plaintext, char **cipher_out) {
|
||||
if (!plaintext || !cipher_out) {
|
||||
return -1;
|
||||
}
|
||||
return signer_nip44_encrypt(g_state.npub_hex, plaintext, cipher_out);
|
||||
}
|
||||
|
||||
int signer_nip44_self_decrypt(const char *sender_pub_hex, const char *ciphertext, char **plain_out) {
|
||||
if (!sender_pub_hex || !ciphertext || !plain_out) {
|
||||
return -1;
|
||||
}
|
||||
return signer_nip44_decrypt(sender_pub_hex, ciphertext, plain_out);
|
||||
}
|
||||
85
src/state.c
85
src/state.c
@@ -3,11 +3,17 @@
|
||||
#include "db.h"
|
||||
#include "net.h"
|
||||
|
||||
#include "publish.h"
|
||||
#include "signer.h"
|
||||
|
||||
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/nip044.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/utils.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
nt_state_t g_state;
|
||||
|
||||
@@ -134,9 +140,8 @@ void state_cleanup(void) {
|
||||
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;
|
||||
free(g_state.user_settings_json);
|
||||
g_state.user_settings_json = NULL;
|
||||
|
||||
state_free_string_array(g_state.read_relays, g_state.read_relay_count);
|
||||
g_state.read_relays = NULL;
|
||||
@@ -279,29 +284,18 @@ static int state_pick_latest_json(char **slot, long long *slot_created, const ch
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int state_push_event_copy(char ***arr, int *count, const char *event_json) {
|
||||
char **next;
|
||||
char *copy;
|
||||
int state_nip44_self_encrypt(const char *plaintext, char **cipher_out) {
|
||||
return signer_nip44_self_encrypt(plaintext, cipher_out);
|
||||
}
|
||||
|
||||
if (!arr || !count || !event_json) {
|
||||
return -1;
|
||||
char *state_nip44_self_decrypt(const char *sender_pub_hex, const char *ciphertext) {
|
||||
char *out = NULL;
|
||||
|
||||
if (signer_nip44_self_decrypt(sender_pub_hex, ciphertext, &out) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
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;
|
||||
return out;
|
||||
}
|
||||
|
||||
int state_load_user_info(void) {
|
||||
@@ -314,6 +308,7 @@ int state_load_user_info(void) {
|
||||
char **events = NULL;
|
||||
int event_count = 0;
|
||||
int i;
|
||||
char *settings_event_json = NULL;
|
||||
|
||||
char *kind0_event = NULL;
|
||||
char *kind3_event = NULL;
|
||||
@@ -339,9 +334,8 @@ int state_load_user_info(void) {
|
||||
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;
|
||||
free(g_state.user_settings_json);
|
||||
g_state.user_settings_json = NULL;
|
||||
|
||||
fprintf(stderr, "Attempting to fetch kind 10002 relays from bootstrap relays\n");
|
||||
snprintf(filter,
|
||||
@@ -363,10 +357,10 @@ int state_load_user_info(void) {
|
||||
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");
|
||||
fprintf(stderr, "Attempting to fetch kinds [0,3,10096,17375] from user relays\n");
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"{\"authors\":[\"%s\"],\"kinds\":[0,3,10096,17375,30078],\"limit\":200}",
|
||||
"{\"authors\":[\"%s\"],\"kinds\":[0,3,10096,17375],\"limit\":200}",
|
||||
g_state.npub_hex);
|
||||
|
||||
if (nt_query_sync_verbose(filter,
|
||||
@@ -401,8 +395,6 @@ int state_load_user_info(void) {
|
||||
(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);
|
||||
@@ -432,14 +424,43 @@ int state_load_user_info(void) {
|
||||
free(kind10096_event);
|
||||
free(kind17375_event);
|
||||
|
||||
fprintf(stderr, "Attempting to fetch user-settings (kind 30078, d=user-settings)\n");
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"{\"authors\":[\"%s\"],\"kinds\":[30078],\"#d\":[\"user-settings\"],\"limit\":1}",
|
||||
g_state.npub_hex);
|
||||
|
||||
if (nt_get_first_verbose(filter,
|
||||
relays,
|
||||
relay_count,
|
||||
6000,
|
||||
"Phase 3 (user-settings)",
|
||||
1,
|
||||
&settings_event_json) == 0 && settings_event_json) {
|
||||
cJSON *ev = cJSON_Parse(settings_event_json);
|
||||
if (ev) {
|
||||
cJSON *content_item = cJSON_GetObjectItemCaseSensitive(ev, "content");
|
||||
cJSON *pubkey_item = cJSON_GetObjectItemCaseSensitive(ev, "pubkey");
|
||||
if (cJSON_IsString(content_item) && cJSON_IsString(pubkey_item)) {
|
||||
char *decrypted = state_nip44_self_decrypt(pubkey_item->valuestring,
|
||||
content_item->valuestring);
|
||||
if (decrypted) {
|
||||
g_state.user_settings_json = decrypted;
|
||||
}
|
||||
}
|
||||
cJSON_Delete(ev);
|
||||
}
|
||||
free(settings_event_json);
|
||||
}
|
||||
|
||||
fprintf(stderr,
|
||||
"Load summary: 10002=%s 0=%s 3=%s 10096=%s 17375=%s 30078=%d\n",
|
||||
"Load summary: 10002=%s 0=%s 3=%s 10096=%s 17375=%s user_settings=%s\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);
|
||||
g_state.user_settings_json ? "ok" : "missing");
|
||||
|
||||
snprintf(g_state.user_name, sizeof(g_state.user_name), "%s", "not logged in");
|
||||
if (g_state.kind0_json) {
|
||||
|
||||
@@ -13,7 +13,7 @@ static struct termios g_original_termios;
|
||||
static bool g_termios_saved = false;
|
||||
static bool g_raw_enabled = false;
|
||||
|
||||
/* Write a string while converting ^_...^: segments to bold+underline. */
|
||||
/* Write a string while converting ^_...^: segments to underline. */
|
||||
static void tui_write_with_hotkey_markup(const char *s) {
|
||||
bool in_hotkey = false;
|
||||
|
||||
@@ -23,7 +23,7 @@ static void tui_write_with_hotkey_markup(const char *s) {
|
||||
|
||||
for (size_t i = 0; s[i] != '\0';) {
|
||||
if (!in_hotkey && s[i] == '^' && s[i + 1] == '_') {
|
||||
fputs("\x1b[1;4m", stdout); /* bold + underline */
|
||||
fputs("\x1b[4m", stdout); /* underline */
|
||||
in_hotkey = true;
|
||||
i += 2;
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user