Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d64b14548 | ||
|
|
918cc618a9 | ||
|
|
b9911dd4a6 | ||
|
|
9c51e261dd | ||
|
|
e7d8b49ecc | ||
|
|
48ecf5f934 | ||
|
|
d6221f019b | ||
|
|
1630a872b1 | ||
|
|
e8de95a949 | ||
|
|
cdf90295a2 | ||
|
|
ca1c95c41c | ||
|
|
be27b088bf |
@@ -9,6 +9,7 @@ add_compile_options(-Wall -Wextra -D_GNU_SOURCE)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(LIBCURL REQUIRED libcurl)
|
||||
find_package(Curses REQUIRED)
|
||||
|
||||
# nt application sources
|
||||
file(GLOB NT_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.c")
|
||||
@@ -35,7 +36,10 @@ target_include_directories(nostr_core_lib PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/resources/nostr_core_lib/cjson
|
||||
)
|
||||
|
||||
add_executable(nt ${NT_SOURCES})
|
||||
add_executable(nt
|
||||
${NT_SOURCES}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/resources/tui_ncurses/tui_ncurses.c
|
||||
)
|
||||
|
||||
target_include_directories(nt PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
@@ -43,6 +47,8 @@ target_include_directories(nt PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/resources/nostr_core_lib/nostr_core
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/resources/nostr_core_lib/nostr_websocket
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/resources/nostr_core_lib/cjson
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/resources/tui_ncurses
|
||||
${CURSES_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
if(CMAKE_EXE_LINKER_FLAGS MATCHES "(^| )-static( |$)")
|
||||
@@ -63,6 +69,7 @@ target_link_libraries(nt PRIVATE
|
||||
nostr_core_lib
|
||||
sqlite3
|
||||
${NT_CURL_LIBS}
|
||||
${CURSES_LIBRARIES}
|
||||
secp256k1
|
||||
ssl
|
||||
crypto
|
||||
|
||||
16
README.md
16
README.md
@@ -123,12 +123,24 @@ Local SQLite at `~/.nostr/nostr.db` is the only persistent state between invocat
|
||||
- C99 compiler (gcc / clang)
|
||||
- CMake ≥ 3.10
|
||||
|
||||
**Libraries (system):**
|
||||
- `ncurses` — terminal UI
|
||||
### Runtime Dependencies
|
||||
|
||||
- `libncurses` (`ncurses-dev` when building on many distros) — TUI rendering/runtime dependency (uses alternate screen buffer)
|
||||
- `sqlite3` — local cache
|
||||
- `libcurl` — HTTP (NIP-11, AI APIs, nostr.watch)
|
||||
- `openssl` — TLS for WebSockets
|
||||
|
||||
TUI runtime note: `nt` uses the ncurses alternate screen buffer with a footer-log status line for latest action results; use the **View log** main menu entry to open the full scrollable action history.
|
||||
|
||||
### TUI Features
|
||||
|
||||
- Arrow-key navigation + shortcut keys in all menus
|
||||
- Scrollable tables with PgUp/PgDn
|
||||
- Footer status bar showing latest action result
|
||||
- **View log** menu entry to scroll through full action history
|
||||
- Full-screen pager for long content (event JSON, blog posts, relay info)
|
||||
- Modal dialogs for errors and confirmations
|
||||
|
||||
**Libraries (vendored in `resources/nostr_core_lib`):**
|
||||
- `nostr_core_lib` — Nostr protocol, NIPs, crypto, relay pool, websocket
|
||||
- `cJSON` — JSON parsing
|
||||
|
||||
163
build.sh
163
build.sh
@@ -1,49 +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 (output stage)..."
|
||||
docker build \
|
||||
--platform linux/amd64 \
|
||||
-f "$DOCKERFILE" \
|
||||
-t nt-musl-output: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..."
|
||||
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
|
||||
echo "[ERROR] Could not copy /nt_static from output image"
|
||||
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
|
||||
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"
|
||||
|
||||
26
include/nsigner_auth.h
Normal file
26
include/nsigner_auth.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#ifndef NSIGNER_AUTH_H
|
||||
#define NSIGNER_AUTH_H
|
||||
|
||||
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
||||
|
||||
#include <time.h>
|
||||
|
||||
typedef struct {
|
||||
int enabled;
|
||||
unsigned char privkey[32];
|
||||
char label[64];
|
||||
} nt_nsigner_auth_ctx_t;
|
||||
|
||||
/*
|
||||
* Build a TCP auth envelope event (kind 27235) for an n_signer JSON-RPC request.
|
||||
* Caller owns *out_auth and must cJSON_Delete() it.
|
||||
* Returns 0 on success.
|
||||
*/
|
||||
int nt_nsigner_auth_build(const char *request_id,
|
||||
const char *method,
|
||||
const cJSON *params,
|
||||
const nt_nsigner_auth_ctx_t *auth_ctx,
|
||||
time_t created_at,
|
||||
cJSON **out_auth);
|
||||
|
||||
#endif /* NSIGNER_AUTH_H */
|
||||
199
include/nsigner_client.h
Normal file
199
include/nsigner_client.h
Normal file
@@ -0,0 +1,199 @@
|
||||
#ifndef NSIGNER_CLIENT_H
|
||||
#define NSIGNER_CLIENT_H
|
||||
|
||||
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
||||
#include "nsigner_auth.h"
|
||||
|
||||
/* Selector for key derivation/role in n_signer requests. */
|
||||
typedef struct {
|
||||
int has_nostr_index;
|
||||
int nostr_index;
|
||||
char role[64];
|
||||
} nt_nsigner_selector_t;
|
||||
|
||||
static inline nt_nsigner_selector_t nsigner_selector_default(void) {
|
||||
nt_nsigner_selector_t s;
|
||||
s.has_nostr_index = 1;
|
||||
s.nostr_index = 0;
|
||||
s.role[0] = '\0';
|
||||
return s;
|
||||
}
|
||||
|
||||
/* 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
|
||||
#define NT_NSIGNER_E_AUTH -6
|
||||
|
||||
/*
|
||||
* 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 nt_nsigner_selector_t *selector, char *out_hex_65);
|
||||
|
||||
int nsigner_sign_event(const char *socket_name, const nt_nsigner_selector_t *selector,
|
||||
const char *unsigned_event_json, char **signed_event_json_out);
|
||||
|
||||
int nsigner_nip04_encrypt(const char *socket_name, const nt_nsigner_selector_t *selector,
|
||||
const char *peer_pub_hex, const char *plaintext, char **cipher_out);
|
||||
|
||||
int nsigner_nip04_decrypt(const char *socket_name, const nt_nsigner_selector_t *selector,
|
||||
const char *peer_pub_hex, const char *ciphertext, char **plain_out);
|
||||
|
||||
int nsigner_nip44_encrypt(const char *socket_name, const nt_nsigner_selector_t *selector,
|
||||
const char *peer_pub_hex, const char *plaintext, char **cipher_out);
|
||||
|
||||
int nsigner_nip44_decrypt(const char *socket_name, const nt_nsigner_selector_t *selector,
|
||||
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 nt_nsigner_selector_t *selector,
|
||||
const nt_nsigner_auth_ctx_t *auth, char *out_hex_65);
|
||||
|
||||
int nsigner_sign_event_url(const char *url, const nt_nsigner_selector_t *selector,
|
||||
const nt_nsigner_auth_ctx_t *auth,
|
||||
const char *unsigned_event_json, char **signed_event_json_out);
|
||||
|
||||
int nsigner_nip04_encrypt_url(const char *url, const nt_nsigner_selector_t *selector,
|
||||
const nt_nsigner_auth_ctx_t *auth,
|
||||
const char *peer_pub_hex, const char *plaintext, char **cipher_out);
|
||||
|
||||
int nsigner_nip04_decrypt_url(const char *url, const nt_nsigner_selector_t *selector,
|
||||
const nt_nsigner_auth_ctx_t *auth,
|
||||
const char *peer_pub_hex, const char *ciphertext, char **plain_out);
|
||||
|
||||
int nsigner_nip44_encrypt_url(const char *url, const nt_nsigner_selector_t *selector,
|
||||
const nt_nsigner_auth_ctx_t *auth,
|
||||
const char *peer_pub_hex, const char *plaintext, char **cipher_out);
|
||||
|
||||
int nsigner_nip44_decrypt_url(const char *url, const nt_nsigner_selector_t *selector,
|
||||
const nt_nsigner_auth_ctx_t *auth,
|
||||
const char *peer_pub_hex, const char *ciphertext, char **plain_out);
|
||||
|
||||
/*
|
||||
* Persistent URL session helpers.
|
||||
* Open once and reuse the fd for all RPC calls to keep a stable TCP source port.
|
||||
*/
|
||||
int nsigner_session_open_url(const char *url);
|
||||
void nsigner_session_close(int fd);
|
||||
|
||||
int nsigner_session_get_public_key(int *fd_inout,
|
||||
const char *url,
|
||||
const nt_nsigner_selector_t *selector,
|
||||
const nt_nsigner_auth_ctx_t *auth,
|
||||
char *out_hex_65);
|
||||
|
||||
int nsigner_session_sign_event(int *fd_inout,
|
||||
const char *url,
|
||||
const nt_nsigner_selector_t *selector,
|
||||
const nt_nsigner_auth_ctx_t *auth,
|
||||
const char *unsigned_event_json,
|
||||
char **signed_event_json_out);
|
||||
|
||||
int nsigner_session_nip04_encrypt(int *fd_inout,
|
||||
const char *url,
|
||||
const nt_nsigner_selector_t *selector,
|
||||
const nt_nsigner_auth_ctx_t *auth,
|
||||
const char *peer_pub_hex,
|
||||
const char *plaintext,
|
||||
char **cipher_out);
|
||||
|
||||
int nsigner_session_nip04_decrypt(int *fd_inout,
|
||||
const char *url,
|
||||
const nt_nsigner_selector_t *selector,
|
||||
const nt_nsigner_auth_ctx_t *auth,
|
||||
const char *peer_pub_hex,
|
||||
const char *ciphertext,
|
||||
char **plain_out);
|
||||
|
||||
int nsigner_session_nip44_encrypt(int *fd_inout,
|
||||
const char *url,
|
||||
const nt_nsigner_selector_t *selector,
|
||||
const nt_nsigner_auth_ctx_t *auth,
|
||||
const char *peer_pub_hex,
|
||||
const char *plaintext,
|
||||
char **cipher_out);
|
||||
|
||||
int nsigner_session_nip44_decrypt(int *fd_inout,
|
||||
const char *url,
|
||||
const nt_nsigner_selector_t *selector,
|
||||
const nt_nsigner_auth_ctx_t *auth,
|
||||
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 nt_nsigner_selector_t *selector,
|
||||
char *out_hex_65);
|
||||
|
||||
int nsigner_qrexec_sign_event(const char *target_qube,
|
||||
const char *service_name,
|
||||
const nt_nsigner_selector_t *selector,
|
||||
const char *unsigned_event_json,
|
||||
char **signed_event_json_out);
|
||||
|
||||
int nsigner_qrexec_nip04_encrypt(const char *target_qube,
|
||||
const char *service_name,
|
||||
const nt_nsigner_selector_t *selector,
|
||||
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 nt_nsigner_selector_t *selector,
|
||||
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 nt_nsigner_selector_t *selector,
|
||||
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 nt_nsigner_selector_t *selector,
|
||||
const char *peer_pub_hex,
|
||||
const char *ciphertext,
|
||||
char **plain_out);
|
||||
|
||||
#endif /* NSIGNER_CLIENT_H */
|
||||
65
include/signer.h
Normal file
65
include/signer.h
Normal file
@@ -0,0 +1,65 @@
|
||||
#ifndef SIGNER_H
|
||||
#define SIGNER_H
|
||||
|
||||
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
||||
#include "nsigner_client.h"
|
||||
|
||||
#include <time.h>
|
||||
#include <stdint.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];
|
||||
int url_session_fd;
|
||||
nt_nsigner_selector_t selector;
|
||||
nt_nsigner_auth_ctx_t url_auth;
|
||||
} nt_signer_t;
|
||||
|
||||
extern nt_signer_t g_signer;
|
||||
|
||||
void signer_init(void);
|
||||
void signer_shutdown(void);
|
||||
int signer_init_local(void);
|
||||
void signer_set_selector(int has_index, int index, const char *role);
|
||||
int signer_init_nsigner(const char *socket_name, const nt_nsigner_selector_t *selector);
|
||||
int signer_init_nsigner_qrexec(const char *target_qube, const char *service_name, const nt_nsigner_selector_t *selector);
|
||||
int signer_init_nsigner_url(const char *endpoint_url, const nt_nsigner_selector_t *selector);
|
||||
int signer_init_nsigner_url_with_session(const char *endpoint_url,
|
||||
const nt_nsigner_selector_t *selector,
|
||||
int session_fd);
|
||||
int signer_init_nsigner_url_with_session_auth(const char *endpoint_url,
|
||||
const nt_nsigner_selector_t *selector,
|
||||
int session_fd,
|
||||
const nt_nsigner_auth_ctx_t *auth_or_null);
|
||||
|
||||
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 */
|
||||
|
||||
@@ -1,28 +1,40 @@
|
||||
#ifndef TUI_H
|
||||
#define TUI_H
|
||||
|
||||
/* Initialize terminal input mode (non-canonical, no echo). */
|
||||
void tui_init(void);
|
||||
#include "tui_ncurses.h"
|
||||
|
||||
/* Restore terminal settings to their original state. */
|
||||
void tui_cleanup(void);
|
||||
#define TUI_KEY_RESIZE -2
|
||||
#define NT_HK(first, rest) "\033[4m" first "\033[0m" rest
|
||||
|
||||
/* Clear the screen area by printing terminal-height blank lines. */
|
||||
void tui_clear_screen(void);
|
||||
/* printf-like writer into the body window; appends newline. Tracks cursor row. */
|
||||
void nt_print(const char *fmt, ...);
|
||||
/* Reset body window cursor to top + werase */
|
||||
void nt_print_reset(void);
|
||||
/* Bracket external editor spawn: cleanup curses, run spawn, re-init curses */
|
||||
int nt_run_external_editor(int (*spawn)(void *user), void *user);
|
||||
/* Build a TuiFrame with the current app title + given breadcrumb */
|
||||
TuiFrame nt_frame(const char *breadcrumb);
|
||||
/* Build a TuiStatus with current user info */
|
||||
TuiStatus nt_status(void);
|
||||
/* Set the app name/version used by nt_frame */
|
||||
void nt_set_app_info(const char *name, const char *version);
|
||||
/* OSC 2 window title (orthogonal to ncurses) */
|
||||
void nt_set_window_title(const char *title);
|
||||
|
||||
/* Print formatted output and render ^_...^: hotkey highlights. */
|
||||
void tui_print(const char *fmt, ...);
|
||||
/* Set the user label shown in the header. NULL or "" hides it. */
|
||||
void nt_set_user(const char *user);
|
||||
|
||||
/* Prompt and read a blocking input line into buf, returns length. */
|
||||
int tui_get_line(const char *prompt, char *buf, int bufsize);
|
||||
/* Footer-log: append a timestamped line to the global log ring buffer.
|
||||
* The latest line is shown in the footer status area. */
|
||||
void nt_log(const char *fmt, ...);
|
||||
|
||||
/* Wait for and return a single key press byte/sequence lead byte. */
|
||||
int tui_get_key(void);
|
||||
/* Open the full log in a scrollable pager (calls tuin_pager_run). */
|
||||
void nt_log_show(void);
|
||||
|
||||
/* Query current terminal width/height in character cells. */
|
||||
void tui_get_terminal_size(int *width, int *height);
|
||||
/* Clear the log buffer. */
|
||||
void nt_log_clear(void);
|
||||
|
||||
/* Set terminal window title using ANSI OSC sequence. */
|
||||
void tui_set_window_title(const char *title);
|
||||
/* Show arbitrary text in a full-screen scrollable pager. */
|
||||
void nt_pager_show_text(const char *breadcrumb, const char *text);
|
||||
|
||||
#endif /* TUI_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
|
||||
|
||||
5
src/db.c
5
src/db.c
@@ -179,7 +179,10 @@ int db_open(void) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
snprintf(db_path, sizeof(db_path), "%s/nostr.db", dir_path);
|
||||
if (snprintf(db_path, sizeof(db_path), "%s/nostr.db", dir_path) >= (int)sizeof(db_path)) {
|
||||
fprintf(stderr, "db_open: database path too long\n");
|
||||
return -1;
|
||||
}
|
||||
rc = sqlite3_open(db_path, &g_db);
|
||||
if (rc != SQLITE_OK) {
|
||||
fprintf(stderr, "db_open: sqlite open failed: %s\n", sqlite3_errmsg(g_db));
|
||||
|
||||
30
src/editor.c
30
src/editor.c
@@ -77,19 +77,27 @@ static char *editor_read_file_malloc(const char *path) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
static int editor_run_command(const char *tmp_path, const char *editor_env) {
|
||||
typedef struct {
|
||||
const char *tmp_path;
|
||||
const char *editor_env;
|
||||
} editor_spawn_ctx_t;
|
||||
|
||||
static int editor_spawn(void *user) {
|
||||
editor_spawn_ctx_t *ctx = (editor_spawn_ctx_t *)user;
|
||||
const char *tmp_path;
|
||||
const char *editor_env;
|
||||
pid_t pid;
|
||||
int status = 0;
|
||||
|
||||
if (!tmp_path) {
|
||||
if (!ctx || !ctx->tmp_path) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
tui_cleanup();
|
||||
tmp_path = ctx->tmp_path;
|
||||
editor_env = ctx->editor_env;
|
||||
|
||||
pid = fork();
|
||||
if (pid < 0) {
|
||||
tui_init();
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -148,8 +156,6 @@ static int editor_run_command(const char *tmp_path, const char *editor_env) {
|
||||
}
|
||||
}
|
||||
|
||||
tui_init();
|
||||
|
||||
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
|
||||
return -1;
|
||||
}
|
||||
@@ -157,6 +163,18 @@ static int editor_run_command(const char *tmp_path, const char *editor_env) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int editor_run_command(const char *tmp_path, const char *editor_env) {
|
||||
editor_spawn_ctx_t ctx;
|
||||
|
||||
if (!tmp_path) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
ctx.tmp_path = tmp_path;
|
||||
ctx.editor_env = editor_env;
|
||||
return nt_run_external_editor(editor_spawn, &ctx);
|
||||
}
|
||||
|
||||
char *editor_launch(const char *initial_content) {
|
||||
char template_path[] = "/tmp/nt_editor_XXXXXX";
|
||||
int fd;
|
||||
|
||||
232
src/main.c
232
src/main.c
@@ -3,6 +3,7 @@
|
||||
#include "tui.h"
|
||||
|
||||
#include "../resources/nostr_core_lib/nostr_core/nostr_common.h"
|
||||
#include "signer.h"
|
||||
|
||||
/*
|
||||
* Canonical version is the latest git tag.
|
||||
@@ -10,9 +11,10 @@
|
||||
*/
|
||||
#define NT_VERSION_MAJOR 0
|
||||
#define NT_VERSION_MINOR 0
|
||||
#define NT_VERSION_PATCH 2
|
||||
#define NT_VERSION "v0.0.2"
|
||||
#define NT_VERSION_PATCH 14
|
||||
#define NT_VERSION "v0.0.14"
|
||||
|
||||
#include <ncurses.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -32,147 +34,121 @@ void menu_diary(void);
|
||||
void menu_ai(void);
|
||||
void menu_ecash(void);
|
||||
|
||||
static void menu_show_loaded_events(void) {
|
||||
int i;
|
||||
|
||||
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("KIND 30078 events loaded: %d", g_state.kind30078_count);
|
||||
for (i = 0; i < g_state.kind30078_count; i++) {
|
||||
tui_print("[%d] %s", i + 1, g_state.kind30078_events[i] ? g_state.kind30078_events[i] : "(null)");
|
||||
}
|
||||
|
||||
tui_print("\nPress Enter to return.");
|
||||
{
|
||||
char input[8];
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
static void nt_update_app_title(void) {
|
||||
nt_set_app_info("NOSTR TERMINAL", NT_VERSION);
|
||||
if (g_state.logged_in && g_state.user_name[0] != '\0') {
|
||||
nt_set_user(g_state.user_name);
|
||||
} else {
|
||||
nt_set_user("");
|
||||
}
|
||||
}
|
||||
|
||||
static void menu_show_loaded_events(void) {
|
||||
char buf[8192];
|
||||
int pos = 0;
|
||||
|
||||
pos += snprintf(buf + pos, sizeof(buf) - (size_t)pos, "KIND 0 (metadata content):\n%s\n\n",
|
||||
g_state.kind0_json ? g_state.kind0_json : "(not loaded)");
|
||||
pos += snprintf(buf + pos, sizeof(buf) - (size_t)pos, "KIND 3 (follows event):\n%s\n\n",
|
||||
g_state.kind3_json ? g_state.kind3_json : "(not loaded)");
|
||||
pos += snprintf(buf + pos, sizeof(buf) - (size_t)pos, "KIND 10002 (relay list event):\n%s\n\n",
|
||||
g_state.kind10002_json ? g_state.kind10002_json : "(not loaded)");
|
||||
pos += snprintf(buf + pos, sizeof(buf) - (size_t)pos, "KIND 10096 (blossom event):\n%s\n\n",
|
||||
g_state.kind10096_json ? g_state.kind10096_json : "(not loaded)");
|
||||
pos += snprintf(buf + pos, sizeof(buf) - (size_t)pos, "KIND 17375 (wallet event):\n%s\n\n",
|
||||
g_state.kind17375_json ? g_state.kind17375_json : "(not loaded)");
|
||||
pos += snprintf(buf + pos, sizeof(buf) - (size_t)pos, "USER-SETTINGS (kind 30078, d=user-settings):\n%s\n",
|
||||
g_state.user_settings_json ? g_state.user_settings_json : "(not loaded)");
|
||||
|
||||
nt_pager_show_text("> Main Menu > Loaded Events", buf);
|
||||
}
|
||||
|
||||
void menu_main(void) {
|
||||
char input[64];
|
||||
static const TuiMenuItem MAIN_ITEMS[] = {
|
||||
{NT_HK("W", "rite"), 'w'},
|
||||
{NT_HK("T", "weet"), 't'},
|
||||
{NT_HK("P", "rofile"), 'p'},
|
||||
{NT_HK("R", "elays"), 'r'},
|
||||
{NT_HK("F", "ollows"), 'f'},
|
||||
{NT_HK("K", "ind/event dump"), 'k'},
|
||||
{NT_HK("N", "otifications"), 'n'},
|
||||
{NT_HK("B", "logs/posts"), 'b'},
|
||||
{NT_HK("L", "ive feeds"), 'l'},
|
||||
{NT_HK("M", "essage"), 'm'},
|
||||
{"To" NT_HK("d", "o"), 'd'},
|
||||
{NT_HK("J", "ournal"), 'j'},
|
||||
{NT_HK("A", "i"), 'a'},
|
||||
{NT_HK("E", "cash"), 'e'},
|
||||
{NT_HK("V", "iew log"), 'v'},
|
||||
{NT_HK("Q", "uit"), 'q'},
|
||||
};
|
||||
TuiMenuState state = {0};
|
||||
|
||||
while (1) {
|
||||
tui_clear_screen();
|
||||
tui_print("NOSTR TERMINAL %s - %s\n", g_state.version, g_state.user_name);
|
||||
|
||||
if (!g_state.logged_in) {
|
||||
tui_print("^_L^:og in");
|
||||
tui_print("^_Q^:uit.");
|
||||
} else {
|
||||
tui_print("^_W^:rite");
|
||||
tui_print("^_T^:weet");
|
||||
tui_print("^_P^:rofile");
|
||||
tui_print("^_R^:elays");
|
||||
tui_print("^_F^:ollows");
|
||||
tui_print("^_K^:ind/event dump");
|
||||
tui_print("^_N^:otifications");
|
||||
tui_print("P^_o^:sts");
|
||||
tui_print("Li^_v^:e feeds");
|
||||
tui_print("Direct ^_m^:essage");
|
||||
tui_print("To^_d^:o");
|
||||
tui_print("D^_i^:ary");
|
||||
tui_print("^_A^:i");
|
||||
tui_print("^_E^:cash");
|
||||
tui_print("^_Q^:uit.");
|
||||
}
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
|
||||
if (input[0] == 'q' || input[0] == 'Q' || input[0] == 'x' || input[0] == 'X') {
|
||||
tui_print("\nHasta luego.\n");
|
||||
break;
|
||||
}
|
||||
|
||||
if (!g_state.logged_in) {
|
||||
if (input[0] == '\0' || input[0] == 'l' || input[0] == 'L') {
|
||||
menu_login();
|
||||
menu_login();
|
||||
if (!g_state.logged_in) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (input[0]) {
|
||||
case 'l':
|
||||
case 'L':
|
||||
menu_login();
|
||||
break;
|
||||
case 'w':
|
||||
case 'W':
|
||||
nt_update_app_title();
|
||||
TuiFrame frame = nt_frame("> Main Menu");
|
||||
TuiMenu menu = {MAIN_ITEMS, 16};
|
||||
TuiStatus status = nt_status();
|
||||
|
||||
int idx = tuin_menu_run(&frame, &menu, &status, &state);
|
||||
if (idx < 0 || idx == 15) {
|
||||
break;
|
||||
}
|
||||
|
||||
switch (idx) {
|
||||
case 0:
|
||||
menu_write();
|
||||
break;
|
||||
case 't':
|
||||
case 'T':
|
||||
case 1:
|
||||
menu_tweet();
|
||||
break;
|
||||
case 'p':
|
||||
case 'P':
|
||||
case 2:
|
||||
menu_profile();
|
||||
break;
|
||||
case 'r':
|
||||
case 'R':
|
||||
case 3:
|
||||
menu_relays();
|
||||
break;
|
||||
case 'f':
|
||||
case 'F':
|
||||
case 4:
|
||||
menu_follows();
|
||||
break;
|
||||
case 'n':
|
||||
case 'N':
|
||||
menu_notifications();
|
||||
break;
|
||||
case 'k':
|
||||
case 'K':
|
||||
case 5:
|
||||
menu_show_loaded_events();
|
||||
break;
|
||||
case 'o':
|
||||
case 'O':
|
||||
case 6:
|
||||
menu_notifications();
|
||||
break;
|
||||
case 7:
|
||||
menu_posts();
|
||||
break;
|
||||
case 'v':
|
||||
case 'V':
|
||||
case 8:
|
||||
menu_live();
|
||||
break;
|
||||
case 'm':
|
||||
case 'M':
|
||||
case 9:
|
||||
menu_dm();
|
||||
break;
|
||||
case 'd':
|
||||
case 'D':
|
||||
case 10:
|
||||
menu_todo();
|
||||
break;
|
||||
case 'i':
|
||||
case 'I':
|
||||
case 11:
|
||||
menu_diary();
|
||||
break;
|
||||
case 'a':
|
||||
case 'A':
|
||||
case 12:
|
||||
menu_ai();
|
||||
break;
|
||||
case 'e':
|
||||
case 'E':
|
||||
case 13:
|
||||
menu_ecash();
|
||||
break;
|
||||
case 14:
|
||||
nt_log_show();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -183,6 +159,9 @@ int main(int argc, char *argv[]) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
snprintf(g_state.version, sizeof(g_state.version), "%s", NT_VERSION);
|
||||
state_init();
|
||||
|
||||
if (db_open() != 0) {
|
||||
fprintf(stderr, "Failed to open database.\n");
|
||||
return 1;
|
||||
@@ -193,24 +172,51 @@ int main(int argc, char *argv[]) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
tui_init();
|
||||
tui_set_window_title("Nostr Terminal");
|
||||
state_init();
|
||||
snprintf(g_state.version, sizeof(g_state.version), "%s", NT_VERSION);
|
||||
|
||||
if (nostr_init() != 0) {
|
||||
fprintf(stderr, "Failed to initialize nostr library.\n");
|
||||
state_cleanup();
|
||||
tui_cleanup();
|
||||
db_close();
|
||||
return 1;
|
||||
}
|
||||
|
||||
tuin_init();
|
||||
nt_set_app_info("NOSTR TERMINAL", NT_VERSION);
|
||||
nt_set_window_title("Nostr Terminal");
|
||||
|
||||
/* Splash screen */
|
||||
{
|
||||
TuiFrame frame = nt_frame("> Splash");
|
||||
TuiStatus status = {"Press any key to continue"};
|
||||
WINDOW *body = (WINDOW *)tuin_body_window();
|
||||
int h = 0;
|
||||
int w = 0;
|
||||
const char *title = "NOSTR TERMINAL";
|
||||
int len = (int)strlen(title);
|
||||
int col;
|
||||
int row;
|
||||
|
||||
tuin_render_header(&frame);
|
||||
getmaxyx(body, h, w);
|
||||
col = (w - len) / 2;
|
||||
if (col < 0) {
|
||||
col = 0;
|
||||
}
|
||||
row = h / 2;
|
||||
werase(body);
|
||||
mvwprintw(body, row, col, "%s", title);
|
||||
mvwprintw(body, row + 1, col, "%s", NT_VERSION);
|
||||
wnoutrefresh(body);
|
||||
tuin_render_footer(&status);
|
||||
doupdate();
|
||||
tuin_get_key();
|
||||
}
|
||||
|
||||
menu_main();
|
||||
|
||||
tuin_cleanup();
|
||||
signer_shutdown();
|
||||
nostr_cleanup();
|
||||
state_cleanup();
|
||||
tui_cleanup();
|
||||
db_close();
|
||||
return 0;
|
||||
}
|
||||
|
||||
426
src/menu_ai.c
426
src/menu_ai.c
@@ -1,16 +1,15 @@
|
||||
#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/nip004.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/utils.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <ncurses.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
typedef struct {
|
||||
@@ -80,6 +79,99 @@ static void ai_prefs_free(ai_prefs_t *prefs) {
|
||||
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;
|
||||
@@ -142,156 +234,37 @@ static void nt_configure_curl_tls(CURL *curl) {
|
||||
}
|
||||
}
|
||||
|
||||
static int ai_encrypt_for_self(const char *plaintext, char **cipher_out) {
|
||||
unsigned char priv[32];
|
||||
unsigned char pub[32];
|
||||
char *out;
|
||||
size_t out_sz;
|
||||
|
||||
if (!plaintext || !cipher_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*cipher_out = NULL;
|
||||
|
||||
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (nostr_hex_to_bytes(g_state.npub_hex, pub, 32) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out_sz = (strlen(plaintext) * 4U) + 256U;
|
||||
out = (char *)malloc(out_sz);
|
||||
if (!out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (nostr_nip04_encrypt(priv, pub, plaintext, out, out_sz) != 0) {
|
||||
free(out);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*cipher_out = out;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char *ai_decrypt_from_pub(const char *sender_pub_hex, const char *ciphertext) {
|
||||
unsigned char priv[32];
|
||||
unsigned char sender_pub[32];
|
||||
size_t out_sz;
|
||||
char *out;
|
||||
|
||||
if (!sender_pub_hex || !ciphertext) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
if (nostr_hex_to_bytes(sender_pub_hex, sender_pub, 32) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out_sz = strlen(ciphertext) + 4096U;
|
||||
out = (char *)malloc(out_sz);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (nostr_nip04_decrypt(priv, sender_pub, ciphertext, out, out_sz) != 0) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
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 int ai_load_prefs(ai_prefs_t *prefs) {
|
||||
const char **read_relays;
|
||||
int relay_count = 0;
|
||||
char filter[256];
|
||||
char *event_json = NULL;
|
||||
cJSON *event;
|
||||
cJSON *content_item;
|
||||
cJSON *pubkey_item;
|
||||
char *plain;
|
||||
cJSON *root;
|
||||
cJSON *endpoint;
|
||||
cJSON *llm;
|
||||
cJSON *api_key;
|
||||
cJSON *model;
|
||||
cJSON *base_url;
|
||||
cJSON *legacy_endpoint;
|
||||
char *endpoint;
|
||||
|
||||
if (!prefs) {
|
||||
if (!prefs || !g_state.user_settings_json) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
read_relays = state_get_read_relays(&relay_count);
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"{\"authors\":[\"%s\"],\"kinds\":[30078],\"#d\":[\"prefs\"],\"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 = ai_decrypt_from_pub(pubkey_item->valuestring, content_item->valuestring);
|
||||
cJSON_Delete(event);
|
||||
if (!plain) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
root = cJSON_Parse(plain);
|
||||
free(plain);
|
||||
root = cJSON_Parse(g_state.user_settings_json);
|
||||
if (!root || !cJSON_IsObject(root)) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
endpoint = cJSON_GetObjectItemCaseSensitive(root, "ai_endpoint");
|
||||
api_key = cJSON_GetObjectItemCaseSensitive(root, "ai_api_key");
|
||||
model = cJSON_GetObjectItemCaseSensitive(root, "ai_model");
|
||||
|
||||
if (cJSON_IsString(endpoint) && endpoint->valuestring) {
|
||||
ai_set_string(&prefs->endpoint, endpoint->valuestring);
|
||||
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);
|
||||
}
|
||||
@@ -299,51 +272,106 @@ static int ai_load_prefs(ai_prefs_t *prefs) {
|
||||
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;
|
||||
char *json;
|
||||
char *cipher = NULL;
|
||||
cJSON *llm;
|
||||
cJSON *tags;
|
||||
char *new_json;
|
||||
char *cipher = NULL;
|
||||
char *base_url = NULL;
|
||||
int posted;
|
||||
|
||||
if (!prefs) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
root = cJSON_CreateObject();
|
||||
if (!root) {
|
||||
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;
|
||||
}
|
||||
|
||||
cJSON_AddStringToObject(root, "ai_endpoint", prefs->endpoint ? prefs->endpoint : "");
|
||||
cJSON_AddStringToObject(root, "ai_api_key", prefs->api_key ? prefs->api_key : "");
|
||||
cJSON_AddStringToObject(root, "ai_model", prefs->model ? prefs->model : "");
|
||||
if (!cJSON_GetObjectItemCaseSensitive(root, "v")) {
|
||||
cJSON_AddNumberToObject(root, "v", 2);
|
||||
}
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(root, "updatedAt");
|
||||
cJSON_AddNumberToObject(root, "updatedAt", (double)time(NULL));
|
||||
|
||||
json = cJSON_PrintUnformatted(root);
|
||||
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);
|
||||
if (!json) {
|
||||
free(base_url);
|
||||
if (!new_json) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ai_encrypt_for_self(json, &cipher) != 0) {
|
||||
free(json);
|
||||
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("prefs");
|
||||
tags = ai_make_d_tag("user-settings");
|
||||
if (!tags) {
|
||||
free(json);
|
||||
free(cipher);
|
||||
return -1;
|
||||
}
|
||||
|
||||
posted = publish_signed_event_with_report("Prefs", 30078, cipher, tags, 8000);
|
||||
posted = publish_signed_event_with_report("UserSettings", 30078, cipher, tags, 8000);
|
||||
cJSON_Delete(tags);
|
||||
free(json);
|
||||
free(cipher);
|
||||
|
||||
return (posted < 0) ? -1 : 0;
|
||||
@@ -464,6 +492,10 @@ static char *ai_extract_response(const char *resp_json) {
|
||||
return out;
|
||||
}
|
||||
|
||||
static void ai_show_response_stream(const char *text) {
|
||||
nt_pager_show_text("> Main Menu > AI > Chat > Response", text ? text : "");
|
||||
}
|
||||
|
||||
static void ai_chat(const ai_prefs_t *prefs) {
|
||||
char prompt[4096];
|
||||
cJSON *root;
|
||||
@@ -473,20 +505,18 @@ static void ai_chat(const ai_prefs_t *prefs) {
|
||||
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') {
|
||||
prompt[0] = '\0';
|
||||
if (tuin_prompt("prompt >", "", prompt, sizeof(prompt)) != 0 || 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));
|
||||
tuin_notice("AI API key is empty. Configure settings first.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -516,27 +546,25 @@ static void ai_chat(const ai_prefs_t *prefs) {
|
||||
}
|
||||
|
||||
snprintf(auth, sizeof(auth), "Authorization: Bearer %s", prefs->api_key ? prefs->api_key : "");
|
||||
nt_log("Requesting AI response...");
|
||||
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));
|
||||
tuin_notice("AI request failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
answer = ai_extract_response(resp_json);
|
||||
free(resp_json);
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("AI RESPONSE\n");
|
||||
if (answer) {
|
||||
tui_print("%s", answer);
|
||||
nt_log("AI response received.");
|
||||
ai_show_response_stream(answer);
|
||||
} else {
|
||||
tui_print("Failed to parse AI response.");
|
||||
tuin_notice("Failed to parse AI response.");
|
||||
}
|
||||
free(answer);
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
}
|
||||
|
||||
static void ai_choose_model(ai_prefs_t *prefs) {
|
||||
@@ -546,8 +574,8 @@ static void ai_choose_model(ai_prefs_t *prefs) {
|
||||
return;
|
||||
}
|
||||
|
||||
tui_get_line("model >", model, (int)sizeof(model));
|
||||
if (model[0] == '\0') {
|
||||
model[0] = '\0';
|
||||
if (tuin_prompt("model >", prefs->model ? prefs->model : "", model, sizeof(model)) != 0 || model[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -558,69 +586,73 @@ 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') {
|
||||
endpoint[0] = '\0';
|
||||
if (tuin_prompt("new endpoint >", prefs->endpoint ? prefs->endpoint : "", endpoint, sizeof(endpoint)) == 0 &&
|
||||
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') {
|
||||
key[0] = '\0';
|
||||
if (tuin_prompt("new api key >", "", key, sizeof(key)) == 0 && 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') {
|
||||
model[0] = '\0';
|
||||
if (tuin_prompt("new model >", prefs->model ? prefs->model : "", model, sizeof(model)) == 0 &&
|
||||
model[0] != '\0') {
|
||||
ai_set_string(&prefs->model, model);
|
||||
}
|
||||
|
||||
if (ai_save_prefs(prefs) != 0) {
|
||||
tui_print("Failed to save prefs.");
|
||||
tuin_notice("Failed to save prefs.");
|
||||
} else {
|
||||
tui_print("Prefs saved.");
|
||||
nt_log("Prefs saved.");
|
||||
}
|
||||
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
}
|
||||
|
||||
void menu_ai(void) {
|
||||
static const TuiMenuItem AI_ITEMS[] = {
|
||||
{NT_HK("C", "hat"), 'c'},
|
||||
{NT_HK("M", "odel"), 'm'},
|
||||
{NT_HK("S", "ettings"), 's'},
|
||||
{NT_HK("X", "Exit"), 'x'},
|
||||
};
|
||||
ai_prefs_t prefs;
|
||||
char input[16];
|
||||
TuiMenuState menu_state = {0};
|
||||
|
||||
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("1 - Chat");
|
||||
tui_print("2 - Choose model");
|
||||
tui_print("3 - Settings");
|
||||
tui_print("x - Back");
|
||||
TuiFrame frame = nt_frame("> Main Menu > AI");
|
||||
char status_text[1024];
|
||||
TuiStatus status;
|
||||
TuiMenu menu = {AI_ITEMS, (int)(sizeof(AI_ITEMS) / sizeof(AI_ITEMS[0]))};
|
||||
int idx;
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
snprintf(status_text,
|
||||
sizeof(status_text),
|
||||
"Model: %s | Endpoint: %s",
|
||||
prefs.model ? prefs.model : "",
|
||||
prefs.endpoint ? prefs.endpoint : "");
|
||||
status.text = status_text;
|
||||
|
||||
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
|
||||
idx = tuin_menu_run(&frame, &menu, &status, &menu_state);
|
||||
|
||||
if (idx < 0 || idx == 3) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (input[0] == '1') {
|
||||
if (idx == 0) {
|
||||
ai_chat(&prefs);
|
||||
} else if (input[0] == '2') {
|
||||
} else if (idx == 1) {
|
||||
ai_choose_model(&prefs);
|
||||
} else if (input[0] == '3') {
|
||||
} else if (idx == 2) {
|
||||
ai_settings(&prefs);
|
||||
}
|
||||
}
|
||||
|
||||
247
src/menu_diary.c
247
src/menu_diary.c
@@ -3,11 +3,13 @@
|
||||
#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 <ncurses.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -19,6 +21,35 @@ typedef struct {
|
||||
char *content;
|
||||
} diary_entry_t;
|
||||
|
||||
typedef struct {
|
||||
const diary_entry_t *entries;
|
||||
int count;
|
||||
} diary_table_ctx_t;
|
||||
|
||||
typedef struct {
|
||||
const char *initial;
|
||||
char *edited;
|
||||
} diary_editor_ctx_t;
|
||||
|
||||
static int diary_editor_spawn(void *user) {
|
||||
diary_editor_ctx_t *ctx = (diary_editor_ctx_t *)user;
|
||||
if (!ctx) {
|
||||
return -1;
|
||||
}
|
||||
ctx->edited = editor_launch(ctx->initial);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char *diary_launch_editor(const char *initial) {
|
||||
diary_editor_ctx_t ctx;
|
||||
ctx.initial = initial;
|
||||
ctx.edited = NULL;
|
||||
if (nt_run_external_editor(diary_editor_spawn, &ctx) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
return ctx.edited;
|
||||
}
|
||||
|
||||
static char *diary_strdup(const char *s) {
|
||||
size_t n;
|
||||
char *out;
|
||||
@@ -49,63 +80,13 @@ static void diary_free_entries(diary_entry_t *entries, int count) {
|
||||
}
|
||||
|
||||
static int diary_encrypt_for_self(const char *plaintext, char **cipher_out) {
|
||||
unsigned char priv[32];
|
||||
unsigned char pub[32];
|
||||
char *out;
|
||||
size_t out_sz;
|
||||
|
||||
if (!plaintext || !cipher_out) {
|
||||
return -1;
|
||||
}
|
||||
*cipher_out = NULL;
|
||||
|
||||
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (nostr_hex_to_bytes(g_state.npub_hex, pub, 32) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out_sz = (strlen(plaintext) * 4U) + 256U;
|
||||
out = (char *)malloc(out_sz);
|
||||
if (!out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (nostr_nip04_encrypt(priv, pub, plaintext, out, out_sz) != 0) {
|
||||
free(out);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*cipher_out = out;
|
||||
return 0;
|
||||
return signer_nip44_self_encrypt(plaintext, cipher_out);
|
||||
}
|
||||
|
||||
static char *diary_decrypt_from_pub(const char *sender_pub_hex, const char *ciphertext) {
|
||||
unsigned char priv[32];
|
||||
unsigned char sender_pub[32];
|
||||
size_t out_sz;
|
||||
char *out;
|
||||
char *out = NULL;
|
||||
|
||||
if (!sender_pub_hex || !ciphertext) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
if (nostr_hex_to_bytes(sender_pub_hex, sender_pub, 32) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out_sz = strlen(ciphertext) + 4096U;
|
||||
out = (char *)malloc(out_sz);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (nostr_nip04_decrypt(priv, sender_pub, ciphertext, out, out_sz) != 0) {
|
||||
free(out);
|
||||
if (signer_nip44_decrypt(sender_pub_hex, ciphertext, &out) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -239,13 +220,13 @@ static int diary_save_entry(const char *date_d, const char *plain) {
|
||||
}
|
||||
|
||||
if (diary_encrypt_for_self(plain, &cipher) != 0) {
|
||||
tui_print("Failed to encrypt diary entry.");
|
||||
tuin_notice("Failed to encrypt diary entry.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
tags = diary_make_d_tag(date_d);
|
||||
if (!tags) {
|
||||
tui_print("Failed to build diary tags.");
|
||||
tuin_notice("Failed to build diary tags.");
|
||||
free(cipher);
|
||||
return -1;
|
||||
}
|
||||
@@ -255,11 +236,11 @@ static int diary_save_entry(const char *date_d, const char *plain) {
|
||||
free(cipher);
|
||||
|
||||
if (posted <= 0) {
|
||||
tui_print("Diary publish failed (accepted by 0 relays).");
|
||||
tuin_notice("Diary publish failed (accepted by 0 relays).");
|
||||
return -1;
|
||||
}
|
||||
|
||||
tui_print("Diary saved.");
|
||||
nt_log("Diary saved.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -298,6 +279,47 @@ static int diary_cmp_desc_date(const void *a, const void *b) {
|
||||
return strcmp(eb->date_d, ea->date_d);
|
||||
}
|
||||
|
||||
static void diary_show_entry(const diary_entry_t *entry) {
|
||||
const char *content;
|
||||
size_t total;
|
||||
char *text;
|
||||
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
content = entry->content ? entry->content : "";
|
||||
total = strlen("Date: ") + strlen(entry->date_d ? entry->date_d : "") + strlen("\n\n") + strlen(content) + 1U;
|
||||
text = (char *)malloc(total);
|
||||
if (!text) {
|
||||
tuin_notice("Out of memory while preparing diary entry view.");
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(text, total, "Date: %s\n\n%s", entry->date_d ? entry->date_d : "", content);
|
||||
nt_pager_show_text("> Main Menu > Diary > Past Entries > View", text);
|
||||
free(text);
|
||||
}
|
||||
|
||||
static void diary_table_get_cell(int row, int col, char *out, size_t out_size, void *user_data) {
|
||||
const diary_table_ctx_t *ctx = (const diary_table_ctx_t *)user_data;
|
||||
|
||||
if (!out || out_size == 0U) {
|
||||
return;
|
||||
}
|
||||
out[0] = '\0';
|
||||
|
||||
if (!ctx || row < 0 || row >= ctx->count) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (col == 0) {
|
||||
snprintf(out, out_size, "%d", row + 1);
|
||||
} else if (col == 1) {
|
||||
snprintf(out, out_size, "%s", ctx->entries[row].date_d ? ctx->entries[row].date_d : "(no d-tag)");
|
||||
}
|
||||
}
|
||||
|
||||
static void diary_browse_past(void) {
|
||||
const char **read_relays;
|
||||
int relay_count = 0;
|
||||
@@ -306,8 +328,8 @@ static void diary_browse_past(void) {
|
||||
int event_count = 0;
|
||||
diary_entry_t *entries = NULL;
|
||||
int entries_count = 0;
|
||||
int selected = 0;
|
||||
int i;
|
||||
char input[64];
|
||||
|
||||
read_relays = state_get_read_relays(&relay_count);
|
||||
snprintf(filter,
|
||||
@@ -316,8 +338,7 @@ static void diary_browse_past(void) {
|
||||
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));
|
||||
tuin_notice("Failed to query diary entries.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -374,31 +395,53 @@ static void diary_browse_past(void) {
|
||||
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.");
|
||||
if (entries_count == 0) {
|
||||
nt_log("No diary entries found.");
|
||||
diary_free_entries(entries, entries_count);
|
||||
return;
|
||||
}
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
|
||||
while (1) {
|
||||
TuiFrame frame = nt_frame("> Main Menu > Diary > Past Entries");
|
||||
TuiStatus status = nt_status();
|
||||
TuiColumn cols[] = {
|
||||
{"#", 4, 1},
|
||||
{"Date", 0, 0},
|
||||
};
|
||||
diary_table_ctx_t ctx;
|
||||
TuiTable table;
|
||||
TuiMenuItem items[] = {
|
||||
{NT_HK("V", "iew selected"), 'v'},
|
||||
{NT_HK("X", "Back"), 'x'},
|
||||
};
|
||||
TuiMenu menu = {items, 2};
|
||||
TuiMenuState state = {0};
|
||||
int row;
|
||||
int action;
|
||||
|
||||
ctx.entries = entries;
|
||||
ctx.count = entries_count;
|
||||
|
||||
table.columns = cols;
|
||||
table.column_count = (int)(sizeof(cols) / sizeof(cols[0]));
|
||||
table.row_count = entries_count;
|
||||
table.user_data = &ctx;
|
||||
table.get_cell = diary_table_get_cell;
|
||||
table.is_default = NULL;
|
||||
table.prefix_len = NULL;
|
||||
|
||||
row = tuin_table_run(&frame, &table, &status);
|
||||
if (row >= 0 && row < entries_count) {
|
||||
selected = row;
|
||||
}
|
||||
|
||||
action = tuin_menu_run(&frame, &menu, &status, &state);
|
||||
if (action < 0 || action == 1) {
|
||||
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));
|
||||
if (action == 0 && selected >= 0 && selected < entries_count) {
|
||||
diary_show_entry(&entries[selected]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,39 +449,43 @@ static void diary_browse_past(void) {
|
||||
}
|
||||
|
||||
void menu_diary(void) {
|
||||
static const TuiMenuItem DIARY_ITEMS[] = {
|
||||
{NT_HK("E", "dit today's entry"), 'e'},
|
||||
{NT_HK("B", "rowse past entries"), 'b'},
|
||||
{NT_HK("X", "Exit"), 'x'},
|
||||
};
|
||||
|
||||
char today_d[16];
|
||||
char *existing = NULL;
|
||||
char *edited = NULL;
|
||||
char input[16];
|
||||
TuiMenuState menu_state = {0};
|
||||
|
||||
if (diary_get_today_d(today_d) != 0) {
|
||||
tui_print("Failed to compute today's date.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
tuin_notice("Failed to compute today's date.");
|
||||
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("1 - Edit today's entry");
|
||||
tui_print("2 - Browse past entries");
|
||||
tui_print("x - Back");
|
||||
TuiFrame frame = nt_frame("> Main Menu > Diary");
|
||||
char status_text[128];
|
||||
TuiStatus status;
|
||||
TuiMenu menu = {DIARY_ITEMS, 3};
|
||||
int idx;
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
snprintf(status_text, sizeof(status_text), "Today: %s | Existing entry: %s", today_d, existing ? "yes" : "no");
|
||||
status.text = status_text;
|
||||
|
||||
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
|
||||
idx = tuin_menu_run(&frame, &menu, &status, &menu_state);
|
||||
|
||||
if (idx < 0 || idx == 2) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (input[0] == '1') {
|
||||
edited = editor_launch(existing ? existing : "");
|
||||
if (idx == 0) {
|
||||
char *edited = diary_launch_editor(existing ? existing : "");
|
||||
if (!edited) {
|
||||
tui_print("Edit canceled.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
nt_log("Edit canceled.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -447,8 +494,8 @@ void menu_diary(void) {
|
||||
existing = diary_strdup(edited);
|
||||
}
|
||||
free(edited);
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
} else if (input[0] == '2') {
|
||||
nt_log("Diary save attempted.");
|
||||
} else if (idx == 1) {
|
||||
diary_browse_past();
|
||||
}
|
||||
}
|
||||
|
||||
369
src/menu_dm.c
369
src/menu_dm.c
@@ -1,6 +1,7 @@
|
||||
#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"
|
||||
@@ -9,6 +10,7 @@
|
||||
#include "../resources/nostr_core_lib/nostr_core/nip019.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/utils.h"
|
||||
|
||||
#include <ncurses.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -21,6 +23,11 @@ typedef struct {
|
||||
int kind;
|
||||
} dm_item_t;
|
||||
|
||||
typedef struct {
|
||||
const dm_item_t *items;
|
||||
int count;
|
||||
} dm_table_ctx_t;
|
||||
|
||||
static char *dm_strdup(const char *s) {
|
||||
size_t n;
|
||||
char *out;
|
||||
@@ -87,6 +94,64 @@ static const char *dm_shorten_hex(const char *hex, char *buf, size_t buf_sz) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
static void dm_preview_text(const char *text, char *buf, size_t buf_sz, int max_chars) {
|
||||
size_t i;
|
||||
size_t j = 0;
|
||||
|
||||
if (!buf || buf_sz == 0U) {
|
||||
return;
|
||||
}
|
||||
buf[0] = '\0';
|
||||
|
||||
if (!text || text[0] == '\0' || max_chars <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; text[i] != '\0' && j + 1U < buf_sz; i++) {
|
||||
char ch = text[i];
|
||||
if (ch == '\n' || ch == '\r' || ch == '\t') {
|
||||
ch = ' ';
|
||||
}
|
||||
buf[j++] = ch;
|
||||
if ((int)j >= max_chars) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
buf[j] = '\0';
|
||||
|
||||
if (text[i] != '\0' && j + 4U < buf_sz) {
|
||||
buf[j++] = '.';
|
||||
buf[j++] = '.';
|
||||
buf[j++] = '.';
|
||||
buf[j] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static void dm_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, "-");
|
||||
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 dm_push_item(dm_item_t **items, int *count, dm_item_t *src) {
|
||||
dm_item_t *next;
|
||||
|
||||
@@ -144,30 +209,24 @@ static void menu_dm_send(void) {
|
||||
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') {
|
||||
who[0] = '\0';
|
||||
if (tuin_prompt("recipient (npub or hex) >", "", who, sizeof(who)) != 0 || 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));
|
||||
tuin_notice("Invalid recipient. Use npub or 64-char hex pubkey.");
|
||||
return;
|
||||
}
|
||||
|
||||
tui_get_line("message >", msg, (int)sizeof(msg));
|
||||
if (msg[0] == '\0') {
|
||||
msg[0] = '\0';
|
||||
if (tuin_prompt("message >", "", msg, sizeof(msg)) != 0 || msg[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
|
||||
tui_print("Invalid private key in state.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
if (signer_get_local_private_key(priv) != 0) {
|
||||
tuin_notice("Private key unavailable for DM send.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -180,21 +239,18 @@ static void menu_dm_send(void) {
|
||||
NULL,
|
||||
g_state.npub_hex);
|
||||
if (!rumor) {
|
||||
tui_print("Failed to build NIP-17 DM rumor.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
tuin_notice("Failed to build NIP-17 DM rumor.");
|
||||
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));
|
||||
tuin_notice("Failed to create NIP-17 gift wrap event(s).");
|
||||
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;
|
||||
@@ -219,42 +275,16 @@ static void menu_dm_send(void) {
|
||||
}
|
||||
|
||||
if (any_ok) {
|
||||
tui_print("DM sent.");
|
||||
nt_log("DM sent.");
|
||||
} else {
|
||||
tui_print("DM publish failed.");
|
||||
tuin_notice("DM publish failed.");
|
||||
}
|
||||
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
}
|
||||
|
||||
static char *dm_try_decrypt_kind4(const char *sender_pub_hex, const char *ciphertext) {
|
||||
unsigned char priv[32];
|
||||
unsigned char sender_pub[32];
|
||||
size_t out_sz;
|
||||
char *out;
|
||||
|
||||
if (!sender_pub_hex || !ciphertext) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
if (nostr_hex_to_bytes(sender_pub_hex, sender_pub, 32) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out_sz = strlen(ciphertext) + 1024U;
|
||||
out = (char *)malloc(out_sz);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (nostr_nip04_decrypt(priv, sender_pub, ciphertext, out, out_sz) != 0) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *out = NULL;
|
||||
if (!sender_pub_hex || !ciphertext) return NULL;
|
||||
if (signer_nip04_decrypt(sender_pub_hex, ciphertext, &out) != 0) return NULL;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -344,7 +374,7 @@ static void dm_collect_nip17_kind1059(dm_item_t **items, int *items_count) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
|
||||
if (signer_get_local_private_key(priv) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -419,15 +449,183 @@ static void dm_collect_nip17_kind1059(dm_item_t **items, int *items_count) {
|
||||
free(events);
|
||||
}
|
||||
|
||||
static void dm_table_get_cell(int row, int col, char *out, size_t out_size, void *user_data) {
|
||||
const dm_table_ctx_t *ctx = (const dm_table_ctx_t *)user_data;
|
||||
const dm_item_t *it;
|
||||
char sender_short[32];
|
||||
char date_buf[64];
|
||||
|
||||
if (!out || out_size == 0U) {
|
||||
return;
|
||||
}
|
||||
out[0] = '\0';
|
||||
|
||||
if (!ctx || row < 0 || row >= ctx->count) {
|
||||
return;
|
||||
}
|
||||
|
||||
it = &ctx->items[row];
|
||||
dm_format_date(it->created_at, date_buf, sizeof(date_buf));
|
||||
|
||||
if (col == 0) {
|
||||
snprintf(out, out_size, "%d", row + 1);
|
||||
} else if (col == 1) {
|
||||
snprintf(out, out_size, "%d", it->kind);
|
||||
} else if (col == 2) {
|
||||
snprintf(out, out_size, "%s", dm_shorten_hex(it->sender_pub, sender_short, sizeof(sender_short)));
|
||||
} else if (col == 3) {
|
||||
char preview[256];
|
||||
dm_preview_text(it->content, preview, sizeof(preview), 120);
|
||||
snprintf(out, out_size, "%s", preview);
|
||||
} else if (col == 4) {
|
||||
snprintf(out, out_size, "%s", date_buf);
|
||||
}
|
||||
}
|
||||
|
||||
static int dm_build_detail_lines(const dm_item_t *it, char ***lines_out, int *count_out) {
|
||||
char **lines = NULL;
|
||||
int count = 0;
|
||||
char date_buf[64];
|
||||
const char *content;
|
||||
size_t i;
|
||||
size_t start = 0;
|
||||
|
||||
if (!it || !lines_out || !count_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*lines_out = NULL;
|
||||
*count_out = 0;
|
||||
|
||||
dm_format_date(it->created_at, date_buf, sizeof(date_buf));
|
||||
|
||||
#define DM_PUSH_LINE(txt) \
|
||||
do { \
|
||||
char **next = (char **)realloc(lines, (size_t)(count + 1) * sizeof(char *)); \
|
||||
if (!next) { \
|
||||
goto fail; \
|
||||
} \
|
||||
lines = next; \
|
||||
lines[count] = dm_strdup((txt) ? (txt) : ""); \
|
||||
if (!lines[count]) { \
|
||||
goto fail; \
|
||||
} \
|
||||
count++; \
|
||||
} while (0)
|
||||
|
||||
{
|
||||
char hdr[128];
|
||||
snprintf(hdr, sizeof(hdr), "kind: %d", it->kind);
|
||||
DM_PUSH_LINE(hdr);
|
||||
}
|
||||
{
|
||||
char sender_short[64];
|
||||
char hdr[160];
|
||||
snprintf(hdr,
|
||||
sizeof(hdr),
|
||||
"from: %s",
|
||||
dm_shorten_hex(it->sender_pub, sender_short, sizeof(sender_short)));
|
||||
DM_PUSH_LINE(hdr);
|
||||
}
|
||||
{
|
||||
char hdr[128];
|
||||
snprintf(hdr, sizeof(hdr), "created: %s", date_buf);
|
||||
DM_PUSH_LINE(hdr);
|
||||
}
|
||||
DM_PUSH_LINE("");
|
||||
|
||||
content = it->content ? it->content : "";
|
||||
for (i = 0; ; i++) {
|
||||
if (content[i] == '\n' || content[i] == '\0') {
|
||||
size_t len = i - start;
|
||||
char *ln = (char *)malloc(len + 1U);
|
||||
if (!ln) {
|
||||
goto fail;
|
||||
}
|
||||
memcpy(ln, content + start, len);
|
||||
ln[len] = '\0';
|
||||
DM_PUSH_LINE(ln);
|
||||
free(ln);
|
||||
if (content[i] == '\0') {
|
||||
break;
|
||||
}
|
||||
start = i + 1U;
|
||||
}
|
||||
}
|
||||
|
||||
*lines_out = lines;
|
||||
*count_out = count;
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
for (i = 0; i < (size_t)count; i++) {
|
||||
free(lines[i]);
|
||||
}
|
||||
free(lines);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void dm_free_detail_lines(char **lines, int count) {
|
||||
int i;
|
||||
for (i = 0; i < count; i++) {
|
||||
free(lines[i]);
|
||||
}
|
||||
free(lines);
|
||||
}
|
||||
|
||||
static void dm_view_item(const dm_item_t *it) {
|
||||
char **lines = NULL;
|
||||
int line_count = 0;
|
||||
size_t total = 1U;
|
||||
char *text;
|
||||
int i;
|
||||
|
||||
if (!it) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dm_build_detail_lines(it, &lines, &line_count) != 0) {
|
||||
tuin_notice("Failed to render DM thread view.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < line_count; i++) {
|
||||
total += strlen(lines[i] ? lines[i] : "") + 1U;
|
||||
}
|
||||
|
||||
text = (char *)malloc(total);
|
||||
if (!text) {
|
||||
dm_free_detail_lines(lines, line_count);
|
||||
tuin_notice("Out of memory while preparing DM view.");
|
||||
return;
|
||||
}
|
||||
|
||||
text[0] = '\0';
|
||||
for (i = 0; i < line_count; i++) {
|
||||
strcat(text, lines[i] ? lines[i] : "");
|
||||
strcat(text, "\n");
|
||||
}
|
||||
|
||||
nt_pager_show_text("> Main Menu > DM > Thread", text);
|
||||
|
||||
free(text);
|
||||
dm_free_detail_lines(lines, line_count);
|
||||
}
|
||||
|
||||
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_table_ctx_t ctx;
|
||||
TuiColumn cols[] = {
|
||||
{"#", 4, 1},
|
||||
{"Kind", 8, 0},
|
||||
{"From", 16, 0},
|
||||
{"Message", 0, 0},
|
||||
{"Date", 18, 0},
|
||||
};
|
||||
TuiTable table;
|
||||
TuiFrame frame = nt_frame("> Main Menu > DM > Inbox");
|
||||
TuiStatus status;
|
||||
|
||||
dm_collect_nip17_kind1059(&items, &items_count);
|
||||
dm_collect_legacy_kind4(&items, &items_count);
|
||||
@@ -436,42 +634,59 @@ static void menu_dm_read_inbox(void) {
|
||||
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) {
|
||||
nt_log("No DMs found.");
|
||||
dm_free_items(items, items_count);
|
||||
return;
|
||||
}
|
||||
|
||||
if (items_count == 0) {
|
||||
tui_print("No DMs found.");
|
||||
ctx.items = items;
|
||||
ctx.count = items_count;
|
||||
|
||||
table.columns = cols;
|
||||
table.column_count = (int)(sizeof(cols) / sizeof(cols[0]));
|
||||
table.row_count = items_count;
|
||||
table.user_data = &ctx;
|
||||
table.get_cell = dm_table_get_cell;
|
||||
table.is_default = NULL;
|
||||
table.prefix_len = NULL;
|
||||
|
||||
status.text = "Select a message and press Enter";
|
||||
|
||||
while (1) {
|
||||
int row = tuin_table_run(&frame, &table, &status);
|
||||
if (row < 0) {
|
||||
break;
|
||||
}
|
||||
if (row >= 0 && row < items_count) {
|
||||
dm_view_item(&items[row]);
|
||||
}
|
||||
}
|
||||
|
||||
dm_free_items(items, items_count);
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
}
|
||||
|
||||
void menu_dm(void) {
|
||||
char input[16];
|
||||
static const TuiMenuItem DM_ITEMS[] = {
|
||||
{NT_HK("S", "end DM (NIP-17)"), 's'},
|
||||
{NT_HK("R", "ead inbox (kind 1059 + kind 4)"), 'r'},
|
||||
{NT_HK("X", "Exit"), 'x'},
|
||||
};
|
||||
TuiMenuState menu_state = {0};
|
||||
|
||||
while (1) {
|
||||
tui_clear_screen();
|
||||
tui_print("DIRECT MESSAGE\n");
|
||||
tui_print("1 - Send DM (NIP-17)");
|
||||
tui_print("2 - Read inbox (kind 1059 + kind 4)");
|
||||
tui_print("x - Back");
|
||||
TuiFrame frame = nt_frame("> Main Menu > DM");
|
||||
TuiStatus status = nt_status();
|
||||
TuiMenu menu = {DM_ITEMS, (int)(sizeof(DM_ITEMS) / sizeof(DM_ITEMS[0]))};
|
||||
int idx = tuin_menu_run(&frame, &menu, &status, &menu_state);
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
|
||||
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
|
||||
if (idx < 0 || idx == 2) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (input[0] == '1') {
|
||||
if (idx == 0) {
|
||||
menu_dm_send();
|
||||
} else if (input[0] == '2') {
|
||||
} else if (idx == 1) {
|
||||
menu_dm_read_inbox();
|
||||
}
|
||||
}
|
||||
|
||||
327
src/menu_ecash.c
327
src/menu_ecash.c
@@ -20,6 +20,10 @@ typedef struct {
|
||||
int proof_count;
|
||||
} ecash_wallet_t;
|
||||
|
||||
typedef struct {
|
||||
const ecash_wallet_t *wallet;
|
||||
} ecash_balance_table_ctx_t;
|
||||
|
||||
static char *ecash_strdup(const char *s) {
|
||||
size_t n;
|
||||
char *out;
|
||||
@@ -406,124 +410,216 @@ static uint64_t ecash_wallet_total(const ecash_wallet_t *w) {
|
||||
return total;
|
||||
}
|
||||
|
||||
static void ecash_show_balance(const ecash_wallet_t *w) {
|
||||
int i;
|
||||
char hold[16];
|
||||
static uint64_t ecash_mint_total(const ecash_wallet_t *w, int mint_idx) {
|
||||
int j;
|
||||
uint64_t mint_total = 0;
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("ECASH BALANCE\n");
|
||||
tui_print("Total: %llu sats", (unsigned long long)ecash_wallet_total(w));
|
||||
tui_print("");
|
||||
if (!w || mint_idx < 0 || mint_idx >= w->mint_count) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (j = 0; j < w->proof_count; j++) {
|
||||
if (w->proof_mints[j] && w->mints[mint_idx] && strcmp(w->proof_mints[j], w->mints[mint_idx]) == 0) {
|
||||
mint_total += w->proofs[j].amount;
|
||||
}
|
||||
}
|
||||
|
||||
return mint_total;
|
||||
}
|
||||
|
||||
static void ecash_balance_get_cell(int row, int col, char *out, size_t out_size, void *user_data) {
|
||||
const ecash_balance_table_ctx_t *ctx = (const ecash_balance_table_ctx_t *)user_data;
|
||||
const ecash_wallet_t *w;
|
||||
|
||||
if (!out || out_size == 0U) {
|
||||
return;
|
||||
}
|
||||
out[0] = '\0';
|
||||
|
||||
if (!ctx || !ctx->wallet) {
|
||||
return;
|
||||
}
|
||||
|
||||
w = ctx->wallet;
|
||||
if (row < 0 || row >= w->mint_count) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (col == 0) {
|
||||
snprintf(out, out_size, "%d", row + 1);
|
||||
} else if (col == 1) {
|
||||
snprintf(out, out_size, "%s", w->mints[row] ? w->mints[row] : "");
|
||||
} else if (col == 2) {
|
||||
snprintf(out, out_size, "%llu", (unsigned long long)ecash_mint_total(w, row));
|
||||
}
|
||||
}
|
||||
|
||||
static void ecash_show_balance(const ecash_wallet_t *w) {
|
||||
TuiFrame frame = nt_frame("> Main Menu > Ecash > Balance");
|
||||
char status_text[128];
|
||||
TuiStatus status;
|
||||
|
||||
snprintf(status_text,
|
||||
sizeof(status_text),
|
||||
"Total: %llu sats | Proof count: %d",
|
||||
(unsigned long long)ecash_wallet_total(w),
|
||||
w ? w->proof_count : 0);
|
||||
status.text = status_text;
|
||||
|
||||
if (!w || w->mint_count == 0) {
|
||||
tuin_notice("No mints configured.");
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
TuiColumn cols[] = {
|
||||
{"#", 4, 1},
|
||||
{"Mint", 0, 0},
|
||||
{"Sats", 12, 1},
|
||||
};
|
||||
ecash_balance_table_ctx_t ctx;
|
||||
TuiTable table;
|
||||
|
||||
ctx.wallet = w;
|
||||
|
||||
table.columns = cols;
|
||||
table.column_count = (int)(sizeof(cols) / sizeof(cols[0]));
|
||||
table.row_count = w->mint_count;
|
||||
table.user_data = &ctx;
|
||||
table.get_cell = ecash_balance_get_cell;
|
||||
table.is_default = NULL;
|
||||
table.prefix_len = NULL;
|
||||
|
||||
(void)tuin_table_run(&frame, &table, &status);
|
||||
}
|
||||
}
|
||||
|
||||
static int ecash_select_mint_index(const char *breadcrumb, const ecash_wallet_t *w, int *mint_idx_out) {
|
||||
TuiMenuItem *items;
|
||||
char (*labels)[512];
|
||||
TuiMenu menu;
|
||||
TuiMenuState state = {0};
|
||||
TuiFrame frame;
|
||||
TuiStatus status = nt_status();
|
||||
int i;
|
||||
int choice;
|
||||
|
||||
if (!w || w->mint_count <= 0 || !mint_idx_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
items = (TuiMenuItem *)calloc((size_t)(w->mint_count + 1), sizeof(TuiMenuItem));
|
||||
labels = (char(*)[512])calloc((size_t)(w->mint_count + 1), sizeof(*labels));
|
||||
if (!items || !labels) {
|
||||
free(items);
|
||||
free(labels);
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (i = 0; i < w->mint_count; i++) {
|
||||
int j;
|
||||
uint64_t mint_total = 0;
|
||||
snprintf(labels[i], sizeof(labels[i]), "%s", w->mints[i] ? w->mints[i] : "");
|
||||
items[i].label = labels[i];
|
||||
items[i].shortcut = 0;
|
||||
}
|
||||
snprintf(labels[w->mint_count], sizeof(labels[w->mint_count]), "Back");
|
||||
items[w->mint_count].label = labels[w->mint_count];
|
||||
items[w->mint_count].shortcut = 'b';
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
menu.items = items;
|
||||
menu.count = w->mint_count + 1;
|
||||
frame = nt_frame(breadcrumb ? breadcrumb : "> Main Menu > Ecash > Select Mint");
|
||||
|
||||
tui_print("%2d. %s", i + 1, w->mints[i]);
|
||||
tui_print(" balance: %llu sats", (unsigned long long)mint_total);
|
||||
choice = tuin_menu_run(&frame, &menu, &status, &state);
|
||||
|
||||
free(items);
|
||||
free(labels);
|
||||
|
||||
if (choice < 0 || choice == w->mint_count) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
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));
|
||||
*mint_idx_out = choice;
|
||||
return 0;
|
||||
}
|
||||
|
||||
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') {
|
||||
mint[0] = '\0';
|
||||
if (tuin_prompt("mint url >", "", mint, sizeof(mint)) != 0 || mint[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ecash_add_mint(w, mint) != 0) {
|
||||
tui_print("Failed to add mint.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
tuin_notice("Failed to add mint.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ecash_publish_wallet(w) != 0) {
|
||||
tui_print("Failed to publish wallet metadata.");
|
||||
tuin_notice("Failed to publish wallet metadata.");
|
||||
} else {
|
||||
tui_print("Mint added.");
|
||||
nt_log("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));
|
||||
tuin_notice("No mints to remove.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < w->mint_count; i++) {
|
||||
tui_print("%2d. %s", i + 1, w->mints[i]);
|
||||
if (ecash_select_mint_index("> Main Menu > Ecash > Remove Mint", w, &idx) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
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));
|
||||
if (tuin_confirm("Confirm remove selected mint?") != 1) {
|
||||
nt_log("Remove canceled.");
|
||||
return;
|
||||
}
|
||||
|
||||
free(w->mints[idx]);
|
||||
for (i = idx; i < w->mint_count - 1; i++) {
|
||||
w->mints[i] = w->mints[i + 1];
|
||||
for (; idx < w->mint_count - 1; idx++) {
|
||||
w->mints[idx] = w->mints[idx + 1];
|
||||
}
|
||||
w->mint_count--;
|
||||
|
||||
if (ecash_publish_wallet(w) != 0) {
|
||||
tui_print("Failed to publish wallet metadata.");
|
||||
tuin_notice("Failed to publish wallet metadata.");
|
||||
} else {
|
||||
tui_print("Mint removed.");
|
||||
nt_log("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') {
|
||||
|
||||
token[0] = '\0';
|
||||
if (tuin_prompt("paste cashu token >", "", token, sizeof(token)) != 0 || token[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tuin_confirm("Import token into wallet?") != 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cashu_decode_token(token, &decoded) != 0) {
|
||||
tui_print("Failed to decode token.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
tuin_notice("Failed to decode token.");
|
||||
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));
|
||||
tuin_notice("Token did not contain usable proofs.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -535,11 +631,10 @@ static void ecash_receive_token_menu(ecash_wallet_t *w) {
|
||||
cashu_free_decoded_token(&decoded);
|
||||
|
||||
if (ecash_publish_wallet(w) != 0) {
|
||||
tui_print("Token imported locally, but publish failed.");
|
||||
tuin_notice("Token imported locally, but publish failed.");
|
||||
} else {
|
||||
tui_print("Token received and wallet updated.");
|
||||
nt_log("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) {
|
||||
@@ -582,8 +677,8 @@ static void ecash_remove_proofs_by_global_indices(ecash_wallet_t *w, const int *
|
||||
}
|
||||
|
||||
static void ecash_send_token_menu(ecash_wallet_t *w) {
|
||||
char input[128];
|
||||
int mint_idx;
|
||||
char input[128];
|
||||
uint64_t target_amount;
|
||||
int i;
|
||||
nostr_cashu_proof_t *mint_proofs = NULL;
|
||||
@@ -595,35 +690,32 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
|
||||
cashu_decoded_token_t token;
|
||||
char *encoded = NULL;
|
||||
int *selected_global = NULL;
|
||||
char hold[16];
|
||||
char result[1024];
|
||||
|
||||
if (!w) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (w->mint_count <= 0) {
|
||||
tui_print("No mints configured.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
tuin_notice("No mints configured.");
|
||||
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));
|
||||
if (ecash_select_mint_index("> Main Menu > Ecash > Send Token", w, &mint_idx) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
tui_get_line("amount (sats) >", input, (int)sizeof(input));
|
||||
input[0] = '\0';
|
||||
if (tuin_prompt("amount (sats) >", "", input, sizeof(input)) != 0) {
|
||||
return;
|
||||
}
|
||||
target_amount = (uint64_t)strtoull(input, NULL, 10);
|
||||
if (target_amount == 0) {
|
||||
tui_print("Invalid amount.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
tuin_notice("Invalid amount.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (tuin_confirm("Create and remove proofs for outgoing token?") != 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -636,8 +728,7 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
|
||||
if (!next_proofs) {
|
||||
free(mint_proofs);
|
||||
free(mint_global_indices);
|
||||
tui_print("Memory error.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
tuin_notice("Memory error.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -647,8 +738,7 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
|
||||
if (!next_idx) {
|
||||
free(mint_proofs);
|
||||
free(mint_global_indices);
|
||||
tui_print("Memory error.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
tuin_notice("Memory error.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -662,8 +752,7 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
|
||||
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));
|
||||
tuin_notice("No proofs available for selected mint.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -671,8 +760,7 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
|
||||
if (!selected_local) {
|
||||
free(mint_proofs);
|
||||
free(mint_global_indices);
|
||||
tui_print("Memory error.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
tuin_notice("Memory error.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -686,8 +774,7 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
|
||||
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));
|
||||
tuin_notice("Could not select proofs for that amount.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -703,8 +790,7 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
|
||||
free(mint_global_indices);
|
||||
free(selected_global);
|
||||
cashu_free_decoded_token(&token);
|
||||
tui_print("Memory error.");
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
tuin_notice("Memory error.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -719,8 +805,7 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
|
||||
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));
|
||||
tuin_notice("Failed to assemble token proofs.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -734,23 +819,21 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
|
||||
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));
|
||||
tuin_notice("Failed to encode token.");
|
||||
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));
|
||||
snprintf(result,
|
||||
sizeof(result),
|
||||
"Requested: %llu sats\nSelected: %llu sats\n\nToken:\n%s\n\nNote: Mint HTTP swap/melt flows are not yet implemented in this MVP.",
|
||||
(unsigned long long)target_amount,
|
||||
(unsigned long long)selected_total,
|
||||
encoded);
|
||||
nt_pager_show_text("> Main Menu > Ecash > Send Token", result);
|
||||
nt_log("Token created and selected proofs removed from wallet.");
|
||||
|
||||
free(encoded);
|
||||
free(selected_local);
|
||||
@@ -761,40 +844,52 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
|
||||
}
|
||||
|
||||
void menu_ecash(void) {
|
||||
static const TuiMenuItem ECASH_ITEMS[] = {
|
||||
{NT_HK("B", "alance"), 'b'},
|
||||
{NT_HK("A", "dd mint"), 'a'},
|
||||
{NT_HK("R", "emove mint"), 'r'},
|
||||
{NT_HK("I", "mport token"), 'i'},
|
||||
{NT_HK("S", "end token"), 's'},
|
||||
{"E" NT_HK("x", "it"), 'x'},
|
||||
};
|
||||
|
||||
ecash_wallet_t wallet;
|
||||
char input[16];
|
||||
TuiMenuState menu_state = {0};
|
||||
|
||||
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("1 - Balance");
|
||||
tui_print("2 - Add mint");
|
||||
tui_print("3 - Remove mint");
|
||||
tui_print("4 - Receive token");
|
||||
tui_print("5 - Send token");
|
||||
tui_print("x - Back");
|
||||
TuiFrame frame = nt_frame("> Main Menu > Ecash");
|
||||
char status_text[128];
|
||||
TuiStatus status;
|
||||
TuiMenu menu = {ECASH_ITEMS, 6};
|
||||
int idx;
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
snprintf(status_text,
|
||||
sizeof(status_text),
|
||||
"Mints: %d | Proofs: %d | Total: %llu sats",
|
||||
wallet.mint_count,
|
||||
wallet.proof_count,
|
||||
(unsigned long long)ecash_wallet_total(&wallet));
|
||||
status.text = status_text;
|
||||
|
||||
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
|
||||
idx = tuin_menu_run(&frame, &menu, &status, &menu_state);
|
||||
|
||||
if (idx < 0 || idx == 5) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (input[0] == '1') {
|
||||
if (idx == 0) {
|
||||
ecash_show_balance(&wallet);
|
||||
} else if (input[0] == '2') {
|
||||
} else if (idx == 1) {
|
||||
ecash_add_mint_menu(&wallet);
|
||||
} else if (input[0] == '3') {
|
||||
} else if (idx == 2) {
|
||||
ecash_remove_mint_menu(&wallet);
|
||||
} else if (input[0] == '4') {
|
||||
} else if (idx == 3) {
|
||||
ecash_receive_token_menu(&wallet);
|
||||
} else if (input[0] == '5') {
|
||||
} else if (idx == 4) {
|
||||
ecash_send_token_menu(&wallet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
cJSON *tags;
|
||||
int *tag_indices;
|
||||
int count;
|
||||
} follows_table_ctx_t;
|
||||
|
||||
static int follows_publish_kind3(cJSON *event_obj) {
|
||||
cJSON *tags;
|
||||
cJSON *content;
|
||||
@@ -30,10 +36,123 @@ static int follows_publish_kind3(cJSON *event_obj) {
|
||||
8000);
|
||||
}
|
||||
|
||||
static int follows_collect_rows(cJSON *tags, int **out_indices) {
|
||||
int n;
|
||||
int i;
|
||||
int count = 0;
|
||||
int *indices;
|
||||
|
||||
if (!out_indices || !cJSON_IsArray(tags)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
*out_indices = NULL;
|
||||
n = cJSON_GetArraySize(tags);
|
||||
if (n <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
indices = (int *)malloc((size_t)n * sizeof(int));
|
||||
if (!indices) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (i = 0; i < n; i++) {
|
||||
cJSON *tag = cJSON_GetArrayItem(tags, i);
|
||||
cJSON *t0 = cJSON_GetArrayItem(tag, 0);
|
||||
if (!cJSON_IsArray(tag) || !cJSON_IsString(t0) || strcmp(t0->valuestring, "p") != 0) {
|
||||
continue;
|
||||
}
|
||||
indices[count++] = i;
|
||||
}
|
||||
|
||||
*out_indices = indices;
|
||||
return count;
|
||||
}
|
||||
|
||||
static void follows_table_get_cell(int row, int col, char *out, size_t out_size, void *user_data) {
|
||||
const follows_table_ctx_t *ctx = (const follows_table_ctx_t *)user_data;
|
||||
cJSON *tag;
|
||||
cJSON *t1;
|
||||
cJSON *t3;
|
||||
|
||||
if (!out || out_size == 0U) {
|
||||
return;
|
||||
}
|
||||
out[0] = '\0';
|
||||
|
||||
if (!ctx || row < 0 || row >= ctx->count) {
|
||||
return;
|
||||
}
|
||||
|
||||
tag = cJSON_GetArrayItem(ctx->tags, ctx->tag_indices[row]);
|
||||
t1 = cJSON_GetArrayItem(tag, 1);
|
||||
t3 = cJSON_GetArrayItem(tag, 3);
|
||||
|
||||
if (col == 0) {
|
||||
snprintf(out, out_size, "%d", row + 1);
|
||||
} else if (col == 1) {
|
||||
char npub[128] = {0};
|
||||
if (cJSON_IsString(t1) && t1->valuestring) {
|
||||
unsigned char pub[32];
|
||||
if (strlen(t1->valuestring) == 64 && nostr_hex_to_bytes(t1->valuestring, pub, 32) == 0) {
|
||||
if (nostr_key_to_bech32(pub, "npub", npub) != 0) {
|
||||
snprintf(npub, sizeof(npub), "%s", t1->valuestring);
|
||||
}
|
||||
} else {
|
||||
snprintf(npub, sizeof(npub), "%s", t1->valuestring);
|
||||
}
|
||||
}
|
||||
snprintf(out, out_size, "%s", npub);
|
||||
} else if (col == 2) {
|
||||
snprintf(out, out_size, "%s", (cJSON_IsString(t3) && t3->valuestring) ? t3->valuestring : "");
|
||||
}
|
||||
}
|
||||
|
||||
static void follows_show_profile(cJSON *meta) {
|
||||
char *meta_json;
|
||||
size_t total;
|
||||
char *view_text;
|
||||
|
||||
if (!meta) {
|
||||
nt_pager_show_text("> Main Menu > Follows > Profile", "No metadata content.");
|
||||
return;
|
||||
}
|
||||
|
||||
meta_json = cJSON_Print(meta);
|
||||
if (!meta_json) {
|
||||
tuin_notice("Failed to render profile metadata.");
|
||||
return;
|
||||
}
|
||||
|
||||
total = strlen("Profile\n\n") + strlen(meta_json) + 1U;
|
||||
view_text = (char *)malloc(total);
|
||||
if (!view_text) {
|
||||
free(meta_json);
|
||||
tuin_notice("Out of memory while preparing profile view.");
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(view_text, total, "Profile\n\n%s", meta_json);
|
||||
nt_pager_show_text("> Main Menu > Follows > Profile", view_text);
|
||||
|
||||
free(view_text);
|
||||
free(meta_json);
|
||||
}
|
||||
|
||||
void menu_follows(void) {
|
||||
static const TuiMenuItem ACTION_ITEMS[] = {
|
||||
{NT_HK("R", "efresh selected profile"), 'r'},
|
||||
{NT_HK("A", "dd follow"), 'a'},
|
||||
{NT_HK("D", "elete selected follow"), 'd'},
|
||||
{NT_HK("P", "ost changes and exit"), 'p'},
|
||||
{"E" NT_HK("x", "it without saving"), 'x'},
|
||||
};
|
||||
|
||||
cJSON *working = NULL;
|
||||
cJSON *tags = NULL;
|
||||
char input[256];
|
||||
TuiMenuState action_state = {0};
|
||||
int selected_tag_index = -1;
|
||||
|
||||
working = g_state.kind3_json ? cJSON_Parse(g_state.kind3_json) : NULL;
|
||||
if (!working) {
|
||||
@@ -51,54 +170,47 @@ void menu_follows(void) {
|
||||
}
|
||||
|
||||
while (1) {
|
||||
int i;
|
||||
int n;
|
||||
int *row_indices = NULL;
|
||||
int row_count = follows_collect_rows(tags, &row_indices);
|
||||
follows_table_ctx_t ctx;
|
||||
TuiColumn cols[] = {
|
||||
{"#", 4, 1},
|
||||
{"npub", 0, 0},
|
||||
{"name", 24, 0},
|
||||
};
|
||||
TuiTable table;
|
||||
TuiFrame frame = nt_frame("> Main Menu > Follows");
|
||||
TuiStatus status = nt_status();
|
||||
TuiMenu menu = {ACTION_ITEMS, (int)(sizeof(ACTION_ITEMS) / sizeof(ACTION_ITEMS[0]))};
|
||||
int selected_row;
|
||||
int action;
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("FOLLOWS");
|
||||
tui_print("\n\n");
|
||||
ctx.tags = tags;
|
||||
ctx.tag_indices = row_indices;
|
||||
ctx.count = row_count;
|
||||
|
||||
n = cJSON_GetArraySize(tags);
|
||||
for (i = 0; i < n; i++) {
|
||||
cJSON *tag = cJSON_GetArrayItem(tags, i);
|
||||
cJSON *t0 = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON *t1 = cJSON_GetArrayItem(tag, 1);
|
||||
cJSON *t3 = cJSON_GetArrayItem(tag, 3);
|
||||
char npub[128] = {0};
|
||||
table.columns = cols;
|
||||
table.column_count = (int)(sizeof(cols) / sizeof(cols[0]));
|
||||
table.row_count = row_count;
|
||||
table.user_data = &ctx;
|
||||
table.get_cell = follows_table_get_cell;
|
||||
table.is_default = NULL;
|
||||
table.prefix_len = NULL;
|
||||
|
||||
if (!cJSON_IsArray(tag) || !cJSON_IsString(t0) || strcmp(t0->valuestring, "p") != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cJSON_IsString(t1) && t1->valuestring) {
|
||||
unsigned char pub[32];
|
||||
if (strlen(t1->valuestring) == 64 && nostr_hex_to_bytes(t1->valuestring, pub, 32) == 0) {
|
||||
if (nostr_key_to_bech32(pub, "npub", npub) != 0) {
|
||||
snprintf(npub, sizeof(npub), "%s", t1->valuestring);
|
||||
}
|
||||
} else {
|
||||
snprintf(npub, sizeof(npub), "%s", t1->valuestring);
|
||||
}
|
||||
}
|
||||
|
||||
tui_print("[%2d] %s %s",
|
||||
i + 1,
|
||||
npub,
|
||||
(cJSON_IsString(t3) && t3->valuestring) ? t3->valuestring : "");
|
||||
selected_row = tuin_table_run(&frame, &table, &status);
|
||||
if (selected_row >= 0 && selected_row < row_count) {
|
||||
selected_tag_index = row_indices[selected_row];
|
||||
}
|
||||
|
||||
tui_print("\n\n");
|
||||
tui_print("P^_r^:ofile");
|
||||
tui_print("^_A^:dd follow");
|
||||
tui_print("^_D^:elete follow");
|
||||
tui_print("^_P^:ost changes and exit.");
|
||||
tui_print("E^_x^:it without saving");
|
||||
free(row_indices);
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
action = tuin_menu_run(&frame, &menu, &status, &action_state);
|
||||
if (action < 0 || action == 4) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (input[0] == 'a' || input[0] == 'A') {
|
||||
if (action == 1) {
|
||||
char who[256];
|
||||
char yn[32];
|
||||
char follow_hex[65] = {0};
|
||||
char filter[256];
|
||||
char *follow_kind0 = NULL;
|
||||
@@ -107,30 +219,28 @@ void menu_follows(void) {
|
||||
cJSON *meta;
|
||||
cJSON *name;
|
||||
|
||||
tui_get_line("npub to add >", who, (int)sizeof(who));
|
||||
if (who[0] == '\0') {
|
||||
who[0] = '\0';
|
||||
if (tuin_prompt("npub to add >", "", who, sizeof(who)) != 0 || who[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strncmp(who, "npub", 4) == 0) {
|
||||
unsigned char pub[32];
|
||||
if (nostr_decode_npub(who, pub) != 0) {
|
||||
tui_print("Invalid npub.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
tuin_notice("Invalid npub.");
|
||||
continue;
|
||||
}
|
||||
nostr_bytes_to_hex(pub, 32, follow_hex);
|
||||
} else if (strlen(who) == 64) {
|
||||
unsigned char pub[32];
|
||||
if (nostr_hex_to_bytes(who, pub, 32) != 0) {
|
||||
tui_print("Invalid hex pubkey.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
tuin_notice("Invalid hex pubkey.");
|
||||
continue;
|
||||
}
|
||||
snprintf(follow_hex, sizeof(follow_hex), "%s", who);
|
||||
strncpy(follow_hex, who, sizeof(follow_hex) - 1U);
|
||||
follow_hex[sizeof(follow_hex) - 1U] = '\0';
|
||||
} else {
|
||||
tui_print("Enter npub or 64-char hex pubkey.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
tuin_notice("Enter npub or 64-char hex pubkey.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -140,8 +250,7 @@ void menu_follows(void) {
|
||||
g_state.read_relay_count > 0 ? g_state.read_relay_count : g_state.bootstrap_relay_count,
|
||||
7000,
|
||||
&follow_kind0) != 0 || !follow_kind0) {
|
||||
tui_print("Can't find user. Try adding more relays.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
tuin_notice("Can't find user. Try adding more relays.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -158,12 +267,9 @@ void menu_follows(void) {
|
||||
}
|
||||
}
|
||||
|
||||
tui_get_line((cJSON_IsString(name) && name->valuestring)
|
||||
? "Add follow [y/n] >"
|
||||
: "Add this follow [y/n] >",
|
||||
yn,
|
||||
(int)sizeof(yn));
|
||||
if (yn[0] == 'y' || yn[0] == 'Y') {
|
||||
if (tuin_confirm((cJSON_IsString(name) && name->valuestring)
|
||||
? "Add follow?"
|
||||
: "Add this follow?") == 1) {
|
||||
cJSON *tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(tag, cJSON_CreateString("p"));
|
||||
cJSON_AddItemToArray(tag, cJSON_CreateString(follow_hex));
|
||||
@@ -175,24 +281,17 @@ void menu_follows(void) {
|
||||
cJSON_Delete(meta);
|
||||
cJSON_Delete(ev);
|
||||
free(follow_kind0);
|
||||
} else if (input[0] == 'd' || input[0] == 'D') {
|
||||
char numbuf[32];
|
||||
char yn[32];
|
||||
int idx;
|
||||
|
||||
tui_get_line("# to delete >", numbuf, (int)sizeof(numbuf));
|
||||
idx = atoi(numbuf) - 1;
|
||||
if (idx < 0 || idx >= cJSON_GetArraySize(tags)) {
|
||||
} else if (action == 2) {
|
||||
if (selected_tag_index < 0 || selected_tag_index >= cJSON_GetArraySize(tags)) {
|
||||
tuin_notice("Select a follow row first.");
|
||||
continue;
|
||||
}
|
||||
|
||||
tui_get_line("Delete follow [y/n] >", yn, (int)sizeof(yn));
|
||||
if (yn[0] == 'y' || yn[0] == 'Y') {
|
||||
cJSON_DeleteItemFromArray(tags, idx);
|
||||
if (tuin_confirm("Delete follow?") == 1) {
|
||||
cJSON_DeleteItemFromArray(tags, selected_tag_index);
|
||||
selected_tag_index = -1;
|
||||
}
|
||||
} else if (input[0] == 'r' || input[0] == 'R') {
|
||||
char numbuf[32];
|
||||
int idx;
|
||||
} else if (action == 0) {
|
||||
cJSON *tag;
|
||||
cJSON *pub;
|
||||
char filter[256];
|
||||
@@ -201,13 +300,12 @@ void menu_follows(void) {
|
||||
cJSON *content;
|
||||
cJSON *meta;
|
||||
|
||||
tui_get_line("#>", numbuf, (int)sizeof(numbuf));
|
||||
idx = atoi(numbuf) - 1;
|
||||
if (idx < 0 || idx >= cJSON_GetArraySize(tags)) {
|
||||
if (selected_tag_index < 0 || selected_tag_index >= cJSON_GetArraySize(tags)) {
|
||||
tuin_notice("Select a follow row first.");
|
||||
continue;
|
||||
}
|
||||
|
||||
tag = cJSON_GetArrayItem(tags, idx);
|
||||
tag = cJSON_GetArrayItem(tags, selected_tag_index);
|
||||
pub = cJSON_GetArrayItem(tag, 1);
|
||||
if (!cJSON_IsString(pub) || !pub->valuestring) {
|
||||
continue;
|
||||
@@ -219,8 +317,7 @@ void menu_follows(void) {
|
||||
g_state.read_relay_count > 0 ? g_state.read_relay_count : g_state.bootstrap_relay_count,
|
||||
7000,
|
||||
&follow_kind0) != 0 || !follow_kind0) {
|
||||
tui_print("Unable to fetch profile.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
tuin_notice("Unable to fetch profile.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -233,35 +330,23 @@ void menu_follows(void) {
|
||||
}
|
||||
}
|
||||
|
||||
tui_print("PROFILE");
|
||||
if (meta) {
|
||||
cJSON *it = NULL;
|
||||
cJSON_ArrayForEach(it, meta) {
|
||||
if (cJSON_IsString(it) && it->valuestring) {
|
||||
tui_print("%s: %s", it->string ? it->string : "field", it->valuestring);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tui_print("No metadata content.");
|
||||
}
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
follows_show_profile(meta);
|
||||
cJSON_Delete(meta);
|
||||
cJSON_Delete(ev);
|
||||
free(follow_kind0);
|
||||
} else if (input[0] == 'p' || input[0] == 'P') {
|
||||
} else if (action == 3) {
|
||||
int posted = follows_publish_kind3(working);
|
||||
char *new_json = cJSON_PrintUnformatted(working);
|
||||
if (new_json) {
|
||||
free(g_state.kind3_json);
|
||||
g_state.kind3_json = new_json;
|
||||
}
|
||||
|
||||
if (posted < 0) {
|
||||
tui_print("Follows publish failed.");
|
||||
tuin_notice("Follows publish failed.");
|
||||
} else {
|
||||
nt_log("Follows published.");
|
||||
}
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
break;
|
||||
} else if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
361
src/menu_live.c
361
src/menu_live.c
@@ -4,13 +4,23 @@
|
||||
|
||||
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
||||
|
||||
#include <ncurses.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
typedef struct {
|
||||
int term_width;
|
||||
} live_feed_ctx_t;
|
||||
char *id;
|
||||
long long created_at;
|
||||
char *pubkey;
|
||||
char *content;
|
||||
} live_event_t;
|
||||
|
||||
typedef struct {
|
||||
live_event_t *items;
|
||||
int count;
|
||||
} live_buffer_t;
|
||||
|
||||
static char *live_strdup(const char *s) {
|
||||
size_t n;
|
||||
@@ -60,7 +70,7 @@ static void live_preview_text(const char *text, int max_len, char *buf, size_t b
|
||||
buf[0] = '\0';
|
||||
|
||||
if (!text || text[0] == '\0' || max_len <= 0) {
|
||||
snprintf(buf, buf_size, "");
|
||||
buf[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,49 +94,6 @@ static void live_preview_text(const char *text, int max_len, char *buf, size_t b
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -217,54 +184,264 @@ static void live_free_authors(char **authors, int count) {
|
||||
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];
|
||||
static void live_free_buffer(live_buffer_t *buf) {
|
||||
int i;
|
||||
|
||||
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);
|
||||
if (!buf || !buf->items) {
|
||||
return;
|
||||
}
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
|
||||
for (i = 0; i < buf->count; i++) {
|
||||
free(buf->items[i].id);
|
||||
free(buf->items[i].pubkey);
|
||||
free(buf->items[i].content);
|
||||
}
|
||||
free(buf->items);
|
||||
buf->items = NULL;
|
||||
buf->count = 0;
|
||||
}
|
||||
|
||||
void menu_live(void) {
|
||||
char input[16];
|
||||
static int live_event_exists(const live_buffer_t *buf, const char *id) {
|
||||
int i;
|
||||
|
||||
while (1) {
|
||||
tui_clear_screen();
|
||||
tui_print("LIVE FEEDS\n");
|
||||
tui_print("1 - Follows feed");
|
||||
tui_print("2 - Mentions");
|
||||
tui_print("3 - Firehose");
|
||||
tui_print("x - Back");
|
||||
if (!buf || !id) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
for (i = 0; i < buf->count; i++) {
|
||||
if (buf->items[i].id && strcmp(buf->items[i].id, id) == 0) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
|
||||
static int live_cmp_desc_created(const void *a, const void *b) {
|
||||
const live_event_t *ia = (const live_event_t *)a;
|
||||
const live_event_t *ib = (const live_event_t *)b;
|
||||
|
||||
if (ia->created_at < ib->created_at) {
|
||||
return 1;
|
||||
}
|
||||
if (ia->created_at > ib->created_at) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void live_append_event_json(live_buffer_t *buf, const char *event_json) {
|
||||
cJSON *ev;
|
||||
cJSON *id_item;
|
||||
cJSON *created_item;
|
||||
cJSON *pubkey_item;
|
||||
cJSON *content_item;
|
||||
live_event_t it;
|
||||
live_event_t *next;
|
||||
|
||||
if (!buf || !event_json) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev = cJSON_Parse(event_json);
|
||||
if (!ev) {
|
||||
return;
|
||||
}
|
||||
|
||||
id_item = cJSON_GetObjectItemCaseSensitive(ev, "id");
|
||||
created_item = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
|
||||
pubkey_item = cJSON_GetObjectItemCaseSensitive(ev, "pubkey");
|
||||
content_item = cJSON_GetObjectItemCaseSensitive(ev, "content");
|
||||
|
||||
if (!cJSON_IsString(id_item) || !id_item->valuestring ||
|
||||
!cJSON_IsNumber(created_item) ||
|
||||
!cJSON_IsString(pubkey_item) || !pubkey_item->valuestring) {
|
||||
cJSON_Delete(ev);
|
||||
return;
|
||||
}
|
||||
|
||||
if (live_event_exists(buf, id_item->valuestring)) {
|
||||
cJSON_Delete(ev);
|
||||
return;
|
||||
}
|
||||
|
||||
memset(&it, 0, sizeof(it));
|
||||
it.id = live_strdup(id_item->valuestring);
|
||||
it.created_at = (long long)created_item->valuedouble;
|
||||
it.pubkey = live_strdup(pubkey_item->valuestring);
|
||||
it.content = (cJSON_IsString(content_item) && content_item->valuestring)
|
||||
? live_strdup(content_item->valuestring)
|
||||
: live_strdup("");
|
||||
|
||||
if (!it.id || !it.pubkey || !it.content) {
|
||||
free(it.id);
|
||||
free(it.pubkey);
|
||||
free(it.content);
|
||||
cJSON_Delete(ev);
|
||||
return;
|
||||
}
|
||||
|
||||
next = (live_event_t *)realloc(buf->items, (size_t)(buf->count + 1) * sizeof(live_event_t));
|
||||
if (!next) {
|
||||
free(it.id);
|
||||
free(it.pubkey);
|
||||
free(it.content);
|
||||
cJSON_Delete(ev);
|
||||
return;
|
||||
}
|
||||
|
||||
buf->items = next;
|
||||
buf->items[buf->count++] = it;
|
||||
|
||||
if (buf->count > 1) {
|
||||
qsort(buf->items, (size_t)buf->count, sizeof(live_event_t), live_cmp_desc_created);
|
||||
}
|
||||
|
||||
cJSON_Delete(ev);
|
||||
}
|
||||
|
||||
static void live_poll_once(const char *filter_json, live_buffer_t *buf) {
|
||||
const char **read_relays;
|
||||
int relay_count = 0;
|
||||
char **events = NULL;
|
||||
int event_count = 0;
|
||||
int i;
|
||||
|
||||
if (!filter_json || !buf) {
|
||||
return;
|
||||
}
|
||||
|
||||
read_relays = state_get_read_relays(&relay_count);
|
||||
if (nt_query_sync(filter_json, read_relays, relay_count, 1200, &events, &event_count) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < event_count; i++) {
|
||||
live_append_event_json(buf, events[i]);
|
||||
free(events[i]);
|
||||
}
|
||||
free(events);
|
||||
}
|
||||
|
||||
static void menu_live_run_filter(const char *title, const char *filter_json) {
|
||||
TuiFrame frame = nt_frame("> Main Menu > Live Feeds > Stream");
|
||||
TuiStatus status;
|
||||
WINDOW *body;
|
||||
live_buffer_t buffer;
|
||||
int top = 0;
|
||||
int ticks = 0;
|
||||
|
||||
if (!filter_json) {
|
||||
return;
|
||||
}
|
||||
|
||||
memset(&buffer, 0, sizeof(buffer));
|
||||
status.text = "Esc exit • ↑↓ PgUp/PgDn scroll";
|
||||
|
||||
body = (WINDOW *)tuin_body_window();
|
||||
wtimeout(body, 100);
|
||||
|
||||
for (;;) {
|
||||
int k;
|
||||
int h;
|
||||
int w;
|
||||
int i;
|
||||
|
||||
if ((ticks % 8) == 0) {
|
||||
live_poll_once(filter_json, &buffer);
|
||||
}
|
||||
ticks++;
|
||||
|
||||
tuin_render_header(&frame);
|
||||
werase(body);
|
||||
getmaxyx(body, h, w);
|
||||
|
||||
if (title && title[0] != '\0') {
|
||||
mvwprintw(body, 0, 0, "%.*s", (w > 1) ? (w - 1) : 0, title);
|
||||
}
|
||||
|
||||
if (buffer.count == 0) {
|
||||
mvwprintw(body, 2, 0, "%.*s", (w > 1) ? (w - 1) : 0, "Waiting for events...");
|
||||
} else {
|
||||
int row = 2;
|
||||
int max_rows = h - row;
|
||||
for (i = 0; i < max_rows && (top + i) < buffer.count; i++) {
|
||||
char pub_short[32];
|
||||
char preview[1024];
|
||||
int max_preview = (w > 24) ? (w - 22) : 8;
|
||||
live_event_t *ev = &buffer.items[top + i];
|
||||
live_preview_text(ev->content, max_preview, preview, sizeof(preview));
|
||||
mvwprintw(body,
|
||||
row + i,
|
||||
0,
|
||||
"%.*s: %.*s",
|
||||
14,
|
||||
live_shorten_hex(ev->pubkey, pub_short, sizeof(pub_short)),
|
||||
(w > 17) ? (w - 17) : 0,
|
||||
preview);
|
||||
}
|
||||
}
|
||||
|
||||
wnoutrefresh(body);
|
||||
tuin_render_footer(&status);
|
||||
doupdate();
|
||||
|
||||
k = wgetch(body);
|
||||
if (k == ERR) {
|
||||
continue;
|
||||
}
|
||||
if (k == KEY_RESIZE) {
|
||||
continue;
|
||||
}
|
||||
if (tuin_is_escape_key(k)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (input[0] == '1') {
|
||||
getmaxyx(body, h, w);
|
||||
if ((k == KEY_DOWN || k == 'j') && top + 1 < buffer.count) {
|
||||
top++;
|
||||
} else if ((k == KEY_UP || k == 'k') && top > 0) {
|
||||
top--;
|
||||
} else if (k == KEY_NPAGE) {
|
||||
top += (h > 3) ? (h - 3) : 1;
|
||||
if (top >= buffer.count) {
|
||||
top = (buffer.count > 0) ? (buffer.count - 1) : 0;
|
||||
}
|
||||
} else if (k == KEY_PPAGE) {
|
||||
top -= (h > 3) ? (h - 3) : 1;
|
||||
if (top < 0) {
|
||||
top = 0;
|
||||
}
|
||||
} else if (k == KEY_HOME) {
|
||||
top = 0;
|
||||
} else if (k == KEY_END) {
|
||||
top = (buffer.count > (h - 3)) ? (buffer.count - (h - 3)) : 0;
|
||||
}
|
||||
}
|
||||
|
||||
wtimeout(body, -1);
|
||||
live_free_buffer(&buffer);
|
||||
}
|
||||
|
||||
void menu_live(void) {
|
||||
static const TuiMenuItem LIVE_ITEMS[] = {
|
||||
{NT_HK("F", "ollows feed"), 'f'},
|
||||
{NT_HK("M", "entions"), 'm'},
|
||||
{NT_HK("G", "lobal firehose"), 'g'},
|
||||
{NT_HK("X", "Exit"), 'x'},
|
||||
};
|
||||
TuiMenuState menu_state = {0};
|
||||
|
||||
while (1) {
|
||||
TuiFrame frame = nt_frame("> Main Menu > Live Feeds");
|
||||
TuiStatus status = nt_status();
|
||||
TuiMenu menu = {LIVE_ITEMS, (int)(sizeof(LIVE_ITEMS) / sizeof(LIVE_ITEMS[0]))};
|
||||
int idx = tuin_menu_run(&frame, &menu, &status, &menu_state);
|
||||
|
||||
if (idx < 0 || idx == 3) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (idx == 0) {
|
||||
char **authors = NULL;
|
||||
int authors_count = 0;
|
||||
cJSON *filter_arr = NULL;
|
||||
@@ -275,8 +452,7 @@ void menu_live(void) {
|
||||
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));
|
||||
tuin_notice("No follows found in kind3 list.");
|
||||
live_free_authors(authors, authors_count);
|
||||
continue;
|
||||
}
|
||||
@@ -291,8 +467,7 @@ void menu_live(void) {
|
||||
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));
|
||||
tuin_notice("Failed to build follows filter.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -306,23 +481,29 @@ void menu_live(void) {
|
||||
|
||||
filter_str = cJSON_PrintUnformatted(filter_arr);
|
||||
if (filter_str) {
|
||||
nt_log("Connected to follows live feed.");
|
||||
menu_live_run_filter("FOLLOWS FEED", filter_str);
|
||||
nt_log("Disconnected from follows live feed.");
|
||||
free(filter_str);
|
||||
}
|
||||
|
||||
cJSON_Delete(filter_arr);
|
||||
live_free_authors(authors, authors_count);
|
||||
} else if (input[0] == '2') {
|
||||
} else if (idx == 1) {
|
||||
char filter[512];
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"[{\"kinds\":[1],\"#p\":[\"%s\"],\"limit\":500}]",
|
||||
g_state.npub_hex);
|
||||
nt_log("Connected to mentions live feed.");
|
||||
menu_live_run_filter("MENTIONS", filter);
|
||||
} else if (input[0] == '3') {
|
||||
nt_log("Disconnected from mentions live feed.");
|
||||
} else if (idx == 2) {
|
||||
char filter[128];
|
||||
snprintf(filter, sizeof(filter), "[{\"kinds\":[1],\"limit\":500}]");
|
||||
nt_log("Connected to global live feed.");
|
||||
menu_live_run_filter("FIREHOSE", filter);
|
||||
nt_log("Disconnected from global live feed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
494
src/menu_login.c
494
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"
|
||||
@@ -10,13 +13,53 @@
|
||||
#include "../resources/nostr_core_lib/nostr_core/utils.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <sys/random.h>
|
||||
|
||||
#define NT_DEV_SEED_PHRASE "cube dirt movie learn depth axis ball view aunt electric finish release"
|
||||
|
||||
static void menu_render_body(const char *breadcrumb) {
|
||||
TuiFrame frame = nt_frame(breadcrumb);
|
||||
TuiStatus status = nt_status();
|
||||
tuin_render_header(&frame);
|
||||
nt_print_reset();
|
||||
tuin_render_footer(&status);
|
||||
}
|
||||
|
||||
static int menu_prompt(const char *breadcrumb,
|
||||
const char *prompt,
|
||||
const char *default_value,
|
||||
char *out,
|
||||
size_t out_size) {
|
||||
menu_render_body(breadcrumb);
|
||||
if (tuin_prompt(prompt, default_value ? default_value : "", out, out_size) != 0) {
|
||||
if (out && out_size > 0) {
|
||||
out[0] = '\0';
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void menu_noticef(const char *fmt, ...) {
|
||||
char msg[1024];
|
||||
va_list ap;
|
||||
|
||||
if (!fmt) {
|
||||
return;
|
||||
}
|
||||
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(msg, sizeof(msg), fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
tuin_notice(msg);
|
||||
}
|
||||
|
||||
static int menu_load_test_mnemonic(char *out, int out_size) {
|
||||
FILE *fp;
|
||||
|
||||
@@ -99,28 +142,32 @@ static void menu_add_new_user(void) {
|
||||
|
||||
username[0] = '\0';
|
||||
do {
|
||||
tui_get_line("Enter a username for this account (required) >", username, (int)sizeof(username));
|
||||
(void)menu_prompt("> Login > New Account > Profile Setup",
|
||||
"Enter a username for this account (required)",
|
||||
"",
|
||||
username,
|
||||
sizeof(username));
|
||||
} while (username[0] == '\0');
|
||||
|
||||
cJSON_AddStringToObject(meta, "name", username);
|
||||
cJSON_AddStringToObject(meta, "displayname", username);
|
||||
|
||||
tui_get_line("Enter \"about\" info: >", buf, (int)sizeof(buf));
|
||||
(void)menu_prompt("> Login > New Account > Profile Setup", "Enter \"about\" info:", "", buf, sizeof(buf));
|
||||
cJSON_AddStringToObject(meta, "about", buf);
|
||||
|
||||
tui_get_line("Enter profile picture url: >", buf, (int)sizeof(buf));
|
||||
(void)menu_prompt("> Login > New Account > Profile Setup", "Enter profile picture url:", "", buf, sizeof(buf));
|
||||
cJSON_AddStringToObject(meta, "picture", buf);
|
||||
|
||||
tui_get_line("Enter banner picture url: >", buf, (int)sizeof(buf));
|
||||
(void)menu_prompt("> Login > New Account > Profile Setup", "Enter banner picture url:", "", buf, sizeof(buf));
|
||||
cJSON_AddStringToObject(meta, "banner", buf);
|
||||
|
||||
tui_get_line("Enter website url: >", buf, (int)sizeof(buf));
|
||||
(void)menu_prompt("> Login > New Account > Profile Setup", "Enter website url:", "", buf, sizeof(buf));
|
||||
cJSON_AddStringToObject(meta, "website", buf);
|
||||
|
||||
tui_get_line("Enter lud16: >", buf, (int)sizeof(buf));
|
||||
(void)menu_prompt("> Login > New Account > Profile Setup", "Enter lud16:", "", buf, sizeof(buf));
|
||||
cJSON_AddStringToObject(meta, "lud16", buf);
|
||||
|
||||
tui_get_line("Enter nip05: >", buf, (int)sizeof(buf));
|
||||
(void)menu_prompt("> Login > New Account > Profile Setup", "Enter nip05:", "", buf, sizeof(buf));
|
||||
cJSON_AddStringToObject(meta, "nip05", buf);
|
||||
|
||||
free(g_state.kind0_json);
|
||||
@@ -149,6 +196,21 @@ static void menu_add_new_user(void) {
|
||||
}
|
||||
|
||||
void menu_login(void) {
|
||||
static const TuiMenuItem LOGIN_ITEMS[] = {
|
||||
{NT_HK("P", "rivate key"), 'p'},
|
||||
{NT_HK("M", "nemonic"), 'm'},
|
||||
{"N_" NT_HK("s", "igner"), 's'},
|
||||
{NT_HK("N", "ew account"), 'n'},
|
||||
{NT_HK("Q", "uit"), 'q'},
|
||||
};
|
||||
static const TuiMenuItem SIGNER_TRANSPORT_ITEMS[] = {
|
||||
{"URL signer (FIPS/web address)", 'u'},
|
||||
{"Signer qrexec transport", 's'},
|
||||
{"Launch n_signer (stub)", 'l'},
|
||||
{"Back", 'b'},
|
||||
};
|
||||
|
||||
TuiMenuState login_state = {0};
|
||||
char input[512];
|
||||
char pending_test_mnemonic[512] = {0};
|
||||
int pending_test_mnemonic_ready = 0;
|
||||
@@ -156,57 +218,408 @@ void menu_login(void) {
|
||||
while (1) {
|
||||
unsigned char priv[32];
|
||||
unsigned char pub[32];
|
||||
int login_idx;
|
||||
TuiFrame frame = nt_frame("> Login");
|
||||
TuiMenu menu = {LOGIN_ITEMS, 5};
|
||||
TuiStatus status = nt_status();
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("LOGIN");
|
||||
login_idx = tuin_menu_run(&frame, &menu, &status, &login_state);
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
|
||||
if (strcmp(input, "q") == 0 || strcmp(input, "x") == 0) {
|
||||
tui_print("\nHasta luego.\n");
|
||||
if (login_idx < 0 || login_idx == 4) {
|
||||
nt_log("Hasta luego.");
|
||||
signer_shutdown();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
input[0] = '\0';
|
||||
if (login_idx == 0) {
|
||||
(void)menu_prompt("> Login > Private Key", "Private key (nsec or 64-hex)", "", input, sizeof(input));
|
||||
if (input[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
} else if (login_idx == 1) {
|
||||
(void)menu_prompt("> Login > Mnemonic", "Seed phrase (12 words)", "", input, sizeof(input));
|
||||
if (input[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
} else if (login_idx == 2) {
|
||||
snprintf(input, sizeof(input), "%s", "s");
|
||||
} else if (login_idx == 3) {
|
||||
snprintf(input, sizeof(input), "%s", "n");
|
||||
}
|
||||
|
||||
if (strcmp(input, "s") == 0 || strcmp(input, "S") == 0) {
|
||||
char **names = NULL;
|
||||
int name_count = 0;
|
||||
|
||||
if (nsigner_list(&names, &name_count) == 0 && name_count > 0) {
|
||||
char pubkey_hex[65] = {0};
|
||||
int selected = 0;
|
||||
nt_nsigner_selector_t selector = nsigner_selector_default();
|
||||
char idxbuf[32] = {0};
|
||||
int nostr_index = 0;
|
||||
int choose_transport = 0;
|
||||
int idx;
|
||||
int local_choice;
|
||||
TuiMenuState local_state = {0};
|
||||
TuiMenuItem *local_items = NULL;
|
||||
char (*labels)[256] = NULL;
|
||||
|
||||
local_items = (TuiMenuItem *)calloc((size_t)(name_count + 2), sizeof(TuiMenuItem));
|
||||
labels = (char(*)[256])calloc((size_t)(name_count + 2), sizeof(*labels));
|
||||
|
||||
if (!local_items || !labels) {
|
||||
for (idx = 0; idx < name_count; idx++) {
|
||||
free(names[idx]);
|
||||
}
|
||||
free(names);
|
||||
free(local_items);
|
||||
free(labels);
|
||||
menu_noticef("Out of memory while building local n_signer menu.");
|
||||
continue;
|
||||
}
|
||||
|
||||
for (idx = 0; idx < name_count; idx++) {
|
||||
snprintf(labels[idx], sizeof(labels[idx]), "@%s", names[idx]);
|
||||
local_items[idx].label = labels[idx];
|
||||
local_items[idx].shortcut = 0;
|
||||
}
|
||||
snprintf(labels[name_count], sizeof(labels[name_count]), "Transport options");
|
||||
local_items[name_count].label = labels[name_count];
|
||||
local_items[name_count].shortcut = 't';
|
||||
|
||||
snprintf(labels[name_count + 1], sizeof(labels[name_count + 1]), "Back");
|
||||
local_items[name_count + 1].label = labels[name_count + 1];
|
||||
local_items[name_count + 1].shortcut = 'b';
|
||||
|
||||
{
|
||||
TuiFrame local_frame = nt_frame("> Login > Signer Local");
|
||||
TuiMenu local_menu = {local_items, name_count + 2};
|
||||
TuiStatus local_status = nt_status();
|
||||
local_choice = tuin_menu_run(&local_frame, &local_menu, &local_status, &local_state);
|
||||
}
|
||||
|
||||
if (local_choice < 0 || local_choice == name_count + 1) {
|
||||
for (idx = 0; idx < name_count; idx++) {
|
||||
free(names[idx]);
|
||||
}
|
||||
free(names);
|
||||
free(local_items);
|
||||
free(labels);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (local_choice == name_count) {
|
||||
choose_transport = 1;
|
||||
} else {
|
||||
selected = local_choice;
|
||||
(void)menu_prompt("> Login > Signer Local", "Seed phrase index (0)", "", idxbuf, sizeof(idxbuf));
|
||||
}
|
||||
|
||||
if (idxbuf[0] != '\0') {
|
||||
nostr_index = atoi(idxbuf);
|
||||
if (nostr_index < 0) {
|
||||
nostr_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!choose_transport) {
|
||||
selector.has_nostr_index = 1;
|
||||
selector.nostr_index = nostr_index;
|
||||
selector.role[0] = '\0';
|
||||
|
||||
menu_render_body("> Login > Signer Local");
|
||||
nt_log("Waiting for n_signer approval...");
|
||||
|
||||
if (nsigner_get_public_key(names[selected], &selector, pubkey_hex) != 0) {
|
||||
for (idx = 0; idx < name_count; idx++) {
|
||||
free(names[idx]);
|
||||
}
|
||||
free(names);
|
||||
free(local_items);
|
||||
free(labels);
|
||||
tuin_notice("Failed to get public key from n_signer (denied or error).");
|
||||
continue;
|
||||
}
|
||||
|
||||
signer_init_nsigner(names[selected], &selector);
|
||||
|
||||
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;
|
||||
|
||||
nt_log("Signed in via n_signer (@%s) npub: %s", names[selected], g_state.npub_bech32);
|
||||
|
||||
for (idx = 0; idx < name_count; idx++) {
|
||||
free(names[idx]);
|
||||
}
|
||||
free(names);
|
||||
free(local_items);
|
||||
free(labels);
|
||||
|
||||
(void)state_load_user_info();
|
||||
|
||||
if (g_state.kind0_json && strcmp(g_state.kind0_json, "{}") == 0) {
|
||||
nt_log("No profile found on relays. Setting up new user...");
|
||||
menu_add_new_user();
|
||||
(void)state_load_user_info();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (idx = 0; idx < name_count; idx++) {
|
||||
free(names[idx]);
|
||||
}
|
||||
free(names);
|
||||
free(local_items);
|
||||
free(labels);
|
||||
}
|
||||
|
||||
{
|
||||
int transport_idx;
|
||||
TuiMenuState transport_state = {0};
|
||||
TuiFrame transport_frame = nt_frame("> Login > N_signer > Transport");
|
||||
TuiMenu transport_menu = {SIGNER_TRANSPORT_ITEMS, 4};
|
||||
TuiStatus transport_status = nt_status();
|
||||
|
||||
transport_idx = tuin_menu_run(&transport_frame, &transport_menu, &transport_status, &transport_state);
|
||||
|
||||
if (transport_idx == 0) {
|
||||
snprintf(input, sizeof(input), "%s", "u");
|
||||
} else if (transport_idx == 1) {
|
||||
snprintf(input, sizeof(input), "%s", "S");
|
||||
} else if (transport_idx == 2) {
|
||||
tuin_notice("Launch n_signer is not implemented yet.");
|
||||
continue;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
nt_nsigner_selector_t selector = nsigner_selector_default();
|
||||
char idxbuf[32];
|
||||
int nostr_index = 0;
|
||||
int session_fd = -1;
|
||||
nt_nsigner_auth_ctx_t auth = {0};
|
||||
|
||||
snprintf(endpoint_url, sizeof(endpoint_url), "%s", (saved_endpoint && saved_endpoint[0] != '\0') ? saved_endpoint : "");
|
||||
(void)menu_prompt("> Login > URL Signer",
|
||||
"Signer URL (http://<npub>.fips:8080)",
|
||||
endpoint_url,
|
||||
endpoint_url,
|
||||
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 {
|
||||
tuin_notice("No URL provided.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
(void)menu_prompt("> Login > URL Signer", "Seed phrase index (0)", "", idxbuf, sizeof(idxbuf));
|
||||
if (idxbuf[0] != '\0') {
|
||||
nostr_index = atoi(idxbuf);
|
||||
if (nostr_index < 0) {
|
||||
nostr_index = 0;
|
||||
}
|
||||
}
|
||||
selector.has_nostr_index = 1;
|
||||
selector.nostr_index = nostr_index;
|
||||
selector.role[0] = '\0';
|
||||
|
||||
menu_render_body("> Login > URL Signer");
|
||||
nt_log("Connecting to n_signer URL: %s", endpoint_url);
|
||||
nt_log("Waiting for n_signer approval...");
|
||||
|
||||
auth.enabled = 1;
|
||||
if (getrandom(auth.privkey, sizeof(auth.privkey), 0) != (ssize_t)sizeof(auth.privkey)) {
|
||||
tuin_notice("Failed to generate URL auth key.");
|
||||
continue;
|
||||
}
|
||||
snprintf(auth.label, sizeof(auth.label), "%s", "nostr_terminal");
|
||||
|
||||
session_fd = nsigner_session_open_url(endpoint_url);
|
||||
if (session_fd < 0) {
|
||||
tuin_notice("Failed to open persistent connection to n_signer URL.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nsigner_session_get_public_key(&session_fd, endpoint_url, &selector, &auth, pubkey_hex) != 0) {
|
||||
nsigner_session_close(session_fd);
|
||||
tuin_notice("Failed to get public key from n_signer URL (denied or error).");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (signer_init_nsigner_url_with_session_auth(endpoint_url, &selector, session_fd, &auth) != 0) {
|
||||
nsigner_session_close(session_fd);
|
||||
tuin_notice("Failed to initialize URL signer mode.");
|
||||
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;
|
||||
|
||||
nt_log("Signed in via n_signer URL (%s) npub: %s", endpoint_url, g_state.npub_bech32);
|
||||
|
||||
(void)state_load_user_info();
|
||||
|
||||
if (g_state.kind0_json && strcmp(g_state.kind0_json, "{}") == 0) {
|
||||
nt_log("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");
|
||||
nt_nsigner_selector_t selector = nsigner_selector_default();
|
||||
char idxbuf[32];
|
||||
int nostr_index = 0;
|
||||
|
||||
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");
|
||||
|
||||
(void)menu_prompt("> Login > Qrexec Signer",
|
||||
"Target signer qube (nsigner-vault)",
|
||||
target_qube,
|
||||
target_qube,
|
||||
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");
|
||||
}
|
||||
|
||||
(void)menu_prompt("> Login > Qrexec Signer",
|
||||
"Qrexec service (qubes.NsignerRpc)",
|
||||
service_name,
|
||||
service_name,
|
||||
sizeof(service_name));
|
||||
if (service_name[0] == '\0') {
|
||||
snprintf(service_name, sizeof(service_name), "%s", "qubes.NsignerRpc");
|
||||
}
|
||||
|
||||
(void)menu_prompt("> Login > Qrexec Signer", "Seed phrase index (0)", "", idxbuf, sizeof(idxbuf));
|
||||
if (idxbuf[0] != '\0') {
|
||||
nostr_index = atoi(idxbuf);
|
||||
if (nostr_index < 0) {
|
||||
nostr_index = 0;
|
||||
}
|
||||
}
|
||||
selector.has_nostr_index = 1;
|
||||
selector.nostr_index = nostr_index;
|
||||
selector.role[0] = '\0';
|
||||
|
||||
menu_render_body("> Login > Qrexec Signer");
|
||||
nt_log("Calling qrexec service %s in %s...", service_name, target_qube);
|
||||
nt_log("Waiting for n_signer approval...");
|
||||
|
||||
if (nsigner_qrexec_get_public_key(target_qube, service_name, &selector, pubkey_hex) != 0) {
|
||||
tuin_notice("Failed to get public key via qrexec (denied or error).");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (signer_init_nsigner_qrexec(target_qube, service_name, &selector) != 0) {
|
||||
tuin_notice("Failed to initialize qrexec signer mode.");
|
||||
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;
|
||||
|
||||
nt_log("Signed in via qrexec n_signer (%s:%s) npub: %s", target_qube, service_name, g_state.npub_bech32);
|
||||
|
||||
(void)state_load_user_info();
|
||||
|
||||
if (g_state.kind0_json && strcmp(g_state.kind0_json, "{}") == 0) {
|
||||
nt_log("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];
|
||||
int account = 0;
|
||||
|
||||
if (nostr_generate_mnemonic_and_keys(mnemonic, sizeof(mnemonic), 0, priv, pub) != 0) {
|
||||
tui_print("Failed to generate new seed phrase.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
tuin_notice("Failed to generate new seed phrase.");
|
||||
continue;
|
||||
}
|
||||
|
||||
tui_get_line("Enter seed phrase index (0):", idxbuf, (int)sizeof(idxbuf));
|
||||
(void)menu_prompt("> Login > New Account", "Enter seed phrase index (0):", "", idxbuf, sizeof(idxbuf));
|
||||
if (idxbuf[0] != '\0') {
|
||||
account = atoi(idxbuf);
|
||||
if (account < 0) {
|
||||
account = 0;
|
||||
}
|
||||
if (nostr_derive_keys_from_mnemonic(mnemonic, account, priv, pub) != 0) {
|
||||
tui_print("Failed to derive keys from generated mnemonic.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
tuin_notice("Failed to derive keys from generated mnemonic.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (menu_set_keys_from_private_bytes(priv, mnemonic) != 0) {
|
||||
tui_print("Could not initialize keys.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
tuin_notice("Could not initialize keys.");
|
||||
continue;
|
||||
}
|
||||
|
||||
tui_print("");
|
||||
tui_print("Jot this down!");
|
||||
tui_print("");
|
||||
tui_print("Seed phrase: %s", g_state.seed_phrase);
|
||||
tui_print("");
|
||||
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("");
|
||||
menu_render_body("> Login > New Account");
|
||||
nt_log("");
|
||||
nt_log("Jot this down!");
|
||||
nt_log("");
|
||||
nt_log("Seed phrase: %s", g_state.seed_phrase);
|
||||
nt_log("");
|
||||
nt_log("nsecHex: %s", g_state.nsec_hex);
|
||||
nt_log("nsec: %s", g_state.nsec_bech32);
|
||||
nt_log("npubHex: %s", g_state.npub_hex);
|
||||
nt_log("npub: %s", g_state.npub_bech32);
|
||||
nt_log("");
|
||||
|
||||
if (tuin_confirm("Continue with account setup?") != 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
menu_add_new_user();
|
||||
(void)state_load_user_info();
|
||||
@@ -219,7 +632,7 @@ void menu_login(void) {
|
||||
pending_test_mnemonic_ready = 0;
|
||||
} else if (menu_load_test_mnemonic(pending_test_mnemonic, (int)sizeof(pending_test_mnemonic)) == 0) {
|
||||
pending_test_mnemonic_ready = 1;
|
||||
tui_print("Loaded .test_mnemonic. Press Enter again to use it.");
|
||||
nt_log("Loaded .test_mnemonic. Trigger mnemonic/private flow to use it.");
|
||||
continue;
|
||||
} else {
|
||||
snprintf(input, sizeof(input), "%s", NT_DEV_SEED_PHRASE);
|
||||
@@ -243,8 +656,7 @@ void menu_login(void) {
|
||||
(void)state_load_user_info();
|
||||
return;
|
||||
}
|
||||
tui_print("Invalid nsecHex. Try again.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
tuin_notice("Invalid nsecHex. Try again.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -255,8 +667,7 @@ void menu_login(void) {
|
||||
(void)state_load_user_info();
|
||||
return;
|
||||
}
|
||||
tui_print("Invalid nsec. Try again.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
tuin_notice("Invalid nsec. Try again.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -277,12 +688,11 @@ void menu_login(void) {
|
||||
int account = 0;
|
||||
|
||||
if (nostr_bip39_mnemonic_validate(input) != 0) {
|
||||
tui_print("Invalid seed phrase. Try again.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
tuin_notice("Invalid seed phrase. Try again.");
|
||||
continue;
|
||||
}
|
||||
|
||||
tui_get_line("Enter seed phrase index (0) >", idxbuf, (int)sizeof(idxbuf));
|
||||
(void)menu_prompt("> Login > Mnemonic", "Enter seed phrase index (0)", "", idxbuf, sizeof(idxbuf));
|
||||
if (idxbuf[0] != '\0') {
|
||||
account = atoi(idxbuf);
|
||||
if (account < 0) {
|
||||
@@ -292,17 +702,17 @@ void menu_login(void) {
|
||||
|
||||
if (nostr_derive_keys_from_mnemonic(input, account, priv, pub) != 0 ||
|
||||
menu_set_keys_from_private_bytes(priv, input) != 0) {
|
||||
tui_print("Failed to derive keys from seed phrase.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
tuin_notice("Failed to derive keys from seed phrase.");
|
||||
continue;
|
||||
}
|
||||
|
||||
nt_log("Using seed phrase: %s", g_state.seed_phrase);
|
||||
|
||||
(void)state_load_user_info();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
tui_print("Invalid login input. Enter 12 words, nsec, 64-hex, [n], or [q].");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
tuin_notice("Invalid login input. Choose a menu command or enter valid key/mnemonic data.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ typedef struct {
|
||||
char *content;
|
||||
} notif_item_t;
|
||||
|
||||
typedef struct {
|
||||
const notif_item_t *items;
|
||||
int count;
|
||||
} notif_table_ctx_t;
|
||||
|
||||
static char *notif_strdup(const char *s) {
|
||||
size_t n;
|
||||
char *out;
|
||||
@@ -168,6 +173,40 @@ static int notif_cmp_desc_created(const void *a, const void *b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void notif_table_get_cell(int row, int col, char *out, size_t out_size, void *user_data) {
|
||||
const notif_table_ctx_t *ctx = (const notif_table_ctx_t *)user_data;
|
||||
const notif_item_t *it;
|
||||
char pub_short[32];
|
||||
|
||||
if (!out || out_size == 0U) {
|
||||
return;
|
||||
}
|
||||
out[0] = '\0';
|
||||
|
||||
if (!ctx || row < 0 || row >= ctx->count) {
|
||||
return;
|
||||
}
|
||||
|
||||
it = &ctx->items[row];
|
||||
|
||||
if (col == 0) {
|
||||
snprintf(out, out_size, "%s", (it->kind == 7) ? "♥ reaction" : "↩ reply");
|
||||
} else if (col == 1) {
|
||||
snprintf(out, out_size, "%s", notif_shorten_hex(it->pubkey, pub_short, sizeof(pub_short)));
|
||||
} else if (col == 2) {
|
||||
if (it->kind == 7) {
|
||||
char ev_short[32];
|
||||
snprintf(out,
|
||||
out_size,
|
||||
"%s",
|
||||
notif_shorten_hex(it->event_ref ? it->event_ref : "(unknown)", ev_short, sizeof(ev_short)));
|
||||
} else {
|
||||
char preview[160];
|
||||
snprintf(out, out_size, "%s", notif_preview_text(it->content, preview, sizeof(preview)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void menu_notifications(void) {
|
||||
const char **read_relays;
|
||||
int relay_count = 0;
|
||||
@@ -181,7 +220,16 @@ void menu_notifications(void) {
|
||||
int reactions = 0;
|
||||
int replies = 0;
|
||||
int i;
|
||||
char input[32];
|
||||
char status_text[128];
|
||||
TuiFrame frame = nt_frame("> Main Menu > Notifications");
|
||||
TuiColumn cols[] = {
|
||||
{"Type", 12, 0},
|
||||
{"From", 20, 0},
|
||||
{"Detail", 0, 0},
|
||||
};
|
||||
notif_table_ctx_t ctx;
|
||||
TuiTable table;
|
||||
TuiStatus status;
|
||||
|
||||
read_relays = state_get_read_relays(&relay_count);
|
||||
|
||||
@@ -194,13 +242,9 @@ void menu_notifications(void) {
|
||||
g_state.npub_hex,
|
||||
since);
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("NOTIFICATIONS\n");
|
||||
tui_print("Querying %d read relay(s)...", relay_count);
|
||||
|
||||
nt_log("Fetching notifications...");
|
||||
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));
|
||||
tuin_notice("Failed to query notifications (all relays failed).");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -271,39 +315,33 @@ void menu_notifications(void) {
|
||||
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);
|
||||
if (items_count == 0) {
|
||||
nt_log("No notifications found.");
|
||||
notif_free_items(items, items_count);
|
||||
return;
|
||||
}
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
snprintf(status_text, sizeof(status_text), "%d reactions, %d replies in last 3 days", reactions, replies);
|
||||
|
||||
ctx.items = items;
|
||||
ctx.count = items_count;
|
||||
|
||||
table.columns = cols;
|
||||
table.column_count = (int)(sizeof(cols) / sizeof(cols[0]));
|
||||
table.row_count = items_count;
|
||||
table.user_data = &ctx;
|
||||
table.get_cell = notif_table_get_cell;
|
||||
table.is_default = NULL;
|
||||
table.prefix_len = NULL;
|
||||
|
||||
status.text = status_text;
|
||||
|
||||
(void)tuin_table_run(&frame, &table, &status);
|
||||
|
||||
notif_free_items(items, items_count);
|
||||
}
|
||||
|
||||
443
src/menu_posts.c
443
src/menu_posts.c
@@ -3,11 +3,13 @@
|
||||
#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 <ncurses.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -25,6 +27,35 @@ typedef struct {
|
||||
char *dtag;
|
||||
} post_item_t;
|
||||
|
||||
typedef struct {
|
||||
const post_item_t *items;
|
||||
int count;
|
||||
} posts_table_ctx_t;
|
||||
|
||||
typedef struct {
|
||||
const char *initial;
|
||||
char *edited;
|
||||
} posts_editor_ctx_t;
|
||||
|
||||
static int posts_editor_spawn(void *user) {
|
||||
posts_editor_ctx_t *ctx = (posts_editor_ctx_t *)user;
|
||||
if (!ctx) {
|
||||
return -1;
|
||||
}
|
||||
ctx->edited = editor_launch(ctx->initial);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char *posts_launch_editor(const char *initial) {
|
||||
posts_editor_ctx_t ctx;
|
||||
ctx.initial = initial;
|
||||
ctx.edited = NULL;
|
||||
if (nt_run_external_editor(posts_editor_spawn, &ctx) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
return ctx.edited;
|
||||
}
|
||||
|
||||
static char *posts_strdup(const char *s) {
|
||||
size_t n;
|
||||
char *out;
|
||||
@@ -105,30 +136,9 @@ static char *posts_find_tag_value(cJSON *tags, const char *tag_name) {
|
||||
}
|
||||
|
||||
static char *posts_try_decrypt_nip04(const char *sender_pub_hex, const char *ciphertext) {
|
||||
unsigned char priv[32];
|
||||
unsigned char sender_pub[32];
|
||||
size_t out_sz;
|
||||
char *out;
|
||||
char *out = NULL;
|
||||
|
||||
if (!sender_pub_hex || !ciphertext) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
if (nostr_hex_to_bytes(sender_pub_hex, sender_pub, 32) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out_sz = strlen(ciphertext) + 1024U;
|
||||
out = (char *)malloc(out_sz);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (nostr_nip04_decrypt(priv, sender_pub, ciphertext, out, out_sz) != 0) {
|
||||
free(out);
|
||||
if (signer_nip04_decrypt(sender_pub_hex, ciphertext, &out) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -136,37 +146,7 @@ static char *posts_try_decrypt_nip04(const char *sender_pub_hex, const char *cip
|
||||
}
|
||||
|
||||
static int posts_encrypt_nip04_for_self(const char *plaintext, char **cipher_out) {
|
||||
unsigned char priv[32];
|
||||
unsigned char pub[32];
|
||||
size_t out_sz;
|
||||
char *out;
|
||||
|
||||
if (!plaintext || !cipher_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*cipher_out = NULL;
|
||||
|
||||
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (nostr_hex_to_bytes(g_state.npub_hex, pub, 32) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out_sz = (strlen(plaintext) * 4U) + 256U;
|
||||
out = (char *)malloc(out_sz);
|
||||
if (!out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (nostr_nip04_encrypt(priv, pub, plaintext, out, out_sz) != 0) {
|
||||
free(out);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*cipher_out = out;
|
||||
return 0;
|
||||
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) {
|
||||
@@ -328,53 +308,178 @@ static const char *posts_kind_label(int kind) {
|
||||
return "post";
|
||||
}
|
||||
|
||||
static void posts_view_item(const post_item_t *it) {
|
||||
static int posts_build_detail_lines(const post_item_t *it, char ***lines_out, int *count_out) {
|
||||
char **lines = NULL;
|
||||
int count = 0;
|
||||
char date_buf[64];
|
||||
char input[16];
|
||||
char *decrypted = NULL;
|
||||
char *render;
|
||||
const char *content;
|
||||
size_t i;
|
||||
size_t start = 0;
|
||||
|
||||
if (!it) {
|
||||
return;
|
||||
if (!it || !lines_out || !count_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*lines_out = NULL;
|
||||
*count_out = 0;
|
||||
|
||||
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);
|
||||
content = decrypted;
|
||||
} else if (it->kind == 30024 || it->kind == 30078) {
|
||||
tui_print("(decrypt failed; showing raw content)");
|
||||
tui_print("%s", it->content);
|
||||
content = "(decrypt failed; showing raw content)\n";
|
||||
} else {
|
||||
tui_print("%s", it->content);
|
||||
content = it->content;
|
||||
}
|
||||
|
||||
#define POSTS_PUSH_LINE(txt) \
|
||||
do { \
|
||||
char **next = (char **)realloc(lines, (size_t)(count + 1) * sizeof(char *)); \
|
||||
if (!next) { \
|
||||
goto fail; \
|
||||
} \
|
||||
lines = next; \
|
||||
lines[count] = posts_strdup((txt) ? (txt) : ""); \
|
||||
if (!lines[count]) { \
|
||||
goto fail; \
|
||||
} \
|
||||
count++; \
|
||||
} while (0)
|
||||
|
||||
{
|
||||
char hdr[1024];
|
||||
snprintf(hdr, sizeof(hdr), "id: %s", it->id ? it->id : "");
|
||||
POSTS_PUSH_LINE(hdr);
|
||||
}
|
||||
{
|
||||
char hdr[256];
|
||||
snprintf(hdr, sizeof(hdr), "kind: %d (%s)", it->kind, posts_kind_label(it->kind));
|
||||
POSTS_PUSH_LINE(hdr);
|
||||
}
|
||||
{
|
||||
char hdr[256];
|
||||
snprintf(hdr, sizeof(hdr), "date: %s", date_buf);
|
||||
POSTS_PUSH_LINE(hdr);
|
||||
}
|
||||
{
|
||||
char hdr[1024];
|
||||
snprintf(hdr, sizeof(hdr), "d: %s", (it->dtag && it->dtag[0] != '\0') ? it->dtag : "");
|
||||
POSTS_PUSH_LINE(hdr);
|
||||
}
|
||||
{
|
||||
char hdr[1024];
|
||||
snprintf(hdr, sizeof(hdr), "title: %s", (it->title && it->title[0] != '\0') ? it->title : "");
|
||||
POSTS_PUSH_LINE(hdr);
|
||||
}
|
||||
POSTS_PUSH_LINE("");
|
||||
|
||||
if (!decrypted && (it->kind == 30024 || it->kind == 30078)) {
|
||||
POSTS_PUSH_LINE("(decrypt failed; showing raw content)");
|
||||
render = it->content ? it->content : "";
|
||||
} else {
|
||||
render = (char *)content;
|
||||
}
|
||||
|
||||
for (i = 0; ; i++) {
|
||||
if (render[i] == '\n' || render[i] == '\0') {
|
||||
size_t len = i - start;
|
||||
char *ln = (char *)malloc(len + 1U);
|
||||
if (!ln) {
|
||||
goto fail;
|
||||
}
|
||||
memcpy(ln, render + start, len);
|
||||
ln[len] = '\0';
|
||||
POSTS_PUSH_LINE(ln);
|
||||
free(ln);
|
||||
if (render[i] == '\0') {
|
||||
break;
|
||||
}
|
||||
start = i + 1U;
|
||||
}
|
||||
}
|
||||
|
||||
free(decrypted);
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
*lines_out = lines;
|
||||
*count_out = count;
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
for (i = 0; i < (size_t)count; i++) {
|
||||
free(lines[i]);
|
||||
}
|
||||
free(lines);
|
||||
free(decrypted);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void posts_free_detail_lines(char **lines, int count) {
|
||||
int i;
|
||||
for (i = 0; i < count; i++) {
|
||||
free(lines[i]);
|
||||
}
|
||||
free(lines);
|
||||
}
|
||||
|
||||
static void posts_view_item(const post_item_t *it) {
|
||||
char **lines = NULL;
|
||||
int line_count = 0;
|
||||
size_t total = 1U;
|
||||
char *text;
|
||||
int i;
|
||||
|
||||
if (!it) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (posts_build_detail_lines(it, &lines, &line_count) != 0) {
|
||||
tuin_notice("Failed to render post content.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < line_count; i++) {
|
||||
total += strlen(lines[i] ? lines[i] : "") + 1U;
|
||||
}
|
||||
|
||||
text = (char *)malloc(total);
|
||||
if (!text) {
|
||||
posts_free_detail_lines(lines, line_count);
|
||||
tuin_notice("Out of memory while preparing post view.");
|
||||
return;
|
||||
}
|
||||
|
||||
text[0] = '\0';
|
||||
for (i = 0; i < line_count; i++) {
|
||||
strcat(text, lines[i] ? lines[i] : "");
|
||||
strcat(text, "\n");
|
||||
}
|
||||
|
||||
nt_pager_show_text("> Main Menu > Posts > View", text);
|
||||
|
||||
free(text);
|
||||
posts_free_detail_lines(lines, line_count);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (tuin_confirm("Delete this post from relays?") != 1) {
|
||||
nt_log("Delete canceled.");
|
||||
return;
|
||||
}
|
||||
|
||||
tags = cJSON_CreateArray();
|
||||
if (!tags) {
|
||||
return;
|
||||
@@ -394,12 +499,10 @@ static void posts_delete_item(const post_item_t *it) {
|
||||
cJSON_Delete(tags);
|
||||
|
||||
if (posted < 0) {
|
||||
tui_print("Delete publish failed.");
|
||||
tuin_notice("Delete publish failed.");
|
||||
} else {
|
||||
tui_print("Deletion published.");
|
||||
nt_log("Deletion published.");
|
||||
}
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
}
|
||||
|
||||
static void posts_edit_item(const post_item_t *it) {
|
||||
@@ -408,7 +511,6 @@ static void posts_edit_item(const post_item_t *it) {
|
||||
char *out_content = NULL;
|
||||
cJSON *tags_copy = NULL;
|
||||
int posted;
|
||||
char input[16];
|
||||
|
||||
if (!it) {
|
||||
return;
|
||||
@@ -424,32 +526,28 @@ static void posts_edit_item(const post_item_t *it) {
|
||||
}
|
||||
|
||||
if (!initial) {
|
||||
tui_print("Failed to prepare editor content.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
tuin_notice("Failed to prepare editor content.");
|
||||
return;
|
||||
}
|
||||
|
||||
edited = editor_launch(initial);
|
||||
edited = posts_launch_editor(initial);
|
||||
free(initial);
|
||||
|
||||
if (!edited) {
|
||||
tui_print("Edit canceled.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
nt_log("Edit canceled.");
|
||||
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));
|
||||
tuin_notice("Failed to encrypt updated content.");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
out_content = posts_strdup(edited);
|
||||
if (!out_content) {
|
||||
free(edited);
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -458,8 +556,7 @@ static void posts_edit_item(const post_item_t *it) {
|
||||
if (!tags_copy) {
|
||||
free(edited);
|
||||
free(out_content);
|
||||
tui_print("Failed to duplicate tags.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
tuin_notice("Failed to duplicate tags.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -469,77 +566,119 @@ static void posts_edit_item(const post_item_t *it) {
|
||||
free(out_content);
|
||||
|
||||
if (posted < 0) {
|
||||
tui_print("Post update failed.");
|
||||
tuin_notice("Post update failed.");
|
||||
} else {
|
||||
tui_print("Post updated.");
|
||||
nt_log("Post updated.");
|
||||
}
|
||||
}
|
||||
|
||||
static void posts_table_get_cell(int row, int col, char *out, size_t out_size, void *user_data) {
|
||||
const posts_table_ctx_t *ctx = (const posts_table_ctx_t *)user_data;
|
||||
const post_item_t *it;
|
||||
char date_buf[64];
|
||||
const char *label;
|
||||
|
||||
if (!out || out_size == 0U) {
|
||||
return;
|
||||
}
|
||||
out[0] = '\0';
|
||||
|
||||
if (!ctx || row < 0 || row >= ctx->count) {
|
||||
return;
|
||||
}
|
||||
|
||||
it = &ctx->items[row];
|
||||
label = (it->title && it->title[0] != '\0')
|
||||
? it->title
|
||||
: ((it->dtag && it->dtag[0] != '\0') ? it->dtag : "(untitled)");
|
||||
posts_format_date(it->created_at, date_buf, sizeof(date_buf));
|
||||
|
||||
if (col == 0) {
|
||||
snprintf(out, out_size, "%d", row + 1);
|
||||
} else if (col == 1) {
|
||||
snprintf(out, out_size, "%d/%s", it->kind, posts_kind_label(it->kind));
|
||||
} else if (col == 2) {
|
||||
snprintf(out, out_size, "%s", label);
|
||||
} else if (col == 3) {
|
||||
snprintf(out, out_size, "%s", date_buf);
|
||||
}
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
}
|
||||
|
||||
static void posts_kind_menu(const char *title, const char *filter_json) {
|
||||
static const TuiMenuItem ACTION_ITEMS[] = {
|
||||
{NT_HK("V", "iew selected"), 'v'},
|
||||
{NT_HK("E", "dit selected"), 'e'},
|
||||
{NT_HK("D", "elete selected"), 'd'},
|
||||
{NT_HK("X", "Exit"), 'x'},
|
||||
};
|
||||
|
||||
post_item_t *items = NULL;
|
||||
int items_count = 0;
|
||||
char input[32];
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("%s\n", title);
|
||||
int selected = 0;
|
||||
TuiMenuState action_state = {0};
|
||||
|
||||
nt_log("Fetching posts...");
|
||||
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));
|
||||
tuin_notice("Failed to query posts (all relays failed).");
|
||||
return;
|
||||
}
|
||||
|
||||
while (1) {
|
||||
int i;
|
||||
TuiFrame frame = nt_frame("> Main Menu > Posts > List");
|
||||
char status_text[256];
|
||||
TuiStatus status;
|
||||
TuiColumn cols[] = {
|
||||
{"#", 4, 1},
|
||||
{"Kind", 16, 0},
|
||||
{"Title", 0, 0},
|
||||
{"Date", 18, 0},
|
||||
};
|
||||
posts_table_ctx_t ctx;
|
||||
TuiTable table;
|
||||
TuiMenu menu = {ACTION_ITEMS, 4};
|
||||
int row;
|
||||
int action;
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("%s\n", title);
|
||||
for (i = 0; i < items_count; i++) {
|
||||
char date_buf[64];
|
||||
const char *label;
|
||||
snprintf(status_text, sizeof(status_text), "%s", title ? title : "Posts");
|
||||
status.text = status_text;
|
||||
|
||||
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)");
|
||||
ctx.items = items;
|
||||
ctx.count = items_count;
|
||||
|
||||
tui_print("[%2d] (%d/%s) %s %s",
|
||||
i + 1,
|
||||
items[i].kind,
|
||||
posts_kind_label(items[i].kind),
|
||||
label,
|
||||
date_buf);
|
||||
table.columns = cols;
|
||||
table.column_count = (int)(sizeof(cols) / sizeof(cols[0]));
|
||||
table.row_count = items_count;
|
||||
table.user_data = &ctx;
|
||||
table.get_cell = posts_table_get_cell;
|
||||
table.is_default = NULL;
|
||||
table.prefix_len = NULL;
|
||||
|
||||
row = tuin_table_run(&frame, &table, &status);
|
||||
if (row >= 0 && row < items_count) {
|
||||
selected = row;
|
||||
}
|
||||
|
||||
if (items_count == 0) {
|
||||
tui_print("No posts found.");
|
||||
}
|
||||
|
||||
tui_print("");
|
||||
tui_print("Commands: v <n> (view), e <n> (edit), d <n> (delete), x (back)");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
|
||||
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
|
||||
nt_log("No posts found.");
|
||||
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;
|
||||
}
|
||||
action = tuin_menu_run(&frame, &menu, &status, &action_state);
|
||||
if (action < 0 || action == 3) {
|
||||
break;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (selected < 0 || selected >= items_count) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (action == 0) {
|
||||
posts_view_item(&items[selected]);
|
||||
} else if (action == 1) {
|
||||
posts_edit_item(&items[selected]);
|
||||
} else if (action == 2) {
|
||||
posts_delete_item(&items[selected]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,43 +686,45 @@ static void posts_kind_menu(const char *title, const char *filter_json) {
|
||||
}
|
||||
|
||||
void menu_posts(void) {
|
||||
char input[32];
|
||||
static const TuiMenuItem POSTS_ITEMS[] = {
|
||||
{NT_HK("S", "ecret blog (kind 30024)"), 's'},
|
||||
{NT_HK("P", "ublic blog (kind 30023)"), 'p'},
|
||||
{NT_HK("E", "ncrypted data (kind 30078)"), 'e'},
|
||||
{NT_HK("A", "ll posts"), 'a'},
|
||||
{NT_HK("X", "Exit"), 'x'},
|
||||
};
|
||||
TuiMenuState menu_state = {0};
|
||||
char filter[512];
|
||||
|
||||
while (1) {
|
||||
tui_clear_screen();
|
||||
tui_print("POSTS\n");
|
||||
tui_print("1 - Private blog (kind 30024)");
|
||||
tui_print("2 - Public blog (kind 30023)");
|
||||
tui_print("3 - Encrypted data (kind 30078)");
|
||||
tui_print("4 - All posts");
|
||||
tui_print("x - Back");
|
||||
TuiFrame frame = nt_frame("> Main Menu > Posts");
|
||||
TuiStatus status = nt_status();
|
||||
TuiMenu menu = {POSTS_ITEMS, 5};
|
||||
int idx = tuin_menu_run(&frame, &menu, &status, &menu_state);
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
|
||||
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
|
||||
if (idx < 0 || idx == 4) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (input[0] == '1') {
|
||||
if (idx == 0) {
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"{\"authors\":[\"%s\"],\"kinds\":[30024],\"limit\":200}",
|
||||
g_state.npub_hex);
|
||||
posts_kind_menu("PRIVATE BLOG", filter);
|
||||
} else if (input[0] == '2') {
|
||||
} else if (idx == 1) {
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"{\"authors\":[\"%s\"],\"kinds\":[30023],\"limit\":200}",
|
||||
g_state.npub_hex);
|
||||
posts_kind_menu("PUBLIC BLOG", filter);
|
||||
} else if (input[0] == '3') {
|
||||
} else if (idx == 2) {
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"{\"authors\":[\"%s\"],\"kinds\":[30078],\"limit\":200}",
|
||||
g_state.npub_hex);
|
||||
posts_kind_menu("ENCRYPTED DATA", filter);
|
||||
} else if (input[0] == '4') {
|
||||
} else if (idx == 3) {
|
||||
snprintf(filter,
|
||||
sizeof(filter),
|
||||
"{\"authors\":[\"%s\"],\"kinds\":[30023,30024,30078],\"limit\":300}",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "tui.h"
|
||||
#include "state.h"
|
||||
#include "net.h"
|
||||
#include "signer.h"
|
||||
|
||||
#include "publish.h"
|
||||
|
||||
@@ -15,22 +16,58 @@ static int profile_publish_kind0(const char *content_json) {
|
||||
}
|
||||
|
||||
void menu_profile(void) {
|
||||
char menu_sel[32];
|
||||
static const char *fields[] = {"name", "about", "picture", "banner", "website", "lud16", "nip05"};
|
||||
static const TuiMenuItem PROFILE_ITEMS[] = {
|
||||
{NT_HK("M", "odify account"), 'm'},
|
||||
{NT_HK("P", "ost changes and exit."), 'p'},
|
||||
{"E" NT_HK("x", "it without saving"), 'x'},
|
||||
};
|
||||
TuiMenuState menu_state = {0};
|
||||
char edit_buf[512];
|
||||
|
||||
while (1) {
|
||||
TuiFrame frame = nt_frame("> Main Menu > Profile");
|
||||
TuiMenu menu = {PROFILE_ITEMS, 3};
|
||||
TuiStatus status = nt_status();
|
||||
cJSON *meta;
|
||||
const char *fields[] = {"name", "about", "picture", "banner", "website", "lud16", "nip05"};
|
||||
int i;
|
||||
int idx;
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("PROFILE\n");
|
||||
tuin_render_header(&frame);
|
||||
nt_print_reset();
|
||||
tuin_render_footer(&status);
|
||||
|
||||
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("");
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER) {
|
||||
nt_print("Signer: n_signer (@%s)", g_signer.socket_name);
|
||||
if (g_signer.selector.has_nostr_index) {
|
||||
nt_print("Signer selector: nostr_index=%d", g_signer.selector.nostr_index);
|
||||
} else {
|
||||
nt_print("Signer selector: role=%s", g_signer.selector.role[0] ? g_signer.selector.role : "main");
|
||||
}
|
||||
} else if (g_signer.kind == NT_SIGNER_NSIGNER_QREXEC) {
|
||||
nt_print("Signer: n_signer qrexec");
|
||||
nt_print("Signer target: %s", g_signer.qrexec_target);
|
||||
nt_print("Signer service: %s", g_signer.qrexec_service);
|
||||
if (g_signer.selector.has_nostr_index) {
|
||||
nt_print("Signer selector: nostr_index=%d", g_signer.selector.nostr_index);
|
||||
} else {
|
||||
nt_print("Signer selector: role=%s", g_signer.selector.role[0] ? g_signer.selector.role : "main");
|
||||
}
|
||||
} else if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
|
||||
nt_print("Signer: n_signer URL");
|
||||
nt_print("Signer endpoint: %s", g_signer.endpoint_url);
|
||||
if (g_signer.selector.has_nostr_index) {
|
||||
nt_print("Signer selector: nostr_index=%d", g_signer.selector.nostr_index);
|
||||
} else {
|
||||
nt_print("Signer selector: role=%s", g_signer.selector.role[0] ? g_signer.selector.role : "main");
|
||||
}
|
||||
} else {
|
||||
nt_print("nsecHex: %s", g_state.nsec_hex);
|
||||
nt_print("nsec: %s", g_state.nsec_bech32);
|
||||
}
|
||||
nt_print("npubHex: %s", g_state.npub_hex);
|
||||
nt_print("npub: %s", g_state.npub_bech32);
|
||||
nt_print("");
|
||||
|
||||
meta = cJSON_Parse(g_state.kind0_json ? g_state.kind0_json : "{}");
|
||||
if (!meta) {
|
||||
@@ -39,30 +76,28 @@ void menu_profile(void) {
|
||||
|
||||
for (i = 0; i < (int)(sizeof(fields) / sizeof(fields[0])); i++) {
|
||||
cJSON *v = cJSON_GetObjectItemCaseSensitive(meta, fields[i]);
|
||||
tui_print("%-18s %s", fields[i], (cJSON_IsString(v) && v->valuestring) ? v->valuestring : "");
|
||||
nt_print("%-18s %s", fields[i], (cJSON_IsString(v) && v->valuestring) ? v->valuestring : "");
|
||||
}
|
||||
|
||||
tui_print("\n");
|
||||
tui_print("^_M^:odify account");
|
||||
tui_print("^_P^:ost changes and exit.");
|
||||
tui_print("E^_x^:it without saving");
|
||||
idx = tuin_menu_run(&frame, &menu, &status, &menu_state);
|
||||
if (idx < 0 || idx == 2) {
|
||||
cJSON_Delete(meta);
|
||||
break;
|
||||
}
|
||||
|
||||
tui_get_line(">", menu_sel, (int)sizeof(menu_sel));
|
||||
|
||||
if (menu_sel[0] == 'm' || menu_sel[0] == 'M') {
|
||||
if (idx == 0) {
|
||||
for (i = 0; i < (int)(sizeof(fields) / sizeof(fields[0])); i++) {
|
||||
cJSON *v = cJSON_GetObjectItemCaseSensitive(meta, fields[i]);
|
||||
const char *cur = (cJSON_IsString(v) && v->valuestring) ? v->valuestring : "";
|
||||
char prompt[64];
|
||||
|
||||
snprintf(prompt, sizeof(prompt), "%-20s >", fields[i]);
|
||||
tui_get_line(prompt, edit_buf, (int)sizeof(edit_buf));
|
||||
if (edit_buf[0] != '\0') {
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(meta, fields[i]);
|
||||
cJSON_AddStringToObject(meta, fields[i], edit_buf);
|
||||
} else if (!cJSON_GetObjectItemCaseSensitive(meta, fields[i])) {
|
||||
cJSON_AddStringToObject(meta, fields[i], cur);
|
||||
snprintf(prompt, sizeof(prompt), "%s", fields[i]);
|
||||
if (tuin_prompt(prompt, cur, edit_buf, sizeof(edit_buf)) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(meta, fields[i]);
|
||||
cJSON_AddStringToObject(meta, fields[i], edit_buf);
|
||||
}
|
||||
|
||||
free(g_state.kind0_json);
|
||||
@@ -80,19 +115,19 @@ void menu_profile(void) {
|
||||
snprintf(g_state.user_name, sizeof(g_state.user_name), "%s", name->valuestring);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (menu_sel[0] == 'p' || menu_sel[0] == 'P') {
|
||||
int posted = profile_publish_kind0(g_state.kind0_json ? g_state.kind0_json : "{}");
|
||||
if (posted < 0) {
|
||||
tui_print("Profile publish failed.");
|
||||
} else if (idx == 1) {
|
||||
int posted;
|
||||
if (tuin_confirm("Publish profile changes?") != 1) {
|
||||
cJSON_Delete(meta);
|
||||
continue;
|
||||
}
|
||||
cJSON_Delete(meta);
|
||||
tui_get_line(">", edit_buf, (int)sizeof(edit_buf));
|
||||
break;
|
||||
}
|
||||
|
||||
if (menu_sel[0] == 'x' || menu_sel[0] == 'X') {
|
||||
posted = profile_publish_kind0(g_state.kind0_json ? g_state.kind0_json : "{}");
|
||||
if (posted < 0) {
|
||||
tuin_notice("Profile publish failed.");
|
||||
} else {
|
||||
nt_log("Profile published.");
|
||||
}
|
||||
cJSON_Delete(meta);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,16 @@
|
||||
|
||||
#include "../resources/nostr_core_lib/cjson/cJSON.h"
|
||||
|
||||
#include <ncurses.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
cJSON *tags;
|
||||
int *tag_indices;
|
||||
int count;
|
||||
} relay_table_ctx_t;
|
||||
|
||||
static int relays_publish_kind10002(cJSON *event_obj) {
|
||||
cJSON *tags;
|
||||
cJSON *content;
|
||||
@@ -27,78 +34,117 @@ 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)");
|
||||
static int relays_collect_rows(cJSON *tags, int **out_indices) {
|
||||
int n;
|
||||
int i;
|
||||
int count = 0;
|
||||
int *indices;
|
||||
|
||||
if (!out_indices || !cJSON_IsArray(tags)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
*out_indices = NULL;
|
||||
n = cJSON_GetArraySize(tags);
|
||||
if (n <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
indices = (int *)malloc((size_t)n * sizeof(int));
|
||||
if (!indices) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (i = 0; i < n; i++) {
|
||||
cJSON *tag = cJSON_GetArrayItem(tags, i);
|
||||
cJSON *t0 = cJSON_GetArrayItem(tag, 0);
|
||||
if (!cJSON_IsArray(tag) || !cJSON_IsString(t0) || !t0->valuestring || strcmp(t0->valuestring, "r") != 0) {
|
||||
continue;
|
||||
}
|
||||
indices[count++] = i;
|
||||
}
|
||||
|
||||
*out_indices = indices;
|
||||
return count;
|
||||
}
|
||||
|
||||
static void relays_table_get_cell(int row, int col, char *out, size_t out_size, void *user_data) {
|
||||
const relay_table_ctx_t *ctx = (const relay_table_ctx_t *)user_data;
|
||||
cJSON *tag;
|
||||
cJSON *t1;
|
||||
cJSON *t2;
|
||||
const char *rw = "read write";
|
||||
|
||||
if (!out || out_size == 0U) {
|
||||
return;
|
||||
}
|
||||
out[0] = '\0';
|
||||
|
||||
if (!ctx || row < 0 || row >= ctx->count) {
|
||||
return;
|
||||
}
|
||||
|
||||
doc = cJSON_Parse(nip11_json);
|
||||
if (!doc || !cJSON_IsObject(doc)) {
|
||||
tui_print("NIP-11 (raw): %s", nip11_json);
|
||||
cJSON_Delete(doc);
|
||||
tag = cJSON_GetArrayItem(ctx->tags, ctx->tag_indices[row]);
|
||||
t1 = cJSON_GetArrayItem(tag, 1);
|
||||
t2 = cJSON_GetArrayItem(tag, 2);
|
||||
|
||||
if (cJSON_IsString(t2) && t2->valuestring) {
|
||||
rw = t2->valuestring;
|
||||
}
|
||||
|
||||
if (col == 0) {
|
||||
snprintf(out, out_size, "%d", row + 1);
|
||||
} else if (col == 1) {
|
||||
snprintf(out, out_size, "%s", (cJSON_IsString(t1) && t1->valuestring) ? t1->valuestring : "");
|
||||
} else if (col == 2) {
|
||||
snprintf(out, out_size, "%s", rw);
|
||||
}
|
||||
}
|
||||
|
||||
static void relays_show_nip11_screen(const char *relay_url) {
|
||||
char *nip11 = NULL;
|
||||
|
||||
if (!relay_url || relay_url[0] == '\0') {
|
||||
tuin_notice("Selected relay has no URL.");
|
||||
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");
|
||||
nt_log("Fetching NIP-11: %s", relay_url);
|
||||
|
||||
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 (nt_fetch_nip11(relay_url, &nip11) == 0 && nip11) {
|
||||
size_t total = strlen(relay_url) + strlen(nip11) + 32U;
|
||||
char *view_text = (char *)malloc(total);
|
||||
if (!view_text) {
|
||||
free(nip11);
|
||||
tuin_notice("Out of memory while preparing NIP-11 view.");
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(view_text, total, "Relay: %s\n\n%s", relay_url, nip11);
|
||||
nt_pager_show_text("> Main Menu > Relays > NIP-11", view_text);
|
||||
free(view_text);
|
||||
} else {
|
||||
tuin_notice("NIP-11 fetch failed.");
|
||||
}
|
||||
|
||||
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);
|
||||
free(nip11);
|
||||
}
|
||||
|
||||
void menu_relays(void) {
|
||||
static const TuiMenuItem ACTION_ITEMS[] = {
|
||||
{NT_HK("A", "dd relay"), 'a'},
|
||||
{NT_HK("D", "elete selected relay"), 'd'},
|
||||
{NT_HK("M", "odify selected relay"), 'm'},
|
||||
{NT_HK("N", "IP-11 selected relay"), 'n'},
|
||||
{NT_HK("P", "ost changes and exit"), 'p'},
|
||||
{"E" NT_HK("x", "it without saving"), 'x'},
|
||||
};
|
||||
|
||||
cJSON *working = NULL;
|
||||
cJSON *tags = NULL;
|
||||
char input[512];
|
||||
TuiMenuState action_state = {0};
|
||||
int selected_tag_index = -1;
|
||||
|
||||
working = g_state.kind10002_json ? cJSON_Parse(g_state.kind10002_json) : NULL;
|
||||
if (!working) {
|
||||
@@ -116,69 +162,78 @@ void menu_relays(void) {
|
||||
}
|
||||
|
||||
while (1) {
|
||||
int i;
|
||||
int n;
|
||||
int *row_indices = NULL;
|
||||
int row_count = relays_collect_rows(tags, &row_indices);
|
||||
relay_table_ctx_t ctx;
|
||||
TuiFrame frame = nt_frame("> Main Menu > Relays");
|
||||
TuiStatus status = nt_status();
|
||||
TuiColumn cols[] = {
|
||||
{"#", 4, 1},
|
||||
{"URL", 0, 0},
|
||||
{"R/W", 12, 0},
|
||||
};
|
||||
TuiTable table;
|
||||
int selected_row;
|
||||
TuiMenu action_menu = {ACTION_ITEMS, (int)(sizeof(ACTION_ITEMS) / sizeof(ACTION_ITEMS[0]))};
|
||||
int action;
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("RELAYS\n");
|
||||
ctx.tags = tags;
|
||||
ctx.tag_indices = row_indices;
|
||||
ctx.count = row_count;
|
||||
|
||||
n = cJSON_GetArraySize(tags);
|
||||
for (i = 0; i < n; i++) {
|
||||
cJSON *tag = cJSON_GetArrayItem(tags, i);
|
||||
cJSON *t0 = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON *t1 = cJSON_GetArrayItem(tag, 1);
|
||||
cJSON *t2 = cJSON_GetArrayItem(tag, 2);
|
||||
const char *rw = "read write";
|
||||
table.columns = cols;
|
||||
table.column_count = (int)(sizeof(cols) / sizeof(cols[0]));
|
||||
table.row_count = row_count;
|
||||
table.user_data = &ctx;
|
||||
table.get_cell = relays_table_get_cell;
|
||||
table.is_default = NULL;
|
||||
table.prefix_len = NULL;
|
||||
|
||||
if (!cJSON_IsArray(tag) || !cJSON_IsString(t0) || !t0->valuestring || strcmp(t0->valuestring, "r") != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cJSON_IsString(t2) && t2->valuestring) {
|
||||
rw = t2->valuestring;
|
||||
}
|
||||
|
||||
tui_print("[%2d] %-30s %s", i + 1, (cJSON_IsString(t1) && t1->valuestring) ? t1->valuestring : "", rw);
|
||||
selected_row = tuin_table_run(&frame, &table, &status);
|
||||
if (selected_row >= 0 && selected_row < row_count) {
|
||||
selected_tag_index = row_indices[selected_row];
|
||||
}
|
||||
|
||||
tui_print("\n\n");
|
||||
tui_print("^_A^:dd relay");
|
||||
tui_print("^_D^:elete relay");
|
||||
tui_print("^_M^:odify relay");
|
||||
tui_print("^_P^:ost changes and exit.");
|
||||
tui_print("E^_x^:it without saving");
|
||||
free(row_indices);
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
action = tuin_menu_run(&frame, &action_menu, &status, &action_state);
|
||||
if (action < 0 || action == 5) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (input[0] == 'a' || input[0] == 'A') {
|
||||
if (action == 0) {
|
||||
char relay_url[256];
|
||||
char yn[32];
|
||||
char read_yn[32];
|
||||
char write_yn[32];
|
||||
char *nip11 = NULL;
|
||||
|
||||
tui_print("\n\nADD RELAY\n");
|
||||
tui_get_line("Relay URL >", relay_url, (int)sizeof(relay_url));
|
||||
if (relay_url[0] == '\0') {
|
||||
relay_url[0] = '\0';
|
||||
if (tuin_prompt("Relay URL >", "", relay_url, sizeof(relay_url)) != 0 || relay_url[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
tui_print("Gathering relay data ...");
|
||||
if (nt_fetch_nip11(relay_url, &nip11) == 0 && nip11) {
|
||||
relays_print_nip11_summary(nip11);
|
||||
free(nip11);
|
||||
relays_show_nip11_screen(relay_url);
|
||||
} else {
|
||||
tui_print("NIP-11 fetch failed.");
|
||||
tuin_notice("NIP-11 fetch failed.");
|
||||
}
|
||||
free(nip11);
|
||||
|
||||
yn[0] = '\0';
|
||||
if (tuin_prompt("Add relay [y/n] >", "", yn, sizeof(yn)) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tui_get_line("Add relay [y/n] >", yn, (int)sizeof(yn));
|
||||
if (yn[0] == 'y' || yn[0] == 'Y') {
|
||||
cJSON *tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(tag, cJSON_CreateString("r"));
|
||||
cJSON_AddItemToArray(tag, cJSON_CreateString(relay_url));
|
||||
|
||||
tui_get_line("Read from relay [y/n] >", read_yn, (int)sizeof(read_yn));
|
||||
tui_get_line("Write to relay [y/n] >", write_yn, (int)sizeof(write_yn));
|
||||
read_yn[0] = '\0';
|
||||
write_yn[0] = '\0';
|
||||
(void)tuin_prompt("Read from relay [y/n] >", "", read_yn, sizeof(read_yn));
|
||||
(void)tuin_prompt("Write to relay [y/n] >", "", write_yn, sizeof(write_yn));
|
||||
|
||||
if ((read_yn[0] == 'n' || read_yn[0] == 'N') && (write_yn[0] == 'y' || write_yn[0] == 'Y')) {
|
||||
cJSON_AddItemToArray(tag, cJSON_CreateString("write"));
|
||||
@@ -187,44 +242,41 @@ void menu_relays(void) {
|
||||
}
|
||||
cJSON_AddItemToArray(tags, tag);
|
||||
}
|
||||
} else if (input[0] == 'd' || input[0] == 'D') {
|
||||
char numbuf[32];
|
||||
char yn[32];
|
||||
int idx;
|
||||
} else if (action == 1) {
|
||||
cJSON *tag;
|
||||
cJSON *url;
|
||||
|
||||
tui_get_line("relay to delete >", numbuf, (int)sizeof(numbuf));
|
||||
idx = atoi(numbuf) - 1;
|
||||
if (idx < 0 || idx >= cJSON_GetArraySize(tags)) {
|
||||
if (selected_tag_index < 0 || selected_tag_index >= cJSON_GetArraySize(tags)) {
|
||||
tuin_notice("Select a relay row first.");
|
||||
continue;
|
||||
}
|
||||
|
||||
tag = cJSON_GetArrayItem(tags, idx);
|
||||
tag = cJSON_GetArrayItem(tags, selected_tag_index);
|
||||
url = cJSON_GetArrayItem(tag, 1);
|
||||
tui_get_line("Delete selected relay [y/n] >", yn, (int)sizeof(yn));
|
||||
if (yn[0] == 'y' || yn[0] == 'Y') {
|
||||
cJSON_DeleteItemFromArray(tags, idx);
|
||||
|
||||
if (tuin_confirm("Delete selected relay?") == 1) {
|
||||
cJSON_DeleteItemFromArray(tags, selected_tag_index);
|
||||
selected_tag_index = -1;
|
||||
if (cJSON_IsString(url) && url->valuestring) {
|
||||
tui_print("Deleted %s", url->valuestring);
|
||||
char msg[512];
|
||||
snprintf(msg, sizeof(msg), "Deleted %s", url->valuestring);
|
||||
tuin_notice(msg);
|
||||
} else {
|
||||
tuin_notice("Deleted relay.");
|
||||
}
|
||||
}
|
||||
} else if (input[0] == 'm' || input[0] == 'M') {
|
||||
char numbuf[32];
|
||||
} else if (action == 2) {
|
||||
char read_yn[32];
|
||||
char write_yn[32];
|
||||
int idx;
|
||||
cJSON *old_tag;
|
||||
cJSON *url;
|
||||
cJSON *new_tag;
|
||||
|
||||
tui_get_line("# relay to modify >", numbuf, (int)sizeof(numbuf));
|
||||
idx = atoi(numbuf) - 1;
|
||||
if (idx < 0 || idx >= cJSON_GetArraySize(tags)) {
|
||||
if (selected_tag_index < 0 || selected_tag_index >= cJSON_GetArraySize(tags)) {
|
||||
tuin_notice("Select a relay row first.");
|
||||
continue;
|
||||
}
|
||||
|
||||
old_tag = cJSON_GetArrayItem(tags, idx);
|
||||
old_tag = cJSON_GetArrayItem(tags, selected_tag_index);
|
||||
url = cJSON_GetArrayItem(old_tag, 1);
|
||||
if (!cJSON_IsString(url) || !url->valuestring || url->valuestring[0] == '\0') {
|
||||
continue;
|
||||
@@ -234,8 +286,10 @@ void menu_relays(void) {
|
||||
cJSON_AddItemToArray(new_tag, cJSON_CreateString("r"));
|
||||
cJSON_AddItemToArray(new_tag, cJSON_CreateString(url->valuestring));
|
||||
|
||||
tui_get_line("Read from relay [y/n]?", read_yn, (int)sizeof(read_yn));
|
||||
tui_get_line("Write to relay [y/n]?", write_yn, (int)sizeof(write_yn));
|
||||
read_yn[0] = '\0';
|
||||
write_yn[0] = '\0';
|
||||
(void)tuin_prompt("Read from relay [y/n]?", "", read_yn, sizeof(read_yn));
|
||||
(void)tuin_prompt("Write to relay [y/n]?", "", write_yn, sizeof(write_yn));
|
||||
|
||||
if ((read_yn[0] == 'n' || read_yn[0] == 'N') && (write_yn[0] == 'y' || write_yn[0] == 'Y')) {
|
||||
cJSON_AddItemToArray(new_tag, cJSON_CreateString("write"));
|
||||
@@ -243,21 +297,37 @@ void menu_relays(void) {
|
||||
cJSON_AddItemToArray(new_tag, cJSON_CreateString("read"));
|
||||
}
|
||||
|
||||
cJSON_ReplaceItemInArray(tags, idx, new_tag);
|
||||
} else if (input[0] == 'p' || input[0] == 'P') {
|
||||
cJSON_ReplaceItemInArray(tags, selected_tag_index, new_tag);
|
||||
} else if (action == 3) {
|
||||
cJSON *tag;
|
||||
cJSON *url;
|
||||
if (selected_tag_index < 0 || selected_tag_index >= cJSON_GetArraySize(tags)) {
|
||||
tuin_notice("Select a relay row first.");
|
||||
continue;
|
||||
}
|
||||
|
||||
tag = cJSON_GetArrayItem(tags, selected_tag_index);
|
||||
url = cJSON_GetArrayItem(tag, 1);
|
||||
if (!cJSON_IsString(url) || !url->valuestring) {
|
||||
tuin_notice("Selected relay has no URL.");
|
||||
continue;
|
||||
}
|
||||
relays_show_nip11_screen(url->valuestring);
|
||||
} else if (action == 4) {
|
||||
int posted = relays_publish_kind10002(working);
|
||||
char *new_json = cJSON_PrintUnformatted(working);
|
||||
|
||||
if (new_json) {
|
||||
free(g_state.kind10002_json);
|
||||
g_state.kind10002_json = new_json;
|
||||
state_parse_relay_list();
|
||||
}
|
||||
|
||||
if (posted < 0) {
|
||||
tui_print("Relay list publish failed.");
|
||||
tuin_notice("Relay list publish failed.");
|
||||
} else {
|
||||
nt_log("Relay list published.");
|
||||
}
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
break;
|
||||
} else if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
316
src/menu_todo.c
316
src/menu_todo.c
@@ -2,6 +2,7 @@
|
||||
#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"
|
||||
@@ -16,6 +17,11 @@ typedef struct {
|
||||
int done;
|
||||
} todo_item_t;
|
||||
|
||||
typedef struct {
|
||||
const todo_item_t *items;
|
||||
int count;
|
||||
} todo_table_ctx_t;
|
||||
|
||||
static char *todo_strdup(const char *s) {
|
||||
size_t n;
|
||||
char *out;
|
||||
@@ -86,64 +92,13 @@ static int todo_parse_index_1based(const char *input, int count) {
|
||||
}
|
||||
|
||||
static int todo_encrypt_for_self(const char *plaintext, char **cipher_out) {
|
||||
unsigned char priv[32];
|
||||
unsigned char pub[32];
|
||||
size_t out_sz;
|
||||
char *out;
|
||||
|
||||
if (!plaintext || !cipher_out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*cipher_out = NULL;
|
||||
|
||||
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (nostr_hex_to_bytes(g_state.npub_hex, pub, 32) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out_sz = (strlen(plaintext) * 4U) + 256U;
|
||||
out = (char *)malloc(out_sz);
|
||||
if (!out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (nostr_nip04_encrypt(priv, pub, plaintext, out, out_sz) != 0) {
|
||||
free(out);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*cipher_out = out;
|
||||
return 0;
|
||||
return signer_nip44_self_encrypt(plaintext, cipher_out);
|
||||
}
|
||||
|
||||
static char *todo_decrypt_from_pub(const char *sender_pub_hex, const char *ciphertext) {
|
||||
unsigned char priv[32];
|
||||
unsigned char sender_pub[32];
|
||||
size_t out_sz;
|
||||
char *out;
|
||||
char *out = NULL;
|
||||
|
||||
if (!sender_pub_hex || !ciphertext) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
if (nostr_hex_to_bytes(sender_pub_hex, sender_pub, 32) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out_sz = strlen(ciphertext) + 2048U;
|
||||
out = (char *)malloc(out_sz);
|
||||
if (!out) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (nostr_nip04_decrypt(priv, sender_pub, ciphertext, out, out_sz) != 0) {
|
||||
free(out);
|
||||
if (signer_nip44_decrypt(sender_pub_hex, ciphertext, &out) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -305,99 +260,6 @@ static int todo_load_remote(todo_item_t **items_out, int *count_out) {
|
||||
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;
|
||||
@@ -406,19 +268,19 @@ static int todo_publish(const todo_item_t *items, int count) {
|
||||
|
||||
json = todo_items_to_json(items, count);
|
||||
if (!json) {
|
||||
tui_print("Failed to serialize todo list.");
|
||||
tuin_notice("Failed to serialize todo list.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (todo_encrypt_for_self(json, &cipher) != 0) {
|
||||
tui_print("Failed to encrypt todo list.");
|
||||
tuin_notice("Failed to encrypt todo list.");
|
||||
free(json);
|
||||
return -1;
|
||||
}
|
||||
|
||||
tags = todo_make_d_tag();
|
||||
if (!tags) {
|
||||
tui_print("Failed to build todo tags.");
|
||||
tuin_notice("Failed to build todo tags.");
|
||||
free(json);
|
||||
free(cipher);
|
||||
return -1;
|
||||
@@ -430,53 +292,157 @@ static int todo_publish(const todo_item_t *items, int count) {
|
||||
free(cipher);
|
||||
|
||||
if (posted < 0) {
|
||||
tui_print("Todo publish failed.");
|
||||
tuin_notice("Todo publish failed.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
tui_print("Todo saved.");
|
||||
nt_log("Todo saved.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void todo_table_get_cell(int row, int col, char *out, size_t out_size, void *user_data) {
|
||||
const todo_table_ctx_t *ctx = (const todo_table_ctx_t *)user_data;
|
||||
|
||||
if (!out || out_size == 0U) {
|
||||
return;
|
||||
}
|
||||
out[0] = '\0';
|
||||
|
||||
if (!ctx || row < 0 || row >= ctx->count) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (col == 0) {
|
||||
snprintf(out, out_size, "%d", row + 1);
|
||||
} else if (col == 1) {
|
||||
snprintf(out, out_size, "%c", ctx->items[row].done ? 'x' : ' ');
|
||||
} else if (col == 2) {
|
||||
snprintf(out, out_size, "%s", ctx->items[row].text ? ctx->items[row].text : "");
|
||||
}
|
||||
}
|
||||
|
||||
void menu_todo(void) {
|
||||
static const TuiMenuItem ACTION_ITEMS[] = {
|
||||
{NT_HK("A", "dd item"), 'a'},
|
||||
{NT_HK("C", "omplete/toggle selected item"), 'c'},
|
||||
{NT_HK("D", "elete selected item"), 'd'},
|
||||
{NT_HK("R", "eorder (swap two)"), 'r'},
|
||||
{NT_HK("P", "ost/save changes"), 'p'},
|
||||
{"E" NT_HK("x", "it without saving"), 'x'},
|
||||
};
|
||||
|
||||
todo_item_t *items = NULL;
|
||||
int count = 0;
|
||||
char input[32];
|
||||
int selected = -1;
|
||||
TuiMenuState action_state = {0};
|
||||
|
||||
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 - Add item");
|
||||
tui_print("c - Complete/toggle item");
|
||||
tui_print("d - Delete item");
|
||||
tui_print("r - Reorder (swap two)");
|
||||
tui_print("p - Post/save changes");
|
||||
tui_print("x - Exit without saving");
|
||||
TuiFrame frame = nt_frame("> Main Menu > Todo");
|
||||
TuiStatus status = nt_status();
|
||||
TuiColumn cols[] = {
|
||||
{"#", 4, 1},
|
||||
{"Done", 6, 0},
|
||||
{"Task", 0, 0},
|
||||
};
|
||||
todo_table_ctx_t ctx;
|
||||
TuiTable table;
|
||||
TuiMenu menu = {ACTION_ITEMS, 6};
|
||||
int row;
|
||||
int action;
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
ctx.items = items;
|
||||
ctx.count = count;
|
||||
|
||||
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
|
||||
table.columns = cols;
|
||||
table.column_count = (int)(sizeof(cols) / sizeof(cols[0]));
|
||||
table.row_count = count;
|
||||
table.user_data = &ctx;
|
||||
table.get_cell = todo_table_get_cell;
|
||||
table.is_default = NULL;
|
||||
table.prefix_len = NULL;
|
||||
|
||||
row = tuin_table_run(&frame, &table, &status);
|
||||
if (row >= 0 && row < count) {
|
||||
selected = row;
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
nt_print_reset();
|
||||
nt_print("TODO LIST");
|
||||
nt_print("(empty)");
|
||||
}
|
||||
|
||||
action = tuin_menu_run(&frame, &menu, &status, &action_state);
|
||||
if (action < 0 || action == 5) {
|
||||
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];
|
||||
if (action == 0) {
|
||||
char input[1024];
|
||||
input[0] = '\0';
|
||||
if (tuin_prompt("new item >", "", input, sizeof(input)) == 0 && input[0] != '\0') {
|
||||
if (todo_push_item(&items, &count, input, 0) != 0) {
|
||||
tuin_notice("Failed to add item.");
|
||||
}
|
||||
}
|
||||
} else if (action == 1) {
|
||||
if (selected < 0 || selected >= count) {
|
||||
tuin_notice("Select an item first.");
|
||||
continue;
|
||||
}
|
||||
items[selected].done = !items[selected].done;
|
||||
} else if (action == 2) {
|
||||
int i;
|
||||
if (selected < 0 || selected >= count) {
|
||||
tuin_notice("Select an item first.");
|
||||
continue;
|
||||
}
|
||||
if (tuin_confirm("Delete selected item?") != 1) {
|
||||
nt_log("Delete canceled.");
|
||||
continue;
|
||||
}
|
||||
free(items[selected].text);
|
||||
for (i = selected; i < (count - 1); i++) {
|
||||
items[i] = items[i + 1];
|
||||
}
|
||||
count--;
|
||||
if (selected >= count) {
|
||||
selected = count - 1;
|
||||
}
|
||||
} else if (action == 3) {
|
||||
char input_a[64];
|
||||
char input_b[64];
|
||||
int a;
|
||||
int b;
|
||||
todo_item_t tmp;
|
||||
|
||||
if (!items || count < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
input_a[0] = '\0';
|
||||
input_b[0] = '\0';
|
||||
(void)tuin_prompt("first number >", "", input_a, sizeof(input_a));
|
||||
(void)tuin_prompt("second number >", "", input_b, 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) {
|
||||
tuin_notice("Invalid numbers.");
|
||||
continue;
|
||||
}
|
||||
|
||||
tmp = items[a];
|
||||
items[a] = items[b];
|
||||
items[b] = tmp;
|
||||
} else if (action == 4) {
|
||||
(void)todo_publish(items, count);
|
||||
tui_get_line(">", hold, (int)sizeof(hold));
|
||||
nt_log("Todo publish attempted.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,20 +8,31 @@
|
||||
#include <string.h>
|
||||
|
||||
void menu_tweet(void) {
|
||||
TuiFrame frame = nt_frame("> Main Menu > Tweet");
|
||||
TuiStatus status = nt_status();
|
||||
char text[4096];
|
||||
int posted;
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("TWEET\n");
|
||||
tuin_render_header(&frame);
|
||||
nt_print_reset();
|
||||
tuin_render_footer(&status);
|
||||
|
||||
tui_get_line("tweet >", text, (int)sizeof(text));
|
||||
if (text[0] == '\0') {
|
||||
nt_log("Compose a short post.");
|
||||
nt_log("");
|
||||
|
||||
if (tuin_prompt("tweet", "", text, sizeof(text)) != 0 || text[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tuin_confirm("Publish this tweet?") != 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
posted = publish_signed_event_with_report("Tweet", 1, text, NULL, 8000);
|
||||
|
||||
if (posted < 0) {
|
||||
tui_print("Tweet publish failed.");
|
||||
tuin_notice("Tweet publish failed.");
|
||||
} else {
|
||||
nt_log("Tweet published.");
|
||||
}
|
||||
tui_get_line(">", text, (int)sizeof(text));
|
||||
}
|
||||
|
||||
116
src/menu_write.c
116
src/menu_write.c
@@ -9,6 +9,19 @@
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
typedef struct {
|
||||
char *content;
|
||||
} NtEditorSpawnCtx;
|
||||
|
||||
static int nt_editor_spawn(void *user) {
|
||||
NtEditorSpawnCtx *ctx = (NtEditorSpawnCtx *)user;
|
||||
if (!ctx) {
|
||||
return -1;
|
||||
}
|
||||
ctx->content = editor_launch(NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static cJSON *write_make_blog_tags(const char *d_tag_value, const char *title_opt) {
|
||||
cJSON *tags = cJSON_CreateArray();
|
||||
cJSON *d_tag;
|
||||
@@ -39,93 +52,126 @@ static cJSON *write_make_blog_tags(const char *d_tag_value, const char *title_op
|
||||
}
|
||||
|
||||
void menu_write(void) {
|
||||
static const TuiMenuItem WRITE_ITEMS[] = {
|
||||
{NT_HK("T", "weet it"), 't'},
|
||||
{NT_HK("B", "log post (kind 30023)"), 'b'},
|
||||
{NT_HK("D", "iary entry (kind 30024)"), 'd'},
|
||||
{NT_HK("X", "Exit without saving"), 'x'},
|
||||
};
|
||||
TuiFrame frame = nt_frame("> Main Menu > Write");
|
||||
TuiStatus status = nt_status();
|
||||
TuiMenu menu = {WRITE_ITEMS, 4};
|
||||
TuiMenuState menu_state = {0};
|
||||
NtEditorSpawnCtx ctx = {0};
|
||||
char *content;
|
||||
char input[64];
|
||||
int idx;
|
||||
|
||||
tui_clear_screen();
|
||||
tui_print("WRITE\n");
|
||||
tui_print("Launching external editor...");
|
||||
tuin_render_header(&frame);
|
||||
nt_print_reset();
|
||||
tuin_render_footer(&status);
|
||||
nt_log("Launching external editor...");
|
||||
|
||||
content = editor_launch(NULL);
|
||||
if (!content) {
|
||||
tui_print("No content created.");
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
if (nt_run_external_editor(nt_editor_spawn, &ctx) != 0) {
|
||||
tuin_notice("Failed to launch external editor.");
|
||||
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("^_X^: discard");
|
||||
content = ctx.content;
|
||||
if (!content) {
|
||||
tuin_notice("No content created.");
|
||||
return;
|
||||
}
|
||||
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
nt_print_reset();
|
||||
nt_log("Content length: %d bytes", (int)strlen(content));
|
||||
|
||||
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.");
|
||||
idx = tuin_menu_run(&frame, &menu, &status, &menu_state);
|
||||
|
||||
if (idx == 0) {
|
||||
int posted;
|
||||
if (tuin_confirm("Publish as tweet?") == 1) {
|
||||
posted = publish_signed_event_with_report("Tweet", 1, content, NULL, 8000);
|
||||
if (posted < 0) {
|
||||
tuin_notice("Tweet publish failed.");
|
||||
} else {
|
||||
nt_log("Tweet published.");
|
||||
}
|
||||
}
|
||||
} else if (input[0] == 'b' || input[0] == 'B') {
|
||||
} else if (idx == 1) {
|
||||
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));
|
||||
if (tuin_prompt("title", "", title, sizeof(title)) != 0) {
|
||||
free(content);
|
||||
return;
|
||||
}
|
||||
if (tuin_prompt("slug (d tag) [empty uses title]", "", slug, sizeof(slug)) != 0) {
|
||||
free(content);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tuin_confirm("Publish as blog post?") != 1) {
|
||||
free(content);
|
||||
return;
|
||||
}
|
||||
|
||||
d_val = (slug[0] != '\0') ? slug : title;
|
||||
tags = write_make_blog_tags(d_val, title);
|
||||
|
||||
if (!tags) {
|
||||
tui_print("Failed to build blog tags.");
|
||||
tuin_notice("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.");
|
||||
tuin_notice("Blog publish failed.");
|
||||
} else {
|
||||
nt_log("Blog post published.");
|
||||
}
|
||||
}
|
||||
} else if (input[0] == 'd' || input[0] == 'D') {
|
||||
} else if (idx == 2) {
|
||||
char date_d[16];
|
||||
time_t now = time(NULL);
|
||||
struct tm tmv;
|
||||
cJSON *tags;
|
||||
int posted;
|
||||
|
||||
if (tuin_confirm("Publish as diary entry?") != 1) {
|
||||
free(content);
|
||||
return;
|
||||
}
|
||||
|
||||
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));
|
||||
tuin_notice("Failed to get local date.");
|
||||
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));
|
||||
tuin_notice("Failed to format diary date.");
|
||||
return;
|
||||
}
|
||||
|
||||
tags = write_make_blog_tags(date_d, NULL);
|
||||
if (!tags) {
|
||||
tui_print("Failed to build diary tags.");
|
||||
tuin_notice("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.");
|
||||
tuin_notice("Diary publish failed.");
|
||||
} else {
|
||||
nt_log("Diary entry published.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tui_print("Discarded.");
|
||||
nt_log("Discarded.");
|
||||
}
|
||||
|
||||
free(content);
|
||||
tui_get_line(">", input, (int)sizeof(input));
|
||||
}
|
||||
|
||||
118
src/net.c
118
src/net.c
@@ -2,11 +2,13 @@
|
||||
|
||||
#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 "../resources/nostr_core_lib/nostr_core/nostr_core.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <errno.h>
|
||||
@@ -18,7 +20,7 @@
|
||||
#include <unistd.h>
|
||||
|
||||
#define NT_SUB_ID "sub1"
|
||||
#define NT_WS_BUFFER_SIZE 65536
|
||||
#define NT_WS_BUFFER_SIZE 262144
|
||||
|
||||
static long long nt_now_ms(void) {
|
||||
struct timeval tv;
|
||||
@@ -244,7 +246,7 @@ static int nt_send_nip42_auth(nostr_ws_client_t *client,
|
||||
|
||||
*auth_event_id_out = NULL;
|
||||
|
||||
if (nostr_hex_to_bytes(g_state.nsec_hex, private_key, sizeof(private_key)) != 0) {
|
||||
if (signer_get_local_private_key(private_key) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -528,7 +530,7 @@ void nt_publish_results_free(nt_publish_result_t *results, int result_count) {
|
||||
}
|
||||
|
||||
/* Query one relay and append deduplicated EVENT payloads until EOSE/timeout. */
|
||||
static int nt_query_one_relay(const char *relay_url,
|
||||
static __attribute__((unused)) int nt_query_one_relay(const char *relay_url,
|
||||
cJSON *filter,
|
||||
int timeout_ms,
|
||||
int verbose,
|
||||
@@ -684,13 +686,12 @@ int nt_query_sync_verbose(const char *filter_json,
|
||||
char ***events_out,
|
||||
int *event_count) {
|
||||
cJSON *filter = NULL;
|
||||
nostr_relay_pool_t *pool = NULL;
|
||||
cJSON **pool_events = NULL;
|
||||
int pool_event_count = 0;
|
||||
char **events = NULL;
|
||||
char **seen_ids = NULL;
|
||||
int events_n = 0;
|
||||
int events_cap = 0;
|
||||
int seen_n = 0;
|
||||
int seen_cap = 0;
|
||||
int success_relays = 0;
|
||||
int i;
|
||||
|
||||
if (!events_out || !event_count || !filter_json || !relay_urls || relay_count <= 0) {
|
||||
@@ -707,59 +708,62 @@ int nt_query_sync_verbose(const char *filter_json,
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (i = 0; i < relay_count; i++) {
|
||||
const char *relay = relay_urls[i];
|
||||
int rc;
|
||||
const char *reason = "unknown";
|
||||
|
||||
if (!relay || relay[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (verbose && phase_label) {
|
||||
fprintf(stderr, "%s: attempting relay %d/%d %s\n", phase_label, i + 1, relay_count, relay);
|
||||
}
|
||||
|
||||
rc = nt_query_one_relay(relay,
|
||||
filter,
|
||||
timeout_ms,
|
||||
verbose,
|
||||
phase_label,
|
||||
&events,
|
||||
&events_n,
|
||||
&events_cap,
|
||||
&seen_ids,
|
||||
&seen_n,
|
||||
&seen_cap,
|
||||
&reason);
|
||||
if (rc < 0) {
|
||||
if (verbose && phase_label) {
|
||||
fprintf(stderr, "%s: failed %s (%s)\n", phase_label, relay, reason ? reason : "error");
|
||||
}
|
||||
nt_free_string_array(events, events_n);
|
||||
nt_free_string_array(seen_ids, seen_n);
|
||||
cJSON_Delete(filter);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (rc > 0) {
|
||||
success_relays++;
|
||||
if (verbose && phase_label) {
|
||||
fprintf(stderr, "%s: connected %s (%s)\n", phase_label, relay, reason ? reason : "ok");
|
||||
}
|
||||
} else if (verbose && phase_label) {
|
||||
fprintf(stderr, "%s: failed %s (%s)\n", phase_label, relay, reason ? reason : "failed");
|
||||
}
|
||||
}
|
||||
|
||||
nt_free_string_array(seen_ids, seen_n);
|
||||
cJSON_Delete(filter);
|
||||
|
||||
if (success_relays == 0) {
|
||||
nt_free_string_array(events, events_n);
|
||||
pool = nostr_relay_pool_create(NULL);
|
||||
if (!pool) {
|
||||
cJSON_Delete(filter);
|
||||
fprintf(stderr, "query: failed to create relay pool\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (verbose && phase_label) {
|
||||
fprintf(stderr,
|
||||
"%s: querying %d relays via nostr_core relay pool\n",
|
||||
phase_label,
|
||||
relay_count);
|
||||
}
|
||||
|
||||
pool_events = nostr_relay_pool_query_sync(pool,
|
||||
relay_urls,
|
||||
relay_count,
|
||||
filter,
|
||||
&pool_event_count,
|
||||
timeout_ms);
|
||||
|
||||
cJSON_Delete(filter);
|
||||
filter = NULL;
|
||||
|
||||
if (!pool_events || pool_event_count <= 0) {
|
||||
nostr_relay_pool_destroy(pool);
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (i = 0; i < pool_event_count; i++) {
|
||||
char *event_str = cJSON_PrintUnformatted(pool_events[i]);
|
||||
if (!event_str || nt_push_string(&events, &events_n, &events_cap, event_str) != 0) {
|
||||
free(event_str);
|
||||
nt_free_string_array(events, events_n);
|
||||
for (i = 0; i < pool_event_count; i++) {
|
||||
cJSON_Delete(pool_events[i]);
|
||||
}
|
||||
free(pool_events);
|
||||
nostr_relay_pool_destroy(pool);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < pool_event_count; i++) {
|
||||
cJSON_Delete(pool_events[i]);
|
||||
}
|
||||
free(pool_events);
|
||||
|
||||
if (verbose && phase_label) {
|
||||
fprintf(stderr,
|
||||
"%s: relay pool collected %d unique events\n",
|
||||
phase_label,
|
||||
events_n);
|
||||
}
|
||||
|
||||
nostr_relay_pool_destroy(pool);
|
||||
*events_out = events;
|
||||
*event_count = events_n;
|
||||
return 0;
|
||||
|
||||
109
src/nsigner_auth.c
Normal file
109
src/nsigner_auth.c
Normal file
@@ -0,0 +1,109 @@
|
||||
#include "nsigner_auth.h"
|
||||
|
||||
#include "../resources/nostr_core_lib/nostr_core/nip001.h"
|
||||
#include "../resources/nostr_core_lib/nostr_core/utils.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#define NT_NSIGNER_AUTH_EVENT_KIND 27235
|
||||
|
||||
static cJSON *nt_nsigner_auth_build_tag_pair(const char *name, const char *value) {
|
||||
cJSON *pair;
|
||||
|
||||
if (!name || !value) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pair = cJSON_CreateArray();
|
||||
if (!pair) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(pair, cJSON_CreateString(name));
|
||||
cJSON_AddItemToArray(pair, cJSON_CreateString(value));
|
||||
return pair;
|
||||
}
|
||||
|
||||
static int nt_nsigner_auth_compute_params_hash_hex(const cJSON *params, char out_hex_65[65]) {
|
||||
char *params_json = NULL;
|
||||
unsigned char hash[32];
|
||||
int rc = -1;
|
||||
|
||||
if (!out_hex_65) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (params) {
|
||||
params_json = cJSON_PrintUnformatted((cJSON *)params);
|
||||
} else {
|
||||
params_json = strdup("null");
|
||||
}
|
||||
|
||||
if (!params_json) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (nostr_sha256((const unsigned char *)params_json, strlen(params_json), hash) != 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
nostr_bytes_to_hex(hash, sizeof(hash), out_hex_65);
|
||||
out_hex_65[64] = '\0';
|
||||
rc = 0;
|
||||
|
||||
cleanup:
|
||||
free(params_json);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int nt_nsigner_auth_build(const char *request_id,
|
||||
const char *method,
|
||||
const cJSON *params,
|
||||
const nt_nsigner_auth_ctx_t *auth_ctx,
|
||||
time_t created_at,
|
||||
cJSON **out_auth) {
|
||||
char body_hash_hex[65];
|
||||
cJSON *tags = NULL;
|
||||
cJSON *auth_event = NULL;
|
||||
|
||||
if (!request_id || request_id[0] == '\0' ||
|
||||
!method || method[0] == '\0' ||
|
||||
!auth_ctx || !auth_ctx->enabled ||
|
||||
!out_auth) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_auth = NULL;
|
||||
|
||||
if (nt_nsigner_auth_compute_params_hash_hex(params, body_hash_hex) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
tags = cJSON_CreateArray();
|
||||
if (!tags) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(tags, nt_nsigner_auth_build_tag_pair("nsigner_rpc", request_id));
|
||||
cJSON_AddItemToArray(tags, nt_nsigner_auth_build_tag_pair("nsigner_method", method));
|
||||
cJSON_AddItemToArray(tags, nt_nsigner_auth_build_tag_pair("nsigner_body_hash", body_hash_hex));
|
||||
|
||||
if (created_at <= 0) {
|
||||
created_at = time(NULL);
|
||||
}
|
||||
|
||||
auth_event = nostr_create_and_sign_event(NT_NSIGNER_AUTH_EVENT_KIND,
|
||||
auth_ctx->label,
|
||||
tags,
|
||||
auth_ctx->privkey,
|
||||
created_at);
|
||||
if (!auth_event) {
|
||||
cJSON_Delete(tags);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_auth = auth_event;
|
||||
return 0;
|
||||
}
|
||||
1337
src/nsigner_client.c
Normal file
1337
src/nsigner_client.c
Normal file
File diff suppressed because it is too large
Load Diff
208
src/nt_tui_adapter.c
Normal file
208
src/nt_tui_adapter.c
Normal file
@@ -0,0 +1,208 @@
|
||||
#include "tui.h"
|
||||
|
||||
#include <ncurses.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#define NT_LOG_MAX 512
|
||||
|
||||
typedef struct {
|
||||
char text[512];
|
||||
} NtLogEntry;
|
||||
|
||||
static int g_body_row = 0;
|
||||
static char g_app_name[128] = "NOSTR TERMINAL";
|
||||
static char g_app_version[64] = "";
|
||||
static char g_user[256] = "";
|
||||
static NtLogEntry g_log_entries[NT_LOG_MAX];
|
||||
static int g_log_head = 0;
|
||||
static int g_log_count = 0;
|
||||
|
||||
static WINDOW *nt_body_window(void) {
|
||||
return (WINDOW *)tuin_body_window();
|
||||
}
|
||||
|
||||
void nt_print(const char *fmt, ...) {
|
||||
WINDOW *body = nt_body_window();
|
||||
char line[8192];
|
||||
va_list ap;
|
||||
int h = 0;
|
||||
int w = 0;
|
||||
|
||||
if (!body || !fmt) {
|
||||
return;
|
||||
}
|
||||
|
||||
getmaxyx(body, h, w);
|
||||
(void)w;
|
||||
if (h <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_body_row < 0) {
|
||||
g_body_row = 0;
|
||||
}
|
||||
if (g_body_row >= h) {
|
||||
g_body_row = h - 1;
|
||||
}
|
||||
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(line, sizeof(line), fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
mvwprintw(body, g_body_row, 0, "%s", line);
|
||||
g_body_row++;
|
||||
if (g_body_row >= h) {
|
||||
g_body_row = h - 1;
|
||||
}
|
||||
|
||||
wnoutrefresh(body);
|
||||
doupdate();
|
||||
}
|
||||
|
||||
void nt_print_reset(void) {
|
||||
WINDOW *body = nt_body_window();
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
werase(body);
|
||||
g_body_row = 0;
|
||||
wnoutrefresh(body);
|
||||
doupdate();
|
||||
}
|
||||
|
||||
int nt_run_external_editor(int (*spawn)(void *user), void *user) {
|
||||
int result = -1;
|
||||
|
||||
if (!spawn) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
tuin_cleanup();
|
||||
result = spawn(user);
|
||||
tuin_init();
|
||||
return result;
|
||||
}
|
||||
|
||||
static const char *nt_log_latest(void) {
|
||||
int idx;
|
||||
|
||||
if (g_log_count <= 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
idx = (g_log_head - 1 + NT_LOG_MAX) % NT_LOG_MAX;
|
||||
return g_log_entries[idx].text;
|
||||
}
|
||||
|
||||
TuiFrame nt_frame(const char *breadcrumb) {
|
||||
static char composed_title[512];
|
||||
TuiFrame frame;
|
||||
|
||||
if (g_user[0] != '\0') {
|
||||
snprintf(composed_title, sizeof(composed_title), "%s - %s - %s", g_app_name, g_app_version, g_user);
|
||||
} else {
|
||||
snprintf(composed_title, sizeof(composed_title), "%s - %s", g_app_name, g_app_version);
|
||||
}
|
||||
|
||||
frame.app_name = composed_title;
|
||||
frame.app_version = "";
|
||||
frame.breadcrumb = breadcrumb;
|
||||
return frame;
|
||||
}
|
||||
|
||||
TuiStatus nt_status(void) {
|
||||
TuiStatus status;
|
||||
status.text = nt_log_latest();
|
||||
return status;
|
||||
}
|
||||
|
||||
void nt_set_app_info(const char *name, const char *version) {
|
||||
snprintf(g_app_name, sizeof(g_app_name), "%s", (name && name[0] != '\0') ? name : "NOSTR TERMINAL");
|
||||
snprintf(g_app_version, sizeof(g_app_version), "%s", (version && version[0] != '\0') ? version : "");
|
||||
}
|
||||
|
||||
void nt_set_window_title(const char *title) {
|
||||
if (!title) {
|
||||
return;
|
||||
}
|
||||
fprintf(stderr, "\x1b]2;%s\x07", title);
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
void nt_set_user(const char *user) {
|
||||
snprintf(g_user, sizeof(g_user), "%s", (user && user[0] != '\0') ? user : "");
|
||||
}
|
||||
|
||||
void nt_log(const char *fmt, ...) {
|
||||
char msg[384];
|
||||
char ts[16];
|
||||
time_t now;
|
||||
struct tm tm_now;
|
||||
va_list ap;
|
||||
int idx;
|
||||
|
||||
if (!fmt) {
|
||||
return;
|
||||
}
|
||||
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(msg, sizeof(msg), fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
now = time(NULL);
|
||||
if (localtime_r(&now, &tm_now) != NULL) {
|
||||
snprintf(ts, sizeof(ts), "[%02d:%02d:%02d]",
|
||||
tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec);
|
||||
} else {
|
||||
snprintf(ts, sizeof(ts), "[--:--:--]");
|
||||
}
|
||||
|
||||
idx = g_log_head;
|
||||
snprintf(g_log_entries[idx].text, sizeof(g_log_entries[idx].text), "%s %s", ts, msg);
|
||||
|
||||
g_log_head = (g_log_head + 1) % NT_LOG_MAX;
|
||||
if (g_log_count < NT_LOG_MAX) {
|
||||
g_log_count++;
|
||||
}
|
||||
}
|
||||
|
||||
void nt_log_show(void) {
|
||||
TuiFrame frame = nt_frame("> Log");
|
||||
TuiStatus status = {"Up/Down PgUp/PgDn Esc to return"};
|
||||
static char log_text[NT_LOG_MAX * 520];
|
||||
int pos = 0;
|
||||
int i;
|
||||
|
||||
log_text[0] = '\0';
|
||||
for (i = 0; i < g_log_count; ++i) {
|
||||
int idx = (g_log_head - g_log_count + i + NT_LOG_MAX) % NT_LOG_MAX;
|
||||
int n = snprintf(log_text + pos, sizeof(log_text) - (size_t)pos, "%s%s",
|
||||
g_log_entries[idx].text,
|
||||
(i == g_log_count - 1) ? "" : "\n");
|
||||
if (n < 0) {
|
||||
break;
|
||||
}
|
||||
if ((size_t)n >= sizeof(log_text) - (size_t)pos) {
|
||||
pos = (int)sizeof(log_text) - 1;
|
||||
break;
|
||||
}
|
||||
pos += n;
|
||||
}
|
||||
|
||||
tuin_pager_run(&frame, log_text, &status);
|
||||
}
|
||||
|
||||
void nt_log_clear(void) {
|
||||
g_log_head = 0;
|
||||
g_log_count = 0;
|
||||
}
|
||||
|
||||
void nt_pager_show_text(const char *breadcrumb, const char *text) {
|
||||
TuiFrame frame = nt_frame(breadcrumb);
|
||||
TuiStatus status = {"Up/Down PgUp/PgDn Esc to return"};
|
||||
tuin_pager_run(&frame, text ? text : "", &status);
|
||||
}
|
||||
|
||||
@@ -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,40 +26,17 @@ 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) {
|
||||
tui_print("%s: failed to serialize event.", label);
|
||||
if (signer_create_and_sign(kind, content ? content : "", tags, time(NULL), &event_json) != 0 ||
|
||||
!event_json) {
|
||||
nt_log("%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);
|
||||
nt_log("%s (kind %d)", label, kind);
|
||||
nt_log("Event JSON:");
|
||||
nt_log("%s", event_json);
|
||||
nt_log("Posting to %d relay(s)...", relay_count);
|
||||
|
||||
accepted_count = nt_publish_detailed(event_json,
|
||||
write_relays,
|
||||
@@ -76,7 +48,7 @@ int publish_signed_event_with_report(const char *label,
|
||||
free(event_json);
|
||||
|
||||
if (result_count <= 0) {
|
||||
tui_print("No relay attempts were made.");
|
||||
nt_log("No relay attempts were made.");
|
||||
return accepted_count;
|
||||
}
|
||||
|
||||
@@ -85,14 +57,14 @@ int publish_signed_event_with_report(const char *label,
|
||||
const char *response = results[i].response ? results[i].response : "(no response)";
|
||||
|
||||
if (results[i].posted) {
|
||||
tui_print("[OK] %s", relay);
|
||||
nt_log("[OK] %s", relay);
|
||||
} else {
|
||||
tui_print("[FAIL] %s", relay);
|
||||
tui_print(" %s", response);
|
||||
nt_log("[FAIL] %s", relay);
|
||||
nt_log(" %s", response);
|
||||
}
|
||||
}
|
||||
|
||||
tui_print("Accepted by %d/%d relay(s).", accepted_count, result_count);
|
||||
nt_log("Accepted by %d/%d relay(s).", accepted_count, result_count);
|
||||
nt_publish_results_free(results, result_count);
|
||||
return accepted_count;
|
||||
}
|
||||
|
||||
722
src/signer.c
Normal file
722
src/signer.c
Normal file
@@ -0,0 +1,722 @@
|
||||
#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},
|
||||
.url_session_fd = -1,
|
||||
.selector = {
|
||||
.has_nostr_index = 1,
|
||||
.nostr_index = 0,
|
||||
.role = {0}
|
||||
},
|
||||
.url_auth = {
|
||||
.enabled = 0,
|
||||
.privkey = {0},
|
||||
.label = {0}
|
||||
}
|
||||
};
|
||||
|
||||
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_set_selector(int has_index, int index, const char *role) {
|
||||
g_signer.selector.has_nostr_index = has_index ? 1 : 0;
|
||||
g_signer.selector.nostr_index = (index < 0) ? 0 : index;
|
||||
if (role && role[0] != '\0') {
|
||||
snprintf(g_signer.selector.role, sizeof(g_signer.selector.role), "%s", role);
|
||||
} else {
|
||||
g_signer.selector.role[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
void signer_init(void) {
|
||||
nt_nsigner_selector_t sel = nsigner_selector_default();
|
||||
|
||||
if (g_signer.url_session_fd >= 0) {
|
||||
nsigner_session_close(g_signer.url_session_fd);
|
||||
g_signer.url_session_fd = -1;
|
||||
}
|
||||
|
||||
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';
|
||||
signer_set_selector(sel.has_nostr_index, sel.nostr_index, sel.role);
|
||||
g_signer.url_auth.enabled = 0;
|
||||
memset(g_signer.url_auth.privkey, 0, sizeof(g_signer.url_auth.privkey));
|
||||
g_signer.url_auth.label[0] = '\0';
|
||||
}
|
||||
|
||||
void signer_shutdown(void) {
|
||||
memset(g_signer.url_auth.privkey, 0, sizeof(g_signer.url_auth.privkey));
|
||||
signer_init();
|
||||
}
|
||||
|
||||
int signer_init_local(void) {
|
||||
signer_init();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int signer_init_nsigner(const char *socket_name, const nt_nsigner_selector_t *selector) {
|
||||
nt_nsigner_selector_t sel = selector ? *selector : nsigner_selector_default();
|
||||
|
||||
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';
|
||||
if (g_signer.url_session_fd >= 0) {
|
||||
nsigner_session_close(g_signer.url_session_fd);
|
||||
g_signer.url_session_fd = -1;
|
||||
}
|
||||
signer_set_selector(sel.has_nostr_index, sel.nostr_index, sel.role);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int signer_init_nsigner_qrexec(const char *target_qube, const char *service_name, const nt_nsigner_selector_t *selector) {
|
||||
nt_nsigner_selector_t sel = selector ? *selector : nsigner_selector_default();
|
||||
|
||||
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';
|
||||
if (g_signer.url_session_fd >= 0) {
|
||||
nsigner_session_close(g_signer.url_session_fd);
|
||||
g_signer.url_session_fd = -1;
|
||||
}
|
||||
signer_set_selector(sel.has_nostr_index, sel.nostr_index, sel.role);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int signer_init_nsigner_url_with_session_auth(const char *endpoint_url,
|
||||
const nt_nsigner_selector_t *selector,
|
||||
int session_fd,
|
||||
const nt_nsigner_auth_ctx_t *auth_or_null) {
|
||||
nt_nsigner_selector_t sel = selector ? *selector : nsigner_selector_default();
|
||||
|
||||
if (!endpoint_url || endpoint_url[0] == '\0') {
|
||||
if (session_fd >= 0) {
|
||||
nsigner_session_close(session_fd);
|
||||
}
|
||||
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);
|
||||
|
||||
if (g_signer.url_session_fd >= 0 && g_signer.url_session_fd != session_fd) {
|
||||
nsigner_session_close(g_signer.url_session_fd);
|
||||
}
|
||||
g_signer.url_session_fd = session_fd;
|
||||
|
||||
if (g_signer.url_session_fd < 0) {
|
||||
g_signer.url_session_fd = nsigner_session_open_url(g_signer.endpoint_url);
|
||||
}
|
||||
|
||||
signer_set_selector(sel.has_nostr_index, sel.nostr_index, sel.role);
|
||||
|
||||
g_signer.url_auth.enabled = 0;
|
||||
memset(g_signer.url_auth.privkey, 0, sizeof(g_signer.url_auth.privkey));
|
||||
g_signer.url_auth.label[0] = '\0';
|
||||
|
||||
if (auth_or_null && auth_or_null->enabled) {
|
||||
g_signer.url_auth.enabled = 1;
|
||||
memcpy(g_signer.url_auth.privkey, auth_or_null->privkey, sizeof(g_signer.url_auth.privkey));
|
||||
snprintf(g_signer.url_auth.label, sizeof(g_signer.url_auth.label), "%s", auth_or_null->label);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int signer_init_nsigner_url_with_session(const char *endpoint_url,
|
||||
const nt_nsigner_selector_t *selector,
|
||||
int session_fd) {
|
||||
return signer_init_nsigner_url_with_session_auth(endpoint_url, selector, session_fd, NULL);
|
||||
}
|
||||
|
||||
int signer_init_nsigner_url(const char *endpoint_url, const nt_nsigner_selector_t *selector) {
|
||||
return signer_init_nsigner_url_with_session_auth(endpoint_url, selector, -1, NULL);
|
||||
}
|
||||
|
||||
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.selector,
|
||||
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.selector,
|
||||
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_session_sign_event(&g_signer.url_session_fd,
|
||||
g_signer.endpoint_url,
|
||||
&g_signer.selector,
|
||||
g_signer.url_auth.enabled ? &g_signer.url_auth : NULL,
|
||||
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.selector,
|
||||
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.selector,
|
||||
unsigned_event_json,
|
||||
signed_event_json_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
|
||||
return nsigner_session_sign_event(&g_signer.url_session_fd,
|
||||
g_signer.endpoint_url,
|
||||
&g_signer.selector,
|
||||
g_signer.url_auth.enabled ? &g_signer.url_auth : NULL,
|
||||
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.selector,
|
||||
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.selector,
|
||||
peer_pub_hex,
|
||||
plaintext,
|
||||
cipher_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
|
||||
return nsigner_session_nip04_encrypt(&g_signer.url_session_fd,
|
||||
g_signer.endpoint_url,
|
||||
&g_signer.selector,
|
||||
g_signer.url_auth.enabled ? &g_signer.url_auth : NULL,
|
||||
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.selector,
|
||||
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.selector,
|
||||
peer_pub_hex,
|
||||
ciphertext,
|
||||
plain_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
|
||||
return nsigner_session_nip04_decrypt(&g_signer.url_session_fd,
|
||||
g_signer.endpoint_url,
|
||||
&g_signer.selector,
|
||||
g_signer.url_auth.enabled ? &g_signer.url_auth : NULL,
|
||||
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.selector,
|
||||
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.selector,
|
||||
peer_pub_hex,
|
||||
plaintext,
|
||||
cipher_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
|
||||
return nsigner_session_nip44_encrypt(&g_signer.url_session_fd,
|
||||
g_signer.endpoint_url,
|
||||
&g_signer.selector,
|
||||
g_signer.url_auth.enabled ? &g_signer.url_auth : NULL,
|
||||
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.selector,
|
||||
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.selector,
|
||||
peer_pub_hex,
|
||||
ciphertext,
|
||||
plain_out);
|
||||
}
|
||||
|
||||
if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
|
||||
return nsigner_session_nip44_decrypt(&g_signer.url_session_fd,
|
||||
g_signer.endpoint_url,
|
||||
&g_signer.selector,
|
||||
g_signer.url_auth.enabled ? &g_signer.url_auth : NULL,
|
||||
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) {
|
||||
|
||||
306
src/tui.c
306
src/tui.c
@@ -1,6 +1,8 @@
|
||||
#include "tui.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
@@ -13,6 +15,36 @@ static struct termios g_original_termios;
|
||||
static bool g_termios_saved = false;
|
||||
static bool g_raw_enabled = false;
|
||||
|
||||
static volatile sig_atomic_t g_resize_pending = 0;
|
||||
|
||||
static char g_app_title[256] = "NOSTR TERMINAL";
|
||||
static int g_last_menu_start_col = 0;
|
||||
static int g_frame_line_count = 0;
|
||||
static bool g_frame_active = false;
|
||||
|
||||
static int tui_visible_len_no_markup(const char *s) {
|
||||
int len = 0;
|
||||
|
||||
if (!s) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (size_t i = 0; s[i] != '\0';) {
|
||||
if (s[i] == '^' && s[i + 1] == '_') {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (s[i] == '^' && s[i + 1] == ':') {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
len++;
|
||||
i++;
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
/* Write a string while converting ^_...^: segments to underline. */
|
||||
static void tui_write_with_hotkey_markup(const char *s) {
|
||||
bool in_hotkey = false;
|
||||
@@ -29,6 +61,11 @@ static void tui_write_with_hotkey_markup(const char *s) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_hotkey && s[i] == '^' && s[i + 1] == '^') {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_hotkey && s[i] == '^' && s[i + 1] == ':') {
|
||||
fputs("\x1b[0m", stdout); /* reset */
|
||||
in_hotkey = false;
|
||||
@@ -45,6 +82,86 @@ static void tui_write_with_hotkey_markup(const char *s) {
|
||||
}
|
||||
}
|
||||
|
||||
static int tui_count_newlines(const char *s) {
|
||||
int count = 0;
|
||||
|
||||
if (!s) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (size_t i = 0; s[i] != '\0'; i++) {
|
||||
if (s[i] == '\n') {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
static void tui_track_lines(int lines) {
|
||||
if (g_frame_active && lines > 0) {
|
||||
g_frame_line_count += lines;
|
||||
}
|
||||
}
|
||||
|
||||
static void tui_print_repeat_char(char ch, int width) {
|
||||
if (width <= 0) {
|
||||
fputc('\n', stdout);
|
||||
tui_track_lines(1);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < width; i++) {
|
||||
fputc(ch, stdout);
|
||||
}
|
||||
fputc('\n', stdout);
|
||||
tui_track_lines(1);
|
||||
}
|
||||
|
||||
static void tui_print_centered_text(const char *text, int width) {
|
||||
int len = 0;
|
||||
|
||||
if (!text) {
|
||||
fputc('\n', stdout);
|
||||
tui_track_lines(1);
|
||||
return;
|
||||
}
|
||||
|
||||
len = tui_visible_len_no_markup(text);
|
||||
if (width <= 0) {
|
||||
fputc('\n', stdout);
|
||||
tui_track_lines(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (len >= width) {
|
||||
tui_write_with_hotkey_markup(text);
|
||||
fputc('\n', stdout);
|
||||
tui_track_lines(1);
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
int left = (width - len) / 2;
|
||||
int right = width - len - left;
|
||||
|
||||
for (int i = 0; i < left; i++) {
|
||||
fputc(' ', stdout);
|
||||
}
|
||||
tui_write_with_hotkey_markup(text);
|
||||
for (int i = 0; i < right; i++) {
|
||||
fputc(' ', stdout);
|
||||
}
|
||||
fputc('\n', stdout);
|
||||
tui_track_lines(1);
|
||||
}
|
||||
}
|
||||
|
||||
static void tui_handle_sigwinch(int signum) {
|
||||
(void)signum;
|
||||
g_resize_pending = 1;
|
||||
}
|
||||
|
||||
void tui_init(void) {
|
||||
struct termios raw;
|
||||
|
||||
@@ -81,6 +198,24 @@ void tui_cleanup(void) {
|
||||
}
|
||||
}
|
||||
|
||||
void tui_install_resize_handler(void) {
|
||||
struct sigaction sa;
|
||||
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.sa_handler = tui_handle_sigwinch;
|
||||
sa.sa_flags = SA_RESTART;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
(void)sigaction(SIGWINCH, &sa, NULL);
|
||||
}
|
||||
|
||||
int tui_consume_resize_flag(void) {
|
||||
if (g_resize_pending) {
|
||||
g_resize_pending = 0;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void tui_get_terminal_size(int *width, int *height) {
|
||||
struct winsize ws;
|
||||
int w = 80;
|
||||
@@ -103,6 +238,14 @@ void tui_get_terminal_size(int *width, int *height) {
|
||||
}
|
||||
}
|
||||
|
||||
void tui_set_app_title(const char *title) {
|
||||
if (!title || title[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(g_app_title, sizeof(g_app_title), "%s", title);
|
||||
}
|
||||
|
||||
void tui_clear_screen(void) {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
@@ -126,6 +269,152 @@ void tui_print(const char *fmt, ...) {
|
||||
tui_write_with_hotkey_markup(buffer);
|
||||
fputc('\n', stdout);
|
||||
fflush(stdout);
|
||||
|
||||
tui_track_lines(tui_count_newlines(buffer) + 1);
|
||||
}
|
||||
|
||||
void tui_print_hr(void) {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
(void)height;
|
||||
|
||||
tui_get_terminal_size(&width, &height);
|
||||
tui_print_repeat_char('=', width);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
void tui_print_centered(const char *text) {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
(void)height;
|
||||
|
||||
tui_get_terminal_size(&width, &height);
|
||||
tui_print_centered_text(text ? text : "", width);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
int tui_centered_menu_start_col(void) {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int title_len = 0;
|
||||
(void)height;
|
||||
|
||||
tui_get_terminal_size(&width, &height);
|
||||
title_len = tui_visible_len_no_markup(g_app_title);
|
||||
|
||||
if (width <= title_len) {
|
||||
g_last_menu_start_col = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_last_menu_start_col = (width - title_len) / 2;
|
||||
if (g_last_menu_start_col < 0) {
|
||||
g_last_menu_start_col = 0;
|
||||
}
|
||||
|
||||
return g_last_menu_start_col;
|
||||
}
|
||||
|
||||
void tui_render_top_frame(const tui_view_t *view) {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
(void)height;
|
||||
|
||||
tui_get_terminal_size(&width, &height);
|
||||
|
||||
tui_print_repeat_char('=', width);
|
||||
tui_print_centered_text(g_app_title, width);
|
||||
tui_print_repeat_char('=', width);
|
||||
|
||||
if (view && view->breadcrumb && view->breadcrumb[0] != '\0') {
|
||||
tui_write_with_hotkey_markup(view->breadcrumb);
|
||||
} else {
|
||||
tui_write_with_hotkey_markup("> Main Menu");
|
||||
}
|
||||
fputc('\n', stdout);
|
||||
tui_track_lines(1);
|
||||
|
||||
fputc('\n', stdout);
|
||||
fputc('\n', stdout);
|
||||
tui_track_lines(2);
|
||||
|
||||
fflush(stdout);
|
||||
|
||||
tui_centered_menu_start_col();
|
||||
}
|
||||
|
||||
void tui_print_menu_item(const char *fmt, ...) {
|
||||
char buffer[8192];
|
||||
va_list ap;
|
||||
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(buffer, sizeof(buffer), fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
for (int i = 0; i < g_last_menu_start_col; i++) {
|
||||
fputc(' ', stdout);
|
||||
}
|
||||
tui_write_with_hotkey_markup(buffer);
|
||||
fputc('\n', stdout);
|
||||
fflush(stdout);
|
||||
|
||||
tui_track_lines(tui_count_newlines(buffer) + 1);
|
||||
}
|
||||
|
||||
void tui_begin_frame(const tui_view_t *view) {
|
||||
g_frame_active = true;
|
||||
g_frame_line_count = 0;
|
||||
|
||||
tui_clear_screen();
|
||||
tui_render_top_frame(view);
|
||||
}
|
||||
|
||||
int tui_end_frame_with_prompt(const char *prompt, char *buf, int bufsize) {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int filler = 0;
|
||||
|
||||
tui_get_terminal_size(&width, &height);
|
||||
|
||||
filler = (height - 1) - g_frame_line_count;
|
||||
if (filler < 0) {
|
||||
filler = 0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < filler; i++) {
|
||||
fputc('\n', stdout);
|
||||
}
|
||||
|
||||
if (filler > 0) {
|
||||
fprintf(stdout, "\033[%dA\r", filler);
|
||||
}
|
||||
|
||||
for (int i = 0; i < g_last_menu_start_col; i++) {
|
||||
fputc(' ', stdout);
|
||||
}
|
||||
|
||||
fflush(stdout);
|
||||
|
||||
g_frame_active = false;
|
||||
g_frame_line_count = 0;
|
||||
|
||||
return tui_get_line(prompt ? prompt : ">", buf, bufsize);
|
||||
}
|
||||
|
||||
void tui_show_splash(void) {
|
||||
static const tui_view_t splash_view = {
|
||||
"> Splash",
|
||||
NULL,
|
||||
};
|
||||
char hold[8];
|
||||
|
||||
tui_begin_frame(&splash_view);
|
||||
tui_print("Welcome to Nostr Terminal.");
|
||||
tui_print("");
|
||||
tui_print("Continuous TUI mode is enabled.");
|
||||
tui_print("Frame redraw preserves scrollback by using newline refresh.");
|
||||
tui_print("");
|
||||
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
|
||||
}
|
||||
|
||||
int tui_get_line(const char *prompt, char *buf, int bufsize) {
|
||||
@@ -162,6 +451,9 @@ int tui_get_line(const char *prompt, char *buf, int bufsize) {
|
||||
unsigned char ch = 0;
|
||||
ssize_t n = read(STDIN_FILENO, &ch, 1);
|
||||
if (n <= 0) {
|
||||
if (n < 0 && errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -203,11 +495,19 @@ int tui_get_key(void) {
|
||||
tui_init();
|
||||
}
|
||||
|
||||
if (read(STDIN_FILENO, &ch, 1) != 1) {
|
||||
while (1) {
|
||||
ssize_t n = read(STDIN_FILENO, &ch, 1);
|
||||
if (n == 1) {
|
||||
return (int)ch;
|
||||
}
|
||||
if (n < 0 && errno == EINTR) {
|
||||
if (g_resize_pending) {
|
||||
return TUI_KEY_RESIZE;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
return (int)ch;
|
||||
}
|
||||
|
||||
void tui_set_window_title(const char *title) {
|
||||
|
||||
Reference in New Issue
Block a user