1356 lines
47 KiB
C
1356 lines
47 KiB
C
/*
|
|
* Superball Thrower - C Implementation
|
|
*
|
|
* A high-performance privacy-focused Superball Thrower daemon in C
|
|
* using the nostr_core_lib for all NOSTR protocol operations.
|
|
*
|
|
* Implements SUP-01 through SUP-06 of the Superball protocol.
|
|
*
|
|
* Author: Roo (Code Mode)
|
|
* License: MIT
|
|
*/
|
|
|
|
// ============================================================================
|
|
// [1] INCLUDES & CONSTANTS
|
|
// ============================================================================
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <signal.h>
|
|
#include <pthread.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#include <stdarg.h>
|
|
|
|
// nostr_core_lib headers
|
|
#include "nostr_core.h"
|
|
#include "cJSON.h"
|
|
|
|
// Version
|
|
#define THROWER_VERSION "v0.0.2"
|
|
|
|
// Configuration constants
|
|
#define MAX_RELAYS 50
|
|
#define MAX_QUEUE_SIZE 1000
|
|
#define MAX_PAYLOAD_SIZE 65536
|
|
#define MAX_PADDING_SIZE 4096
|
|
#define DEFAULT_MAX_DELAY 86460
|
|
#define DEFAULT_REFRESH_RATE 300
|
|
#define CONFIG_FILE "config.json"
|
|
|
|
// Log levels
|
|
#define LOG_DEBUG 0
|
|
#define LOG_INFO 1
|
|
#define LOG_WARN 2
|
|
#define LOG_ERROR 3
|
|
|
|
// ============================================================================
|
|
// [2] DATA STRUCTURES
|
|
// ============================================================================
|
|
|
|
// Relay configuration
|
|
typedef struct {
|
|
char* url;
|
|
int read;
|
|
int write;
|
|
char* auth_status; // "no-auth", "auth-required", "error", "unknown"
|
|
} relay_config_t;
|
|
|
|
// Configuration structure
|
|
typedef struct {
|
|
char* private_key_hex;
|
|
char* name;
|
|
char* description;
|
|
int max_delay;
|
|
int refresh_rate;
|
|
char* supported_sups;
|
|
char* software;
|
|
char* version;
|
|
relay_config_t* relays;
|
|
int relay_count;
|
|
int max_queue_size;
|
|
int log_level;
|
|
} superball_config_t;
|
|
|
|
// Routing payload (Type 1 - from builder)
|
|
typedef struct {
|
|
cJSON* event; // Inner event (encrypted or final)
|
|
char** relays; // Target relay URLs
|
|
int relay_count;
|
|
int delay; // Delay in seconds
|
|
char* next_hop_pubkey; // NULL for final posting
|
|
char* audit_tag; // Required audit tag
|
|
char* payment; // Optional eCash token
|
|
int add_padding_bytes; // Optional padding instruction
|
|
} routing_payload_t;
|
|
|
|
// Padding payload (Type 2 - from previous thrower)
|
|
typedef struct {
|
|
cJSON* event; // Still-encrypted inner event
|
|
char* padding; // Padding data to discard
|
|
} padding_payload_t;
|
|
|
|
// Queue item
|
|
typedef struct {
|
|
char event_id[65];
|
|
cJSON* wrapped_event;
|
|
routing_payload_t* routing;
|
|
time_t received_at;
|
|
time_t process_at;
|
|
char status[32]; // "queued", "processing", "completed", "failed"
|
|
} queue_item_t;
|
|
|
|
// Event queue
|
|
typedef struct {
|
|
queue_item_t** items;
|
|
int count;
|
|
int capacity;
|
|
pthread_mutex_t mutex;
|
|
} event_queue_t;
|
|
|
|
// Main daemon context
|
|
typedef struct {
|
|
superball_config_t* config;
|
|
nostr_relay_pool_t* pool;
|
|
event_queue_t* queue;
|
|
pthread_t auto_publish_thread;
|
|
pthread_t queue_processor_thread;
|
|
unsigned char private_key[32];
|
|
unsigned char public_key[32];
|
|
int running;
|
|
int auto_publish_running;
|
|
int processed_events;
|
|
} superball_thrower_t;
|
|
|
|
// Payload type enum
|
|
typedef enum {
|
|
PAYLOAD_ERROR = 0,
|
|
PAYLOAD_ROUTING = 1, // Type 1: Routing instructions from builder
|
|
PAYLOAD_PADDING = 2 // Type 2: Padding wrapper from previous thrower
|
|
} payload_type_t;
|
|
|
|
// ============================================================================
|
|
// [3] FORWARD DECLARATIONS
|
|
// ============================================================================
|
|
|
|
// Utility functions
|
|
static void log_message(int level, const char* format, ...);
|
|
static int add_jitter(int delay);
|
|
static char* generate_padding(int bytes);
|
|
|
|
// Configuration functions
|
|
static superball_config_t* config_load(const char* path);
|
|
static void config_free(superball_config_t* config);
|
|
static int config_validate(superball_config_t* config);
|
|
|
|
// Crypto functions
|
|
static int decrypt_nip44(const unsigned char* private_key, const char* sender_pubkey,
|
|
const char* encrypted, char* output, size_t output_size);
|
|
static int encrypt_nip44(const unsigned char* private_key, const char* recipient_pubkey,
|
|
const char* plaintext, char* output, size_t output_size);
|
|
|
|
// Queue functions
|
|
static event_queue_t* queue_create(int capacity);
|
|
static int queue_add(event_queue_t* queue, queue_item_t* item);
|
|
static queue_item_t* queue_get_ready(event_queue_t* queue);
|
|
static void queue_destroy(event_queue_t* queue);
|
|
static void* queue_processor_thread_func(void* arg);
|
|
|
|
// Relay functions
|
|
static int relay_test_all(superball_thrower_t* thrower);
|
|
|
|
// Event processing functions
|
|
static void on_routing_event(cJSON* event, const char* relay_url, void* user_data);
|
|
static void on_eose(cJSON** events, int event_count, void* user_data);
|
|
static payload_type_t decrypt_payload(superball_thrower_t* thrower, cJSON* event, void** payload_out);
|
|
static routing_payload_t* parse_routing_payload(cJSON* payload);
|
|
static padding_payload_t* parse_padding_payload(cJSON* payload);
|
|
static int validate_routing(routing_payload_t* routing, int max_delay);
|
|
static void forward_to_next_thrower(superball_thrower_t* thrower, cJSON* event, routing_payload_t* routing);
|
|
static void post_final_event(superball_thrower_t* thrower, cJSON* event, routing_payload_t* routing);
|
|
static void publish_callback(const char* relay_url, const char* event_id, int success,
|
|
const char* message, void* user_data);
|
|
static void free_routing_payload(routing_payload_t* payload);
|
|
static void free_padding_payload(padding_payload_t* payload);
|
|
|
|
// Thrower info functions
|
|
static int publish_thrower_info(superball_thrower_t* thrower);
|
|
static int publish_metadata(superball_thrower_t* thrower);
|
|
static int publish_relay_list(superball_thrower_t* thrower);
|
|
static void* auto_publish_thread_func(void* arg);
|
|
|
|
// Main functions
|
|
static void signal_handler(int signum);
|
|
static superball_thrower_t* thrower_create(const char* config_path);
|
|
static int thrower_start(superball_thrower_t* thrower);
|
|
static void thrower_stop(superball_thrower_t* thrower);
|
|
static void thrower_destroy(superball_thrower_t* thrower);
|
|
|
|
// ============================================================================
|
|
// [4] GLOBAL VARIABLES
|
|
// ============================================================================
|
|
|
|
static volatile sig_atomic_t g_running = 1;
|
|
static superball_thrower_t* g_thrower = NULL;
|
|
static int g_log_level = LOG_INFO;
|
|
|
|
// ============================================================================
|
|
// [5] UTILITY FUNCTIONS
|
|
// ============================================================================
|
|
|
|
static void log_message(int level, const char* format, ...) {
|
|
if (level < g_log_level) return;
|
|
|
|
const char* level_str[] = {"DEBUG", "INFO", "WARN", "ERROR"};
|
|
time_t now = time(NULL);
|
|
char timestamp[32];
|
|
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", localtime(&now));
|
|
|
|
fprintf(stderr, "[%s] [%s] ", timestamp, level_str[level]);
|
|
|
|
va_list args;
|
|
va_start(args, format);
|
|
vfprintf(stderr, format, args);
|
|
va_end(args);
|
|
|
|
fprintf(stderr, "\n");
|
|
fflush(stderr);
|
|
}
|
|
|
|
|
|
static int add_jitter(int delay) {
|
|
// Add ±10% random jitter
|
|
int jitter = (rand() % (delay / 5)) - (delay / 10);
|
|
return delay + jitter;
|
|
}
|
|
|
|
static char* generate_padding(int bytes) {
|
|
if (bytes <= 0) return strdup("");
|
|
if (bytes > MAX_PADDING_SIZE) bytes = MAX_PADDING_SIZE;
|
|
|
|
const char* chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
int chars_len = strlen(chars);
|
|
|
|
char* padding = malloc(bytes + 1);
|
|
if (!padding) return NULL;
|
|
|
|
for (int i = 0; i < bytes; i++) {
|
|
padding[i] = chars[rand() % chars_len];
|
|
}
|
|
padding[bytes] = '\0';
|
|
|
|
return padding;
|
|
}
|
|
|
|
// ============================================================================
|
|
// [6] CONFIGURATION FUNCTIONS
|
|
// ============================================================================
|
|
|
|
static superball_config_t* config_load(const char* path) {
|
|
FILE* fp = fopen(path, "r");
|
|
if (!fp) {
|
|
log_message(LOG_ERROR, "Failed to open config file: %s", path);
|
|
return NULL;
|
|
}
|
|
|
|
fseek(fp, 0, SEEK_END);
|
|
long size = ftell(fp);
|
|
fseek(fp, 0, SEEK_SET);
|
|
|
|
char* buffer = malloc(size + 1);
|
|
if (!buffer) {
|
|
fclose(fp);
|
|
return NULL;
|
|
}
|
|
|
|
size_t bytes_read = fread(buffer, 1, size, fp);
|
|
(void)bytes_read; // Suppress unused warning
|
|
buffer[size] = '\0';
|
|
fclose(fp);
|
|
|
|
cJSON* json = cJSON_Parse(buffer);
|
|
free(buffer);
|
|
|
|
if (!json) {
|
|
log_message(LOG_ERROR, "Failed to parse config JSON");
|
|
return NULL;
|
|
}
|
|
|
|
superball_config_t* config = calloc(1, sizeof(superball_config_t));
|
|
if (!config) {
|
|
cJSON_Delete(json);
|
|
return NULL;
|
|
}
|
|
|
|
// Parse thrower section
|
|
cJSON* thrower = cJSON_GetObjectItem(json, "thrower");
|
|
if (thrower) {
|
|
cJSON* item;
|
|
|
|
if ((item = cJSON_GetObjectItem(thrower, "privateKey")))
|
|
config->private_key_hex = strdup(cJSON_GetStringValue(item));
|
|
if ((item = cJSON_GetObjectItem(thrower, "name")))
|
|
config->name = strdup(cJSON_GetStringValue(item));
|
|
if ((item = cJSON_GetObjectItem(thrower, "description")))
|
|
config->description = strdup(cJSON_GetStringValue(item));
|
|
if ((item = cJSON_GetObjectItem(thrower, "maxDelay")))
|
|
config->max_delay = item->valueint;
|
|
else
|
|
config->max_delay = DEFAULT_MAX_DELAY;
|
|
if ((item = cJSON_GetObjectItem(thrower, "refreshRate")))
|
|
config->refresh_rate = item->valueint;
|
|
else
|
|
config->refresh_rate = DEFAULT_REFRESH_RATE;
|
|
if ((item = cJSON_GetObjectItem(thrower, "supportedSups")))
|
|
config->supported_sups = strdup(cJSON_GetStringValue(item));
|
|
if ((item = cJSON_GetObjectItem(thrower, "software")))
|
|
config->software = strdup(cJSON_GetStringValue(item));
|
|
if ((item = cJSON_GetObjectItem(thrower, "version")))
|
|
config->version = strdup(cJSON_GetStringValue(item));
|
|
}
|
|
|
|
// Parse relays section
|
|
cJSON* relays = cJSON_GetObjectItem(json, "relays");
|
|
if (relays && cJSON_IsArray(relays)) {
|
|
config->relay_count = cJSON_GetArraySize(relays);
|
|
config->relays = calloc(config->relay_count, sizeof(relay_config_t));
|
|
|
|
for (int i = 0; i < config->relay_count; i++) {
|
|
cJSON* relay = cJSON_GetArrayItem(relays, i);
|
|
cJSON* url = cJSON_GetObjectItem(relay, "url");
|
|
cJSON* read = cJSON_GetObjectItem(relay, "read");
|
|
cJSON* write = cJSON_GetObjectItem(relay, "write");
|
|
|
|
if (url) config->relays[i].url = strdup(cJSON_GetStringValue(url));
|
|
config->relays[i].read = read ? cJSON_IsTrue(read) : 1;
|
|
config->relays[i].write = write ? cJSON_IsTrue(write) : 1;
|
|
config->relays[i].auth_status = strdup("unknown");
|
|
}
|
|
}
|
|
|
|
// Parse daemon section
|
|
cJSON* daemon = cJSON_GetObjectItem(json, "daemon");
|
|
if (daemon) {
|
|
cJSON* item;
|
|
if ((item = cJSON_GetObjectItem(daemon, "maxQueueSize")))
|
|
config->max_queue_size = item->valueint;
|
|
else
|
|
config->max_queue_size = MAX_QUEUE_SIZE;
|
|
|
|
if ((item = cJSON_GetObjectItem(daemon, "logLevel"))) {
|
|
const char* level = cJSON_GetStringValue(item);
|
|
if (strcmp(level, "debug") == 0) config->log_level = LOG_DEBUG;
|
|
else if (strcmp(level, "info") == 0) config->log_level = LOG_INFO;
|
|
else if (strcmp(level, "warn") == 0) config->log_level = LOG_WARN;
|
|
else if (strcmp(level, "error") == 0) config->log_level = LOG_ERROR;
|
|
else config->log_level = LOG_INFO;
|
|
} else {
|
|
config->log_level = LOG_INFO;
|
|
}
|
|
}
|
|
|
|
cJSON_Delete(json);
|
|
return config;
|
|
}
|
|
|
|
static void config_free(superball_config_t* config) {
|
|
if (!config) return;
|
|
|
|
free(config->private_key_hex);
|
|
free(config->name);
|
|
free(config->description);
|
|
free(config->supported_sups);
|
|
free(config->software);
|
|
free(config->version);
|
|
|
|
for (int i = 0; i < config->relay_count; i++) {
|
|
free(config->relays[i].url);
|
|
free(config->relays[i].auth_status);
|
|
}
|
|
free(config->relays);
|
|
|
|
free(config);
|
|
}
|
|
|
|
static int config_validate(superball_config_t* config) {
|
|
if (!config) return 0;
|
|
if (!config->private_key_hex || strlen(config->private_key_hex) != 64) {
|
|
log_message(LOG_ERROR, "Invalid private key in configuration");
|
|
return 0;
|
|
}
|
|
if (config->relay_count == 0) {
|
|
log_message(LOG_ERROR, "No relays configured");
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
// ============================================================================
|
|
// [7] CRYPTO FUNCTIONS
|
|
// ============================================================================
|
|
|
|
static int decrypt_nip44(const unsigned char* private_key, const char* sender_pubkey,
|
|
const char* encrypted, char* output, size_t output_size) {
|
|
unsigned char sender_pubkey_bytes[32];
|
|
if (nostr_hex_to_bytes(sender_pubkey, sender_pubkey_bytes, 32) != 0) {
|
|
return NOSTR_ERROR_INVALID_INPUT;
|
|
}
|
|
return nostr_nip44_decrypt(private_key, sender_pubkey_bytes, encrypted, output, output_size);
|
|
}
|
|
|
|
static int encrypt_nip44(const unsigned char* private_key, const char* recipient_pubkey,
|
|
const char* plaintext, char* output, size_t output_size) {
|
|
unsigned char recipient_pubkey_bytes[32];
|
|
if (nostr_hex_to_bytes(recipient_pubkey, recipient_pubkey_bytes, 32) != 0) {
|
|
return NOSTR_ERROR_INVALID_INPUT;
|
|
}
|
|
return nostr_nip44_encrypt(private_key, recipient_pubkey_bytes, plaintext, output, output_size);
|
|
}
|
|
|
|
// ============================================================================
|
|
// [8] QUEUE FUNCTIONS
|
|
// ============================================================================
|
|
|
|
static event_queue_t* queue_create(int capacity) {
|
|
event_queue_t* queue = malloc(sizeof(event_queue_t));
|
|
if (!queue) return NULL;
|
|
|
|
queue->items = calloc(capacity, sizeof(queue_item_t*));
|
|
if (!queue->items) {
|
|
free(queue);
|
|
return NULL;
|
|
}
|
|
|
|
queue->count = 0;
|
|
queue->capacity = capacity;
|
|
pthread_mutex_init(&queue->mutex, NULL);
|
|
|
|
return queue;
|
|
}
|
|
|
|
static int queue_add(event_queue_t* queue, queue_item_t* item) {
|
|
pthread_mutex_lock(&queue->mutex);
|
|
|
|
if (queue->count >= queue->capacity) {
|
|
pthread_mutex_unlock(&queue->mutex);
|
|
log_message(LOG_WARN, "Queue full, dropping oldest item");
|
|
return -1;
|
|
}
|
|
|
|
queue->items[queue->count++] = item;
|
|
log_message(LOG_INFO, "Event queued: %s (process in %ld seconds)",
|
|
item->event_id, item->process_at - time(NULL));
|
|
|
|
pthread_mutex_unlock(&queue->mutex);
|
|
return 0;
|
|
}
|
|
|
|
static queue_item_t* queue_get_ready(event_queue_t* queue) {
|
|
pthread_mutex_lock(&queue->mutex);
|
|
|
|
time_t now = time(NULL);
|
|
queue_item_t* ready_item = NULL;
|
|
int ready_index = -1;
|
|
|
|
for (int i = 0; i < queue->count; i++) {
|
|
if (queue->items[i]->process_at <= now &&
|
|
strcmp(queue->items[i]->status, "queued") == 0) {
|
|
ready_item = queue->items[i];
|
|
ready_index = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ready_item) {
|
|
// Remove from queue
|
|
for (int i = ready_index; i < queue->count - 1; i++) {
|
|
queue->items[i] = queue->items[i + 1];
|
|
}
|
|
queue->count--;
|
|
}
|
|
|
|
pthread_mutex_unlock(&queue->mutex);
|
|
return ready_item;
|
|
}
|
|
|
|
static void queue_destroy(event_queue_t* queue) {
|
|
if (!queue) return;
|
|
|
|
pthread_mutex_lock(&queue->mutex);
|
|
|
|
for (int i = 0; i < queue->count; i++) {
|
|
if (queue->items[i]->wrapped_event) {
|
|
cJSON_Delete(queue->items[i]->wrapped_event);
|
|
}
|
|
if (queue->items[i]->routing) {
|
|
free_routing_payload(queue->items[i]->routing);
|
|
}
|
|
free(queue->items[i]);
|
|
}
|
|
|
|
free(queue->items);
|
|
pthread_mutex_unlock(&queue->mutex);
|
|
pthread_mutex_destroy(&queue->mutex);
|
|
free(queue);
|
|
}
|
|
|
|
static void* queue_processor_thread_func(void* arg) {
|
|
superball_thrower_t* thrower = (superball_thrower_t*)arg;
|
|
|
|
log_message(LOG_INFO, "Queue processor thread started");
|
|
|
|
while (thrower->running) {
|
|
queue_item_t* item = queue_get_ready(thrower->queue);
|
|
|
|
if (item) {
|
|
log_message(LOG_INFO, "Processing queued event: %s", item->event_id);
|
|
strcpy(item->status, "processing");
|
|
|
|
// Check if we should forward or post final
|
|
if (item->routing->next_hop_pubkey) {
|
|
forward_to_next_thrower(thrower, item->wrapped_event, item->routing);
|
|
} else {
|
|
post_final_event(thrower, item->wrapped_event, item->routing);
|
|
}
|
|
|
|
thrower->processed_events++;
|
|
|
|
// Cleanup
|
|
if (item->wrapped_event) cJSON_Delete(item->wrapped_event);
|
|
if (item->routing) free_routing_payload(item->routing);
|
|
free(item);
|
|
}
|
|
|
|
sleep(1); // Check queue every second
|
|
}
|
|
|
|
log_message(LOG_INFO, "Queue processor thread stopped");
|
|
return NULL;
|
|
}
|
|
|
|
// ============================================================================
|
|
// [9] RELAY FUNCTIONS
|
|
// ============================================================================
|
|
|
|
static int relay_test_all(superball_thrower_t* thrower) {
|
|
log_message(LOG_INFO, "Testing authentication for %d relays...",
|
|
thrower->config->relay_count);
|
|
|
|
// For now, mark all as no-auth (testing would require WebSocket implementation)
|
|
// In production, implement proper AUTH testing
|
|
for (int i = 0; i < thrower->config->relay_count; i++) {
|
|
free(thrower->config->relays[i].auth_status);
|
|
thrower->config->relays[i].auth_status = strdup("no-auth");
|
|
}
|
|
|
|
log_message(LOG_INFO, "Relay authentication testing complete");
|
|
return 0;
|
|
}
|
|
|
|
// ============================================================================
|
|
// [10] EVENT PROCESSING FUNCTIONS
|
|
// ============================================================================
|
|
|
|
static void on_routing_event(cJSON* event, const char* relay_url, void* user_data) {
|
|
superball_thrower_t* thrower = (superball_thrower_t*)user_data;
|
|
|
|
cJSON* id = cJSON_GetObjectItem(event, "id");
|
|
if (!id) return;
|
|
|
|
const char* event_id = cJSON_GetStringValue(id);
|
|
log_message(LOG_INFO, "Received routing event: %.16s... from %s", event_id, relay_url);
|
|
|
|
// First decryption
|
|
void* payload = NULL;
|
|
payload_type_t type = decrypt_payload(thrower, event, &payload);
|
|
|
|
if (type == PAYLOAD_PADDING) {
|
|
// Type 2: Padding payload - perform second decryption
|
|
padding_payload_t* padding_payload = (padding_payload_t*)payload;
|
|
log_message(LOG_INFO, "Detected padding payload, discarding %zu bytes of padding",
|
|
strlen(padding_payload->padding));
|
|
|
|
// Second decryption to get routing instructions
|
|
void* routing_payload = NULL;
|
|
payload_type_t inner_type = decrypt_payload(thrower, padding_payload->event, &routing_payload);
|
|
|
|
if (inner_type == PAYLOAD_ROUTING) {
|
|
routing_payload_t* routing = (routing_payload_t*)routing_payload;
|
|
|
|
if (validate_routing(routing, thrower->config->max_delay)) {
|
|
// Create queue item
|
|
queue_item_t* item = calloc(1, sizeof(queue_item_t));
|
|
strncpy(item->event_id, event_id, 64);
|
|
item->wrapped_event = cJSON_Duplicate(padding_payload->event, 1);
|
|
item->routing = routing;
|
|
item->received_at = time(NULL);
|
|
item->process_at = time(NULL) + add_jitter(routing->delay);
|
|
strcpy(item->status, "queued");
|
|
|
|
queue_add(thrower->queue, item);
|
|
} else {
|
|
free_routing_payload(routing);
|
|
}
|
|
}
|
|
|
|
free_padding_payload(padding_payload);
|
|
|
|
} else if (type == PAYLOAD_ROUTING) {
|
|
// Type 1: Routing payload - process directly
|
|
log_message(LOG_INFO, "Detected routing payload, processing directly");
|
|
routing_payload_t* routing = (routing_payload_t*)payload;
|
|
|
|
if (validate_routing(routing, thrower->config->max_delay)) {
|
|
// Create queue item
|
|
queue_item_t* item = calloc(1, sizeof(queue_item_t));
|
|
strncpy(item->event_id, event_id, 64);
|
|
item->wrapped_event = cJSON_Duplicate(event, 1);
|
|
item->routing = routing;
|
|
item->received_at = time(NULL);
|
|
item->process_at = time(NULL) + add_jitter(routing->delay);
|
|
strcpy(item->status, "queued");
|
|
|
|
queue_add(thrower->queue, item);
|
|
} else {
|
|
free_routing_payload(routing);
|
|
}
|
|
} else {
|
|
log_message(LOG_ERROR, "Failed to decrypt payload for event %.16s...", event_id);
|
|
}
|
|
}
|
|
|
|
static void on_eose(cJSON** events, int event_count, void* user_data) {
|
|
(void)events; // Suppress unused parameter warning
|
|
(void)user_data; // Suppress unused parameter warning
|
|
log_message(LOG_DEBUG, "End of stored events (EOSE) - %d events received", event_count);
|
|
}
|
|
|
|
static payload_type_t decrypt_payload(superball_thrower_t* thrower, cJSON* event, void** payload_out) {
|
|
cJSON* content = cJSON_GetObjectItem(event, "content");
|
|
cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey");
|
|
|
|
if (!content || !pubkey) return PAYLOAD_ERROR;
|
|
|
|
char decrypted[MAX_PAYLOAD_SIZE];
|
|
int result = decrypt_nip44(thrower->private_key, cJSON_GetStringValue(pubkey),
|
|
cJSON_GetStringValue(content), decrypted, sizeof(decrypted));
|
|
|
|
if (result != NOSTR_SUCCESS) {
|
|
log_message(LOG_ERROR, "NIP-44 decryption failed");
|
|
return PAYLOAD_ERROR;
|
|
}
|
|
|
|
cJSON* payload = cJSON_Parse(decrypted);
|
|
if (!payload) {
|
|
log_message(LOG_ERROR, "Failed to parse decrypted payload JSON");
|
|
return PAYLOAD_ERROR;
|
|
}
|
|
|
|
// Check payload type
|
|
if (cJSON_HasObjectItem(payload, "padding")) {
|
|
*payload_out = parse_padding_payload(payload);
|
|
cJSON_Delete(payload);
|
|
return *payload_out ? PAYLOAD_PADDING : PAYLOAD_ERROR;
|
|
} else if (cJSON_HasObjectItem(payload, "routing")) {
|
|
*payload_out = parse_routing_payload(payload);
|
|
cJSON_Delete(payload);
|
|
return *payload_out ? PAYLOAD_ROUTING : PAYLOAD_ERROR;
|
|
}
|
|
|
|
cJSON_Delete(payload);
|
|
return PAYLOAD_ERROR;
|
|
}
|
|
|
|
static routing_payload_t* parse_routing_payload(cJSON* payload) {
|
|
routing_payload_t* routing = calloc(1, sizeof(routing_payload_t));
|
|
if (!routing) return NULL;
|
|
|
|
cJSON* event = cJSON_GetObjectItem(payload, "event");
|
|
cJSON* routing_obj = cJSON_GetObjectItem(payload, "routing");
|
|
|
|
if (!event || !routing_obj) {
|
|
free(routing);
|
|
return NULL;
|
|
}
|
|
|
|
routing->event = cJSON_Duplicate(event, 1);
|
|
|
|
// Parse routing instructions
|
|
cJSON* relays = cJSON_GetObjectItem(routing_obj, "relays");
|
|
if (relays && cJSON_IsArray(relays)) {
|
|
routing->relay_count = cJSON_GetArraySize(relays);
|
|
routing->relays = calloc(routing->relay_count, sizeof(char*));
|
|
for (int i = 0; i < routing->relay_count; i++) {
|
|
cJSON* relay = cJSON_GetArrayItem(relays, i);
|
|
routing->relays[i] = strdup(cJSON_GetStringValue(relay));
|
|
}
|
|
}
|
|
|
|
cJSON* delay = cJSON_GetObjectItem(routing_obj, "delay");
|
|
routing->delay = delay ? delay->valueint : 0;
|
|
|
|
cJSON* p = cJSON_GetObjectItem(routing_obj, "p");
|
|
routing->next_hop_pubkey = p ? strdup(cJSON_GetStringValue(p)) : NULL;
|
|
|
|
cJSON* audit = cJSON_GetObjectItem(routing_obj, "audit");
|
|
routing->audit_tag = audit ? strdup(cJSON_GetStringValue(audit)) : NULL;
|
|
|
|
cJSON* payment = cJSON_GetObjectItem(routing_obj, "payment");
|
|
routing->payment = payment ? strdup(cJSON_GetStringValue(payment)) : NULL;
|
|
|
|
cJSON* add_padding = cJSON_GetObjectItem(routing_obj, "add_padding_bytes");
|
|
routing->add_padding_bytes = add_padding ? add_padding->valueint : 0;
|
|
|
|
return routing;
|
|
}
|
|
|
|
static padding_payload_t* parse_padding_payload(cJSON* payload) {
|
|
padding_payload_t* padding = calloc(1, sizeof(padding_payload_t));
|
|
if (!padding) return NULL;
|
|
|
|
cJSON* event = cJSON_GetObjectItem(payload, "event");
|
|
cJSON* padding_str = cJSON_GetObjectItem(payload, "padding");
|
|
|
|
if (!event) {
|
|
free(padding);
|
|
return NULL;
|
|
}
|
|
|
|
padding->event = cJSON_Duplicate(event, 1);
|
|
padding->padding = padding_str ? strdup(cJSON_GetStringValue(padding_str)) : strdup("");
|
|
|
|
return padding;
|
|
}
|
|
|
|
static int validate_routing(routing_payload_t* routing, int max_delay) {
|
|
if (!routing) return 0;
|
|
if (!routing->relays || routing->relay_count == 0) {
|
|
log_message(LOG_ERROR, "No relays in routing instructions");
|
|
return 0;
|
|
}
|
|
if (routing->delay < 0 || routing->delay > max_delay) {
|
|
log_message(LOG_ERROR, "Invalid delay: %d (max: %d)", routing->delay, max_delay);
|
|
return 0;
|
|
}
|
|
if (!routing->audit_tag) {
|
|
log_message(LOG_ERROR, "Missing audit tag");
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static void forward_to_next_thrower(superball_thrower_t* thrower, cJSON* event, routing_payload_t* routing) {
|
|
log_message(LOG_INFO, "Forwarding to next thrower: %.16s...", routing->next_hop_pubkey);
|
|
|
|
// Generate ephemeral keypair
|
|
unsigned char ephemeral_private[32];
|
|
unsigned char ephemeral_public[32];
|
|
nostr_generate_keypair(ephemeral_private, ephemeral_public);
|
|
|
|
// Generate padding
|
|
char* padding_data = generate_padding(routing->add_padding_bytes);
|
|
if (routing->add_padding_bytes > 0) {
|
|
log_message(LOG_INFO, "Generated %d bytes of padding", routing->add_padding_bytes);
|
|
}
|
|
|
|
// Create padding payload
|
|
cJSON* padding_payload = cJSON_CreateObject();
|
|
cJSON_AddItemToObject(padding_payload, "event", cJSON_Duplicate(event, 1));
|
|
cJSON_AddItemToObject(padding_payload, "padding", cJSON_CreateString(padding_data));
|
|
free(padding_data);
|
|
|
|
// Encrypt to next thrower
|
|
char* payload_json = cJSON_PrintUnformatted(padding_payload);
|
|
char encrypted[MAX_PAYLOAD_SIZE];
|
|
int result = encrypt_nip44(ephemeral_private, routing->next_hop_pubkey,
|
|
payload_json, encrypted, sizeof(encrypted));
|
|
free(payload_json);
|
|
cJSON_Delete(padding_payload);
|
|
|
|
if (result != NOSTR_SUCCESS) {
|
|
log_message(LOG_ERROR, "Failed to encrypt padding payload");
|
|
return;
|
|
}
|
|
|
|
// Create routing event
|
|
cJSON* tags = cJSON_CreateArray();
|
|
cJSON* p_tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(p_tag, cJSON_CreateString("p"));
|
|
cJSON_AddItemToArray(p_tag, cJSON_CreateString(routing->next_hop_pubkey));
|
|
cJSON_AddItemToArray(tags, p_tag);
|
|
|
|
cJSON* audit_tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(audit_tag, cJSON_CreateString("p"));
|
|
cJSON_AddItemToArray(audit_tag, cJSON_CreateString(routing->audit_tag));
|
|
cJSON_AddItemToArray(tags, audit_tag);
|
|
|
|
cJSON* signed_event = nostr_create_and_sign_event(22222, encrypted, tags,
|
|
ephemeral_private, time(NULL));
|
|
cJSON_Delete(tags);
|
|
|
|
if (!signed_event) {
|
|
log_message(LOG_ERROR, "Failed to create routing event");
|
|
return;
|
|
}
|
|
|
|
// Log the event JSON being published
|
|
char* event_json = cJSON_Print(signed_event);
|
|
if (event_json) {
|
|
log_message(LOG_INFO, "Publishing routing event JSON:\n%s", event_json);
|
|
free(event_json);
|
|
}
|
|
|
|
// Publish to relays
|
|
nostr_relay_pool_publish_async(thrower->pool, (const char**)routing->relays,
|
|
routing->relay_count, signed_event,
|
|
publish_callback, thrower);
|
|
|
|
cJSON_Delete(signed_event);
|
|
log_message(LOG_INFO, "Forwarded event to next thrower");
|
|
}
|
|
|
|
static void post_final_event(superball_thrower_t* thrower, cJSON* event, routing_payload_t* routing) {
|
|
(void)event; // The wrapped event is not used - we publish the inner event from routing
|
|
|
|
log_message(LOG_INFO, "Posting final event to %d relays", routing->relay_count);
|
|
|
|
// The inner event is in routing->event (this is the kind 1 note, not the kind 22222 wrapper)
|
|
if (!routing->event) {
|
|
log_message(LOG_ERROR, "No inner event to publish");
|
|
return;
|
|
}
|
|
|
|
// Log the inner event JSON being published
|
|
char* event_json = cJSON_Print(routing->event);
|
|
if (event_json) {
|
|
log_message(LOG_INFO, "Publishing final event JSON:\n%s", event_json);
|
|
free(event_json);
|
|
}
|
|
|
|
// Publish the inner event directly (this is the actual kind 1 note)
|
|
nostr_relay_pool_publish_async(thrower->pool, (const char**)routing->relays,
|
|
routing->relay_count, routing->event,
|
|
publish_callback, thrower);
|
|
|
|
cJSON* id = cJSON_GetObjectItem(routing->event, "id");
|
|
if (id) {
|
|
log_message(LOG_INFO, "Final event posted: %.16s...", cJSON_GetStringValue(id));
|
|
}
|
|
}
|
|
|
|
static void publish_callback(const char* relay_url, const char* event_id, int success,
|
|
const char* message, void* user_data) {
|
|
superball_thrower_t* thrower = (superball_thrower_t*)user_data;
|
|
|
|
if (success) {
|
|
log_message(LOG_INFO, "✅ Published to %s: %.16s...", relay_url, event_id);
|
|
|
|
// Print relay response if available
|
|
if (message) {
|
|
log_message(LOG_DEBUG, "Relay response: %s", message);
|
|
}
|
|
} else {
|
|
log_message(LOG_ERROR, "❌ Failed to publish to %s: %s", relay_url, message ? message : "unknown error");
|
|
}
|
|
|
|
// Note: We don't have access to the full event JSON here in the callback
|
|
// The event was already published. To see the full event, we'd need to
|
|
// log it before calling nostr_relay_pool_publish_async
|
|
(void)thrower; // Suppress unused warning for now
|
|
}
|
|
|
|
static void free_routing_payload(routing_payload_t* payload) {
|
|
if (!payload) return;
|
|
if (payload->event) cJSON_Delete(payload->event);
|
|
for (int i = 0; i < payload->relay_count; i++) {
|
|
free(payload->relays[i]);
|
|
}
|
|
free(payload->relays);
|
|
free(payload->next_hop_pubkey);
|
|
free(payload->audit_tag);
|
|
free(payload->payment);
|
|
free(payload);
|
|
}
|
|
|
|
static void free_padding_payload(padding_payload_t* payload) {
|
|
if (!payload) return;
|
|
if (payload->event) cJSON_Delete(payload->event);
|
|
free(payload->padding);
|
|
free(payload);
|
|
}
|
|
|
|
// ============================================================================
|
|
// [11] THROWER INFO FUNCTIONS
|
|
// ============================================================================
|
|
|
|
static int publish_thrower_info(superball_thrower_t* thrower) {
|
|
log_message(LOG_INFO, "Publishing Thrower Information Document (SUP-06)...");
|
|
|
|
cJSON* tags = cJSON_CreateArray();
|
|
|
|
if (thrower->config->name) {
|
|
cJSON* tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString("name"));
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString(thrower->config->name));
|
|
cJSON_AddItemToArray(tags, tag);
|
|
}
|
|
|
|
if (thrower->config->description) {
|
|
cJSON* tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString("description"));
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString(thrower->config->description));
|
|
cJSON_AddItemToArray(tags, tag);
|
|
}
|
|
|
|
if (thrower->config->supported_sups) {
|
|
cJSON* tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString("supported_sups"));
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString(thrower->config->supported_sups));
|
|
cJSON_AddItemToArray(tags, tag);
|
|
}
|
|
|
|
if (thrower->config->software) {
|
|
cJSON* tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString("software"));
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString(thrower->config->software));
|
|
cJSON_AddItemToArray(tags, tag);
|
|
}
|
|
|
|
if (thrower->config->version) {
|
|
cJSON* tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString("version"));
|
|
cJSON_AddItemToArray(tag, cJSON_CreateString(thrower->config->version));
|
|
cJSON_AddItemToArray(tags, tag);
|
|
}
|
|
|
|
char refresh_str[32];
|
|
snprintf(refresh_str, sizeof(refresh_str), "%d", thrower->config->refresh_rate);
|
|
cJSON* refresh_tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(refresh_tag, cJSON_CreateString("refresh_rate"));
|
|
cJSON_AddItemToArray(refresh_tag, cJSON_CreateString(refresh_str));
|
|
cJSON_AddItemToArray(tags, refresh_tag);
|
|
|
|
char max_delay_str[32];
|
|
snprintf(max_delay_str, sizeof(max_delay_str), "%d", thrower->config->max_delay);
|
|
cJSON* max_delay_tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(max_delay_tag, cJSON_CreateString("max_delay"));
|
|
cJSON_AddItemToArray(max_delay_tag, cJSON_CreateString(max_delay_str));
|
|
cJSON_AddItemToArray(tags, max_delay_tag);
|
|
|
|
cJSON* event = nostr_create_and_sign_event(12222, "", tags,
|
|
thrower->private_key, time(NULL));
|
|
cJSON_Delete(tags);
|
|
|
|
if (!event) {
|
|
log_message(LOG_ERROR, "Failed to create thrower info event");
|
|
return -1;
|
|
}
|
|
|
|
// Get write-capable relays
|
|
const char** relay_urls = malloc(thrower->config->relay_count * sizeof(char*));
|
|
int relay_count = 0;
|
|
for (int i = 0; i < thrower->config->relay_count; i++) {
|
|
if (thrower->config->relays[i].write &&
|
|
strcmp(thrower->config->relays[i].auth_status, "no-auth") == 0) {
|
|
relay_urls[relay_count++] = thrower->config->relays[i].url;
|
|
}
|
|
}
|
|
|
|
if (relay_count == 0) {
|
|
log_message(LOG_WARN, "No write-capable relays for thrower info");
|
|
free(relay_urls);
|
|
cJSON_Delete(event);
|
|
return -1;
|
|
}
|
|
|
|
nostr_relay_pool_publish_async(thrower->pool, relay_urls, relay_count,
|
|
event, publish_callback, thrower);
|
|
|
|
free(relay_urls);
|
|
cJSON_Delete(event);
|
|
|
|
log_message(LOG_INFO, "Thrower info published to %d relays", relay_count);
|
|
return 0;
|
|
}
|
|
|
|
static int publish_metadata(superball_thrower_t* thrower) {
|
|
log_message(LOG_INFO, "Publishing metadata (kind 0)...");
|
|
|
|
// Create metadata JSON content
|
|
cJSON* metadata = cJSON_CreateObject();
|
|
|
|
if (thrower->config->name) {
|
|
cJSON_AddStringToObject(metadata, "name", thrower->config->name);
|
|
}
|
|
|
|
if (thrower->config->description) {
|
|
cJSON_AddStringToObject(metadata, "about", thrower->config->description);
|
|
}
|
|
|
|
// Add Superball-specific fields
|
|
if (thrower->config->software) {
|
|
cJSON_AddStringToObject(metadata, "nip05", thrower->config->software);
|
|
}
|
|
|
|
// Add version and supported SUPs
|
|
if (thrower->config->version) {
|
|
cJSON_AddStringToObject(metadata, "display_name",
|
|
thrower->config->version);
|
|
}
|
|
|
|
if (thrower->config->supported_sups) {
|
|
cJSON_AddStringToObject(metadata, "website",
|
|
thrower->config->supported_sups);
|
|
}
|
|
|
|
char* content = cJSON_PrintUnformatted(metadata);
|
|
cJSON_Delete(metadata);
|
|
|
|
if (!content) {
|
|
log_message(LOG_ERROR, "Failed to create metadata JSON");
|
|
return -1;
|
|
}
|
|
|
|
// Create kind 0 event with empty tags
|
|
cJSON* tags = cJSON_CreateArray();
|
|
cJSON* event = nostr_create_and_sign_event(0, content, tags,
|
|
thrower->private_key, time(NULL));
|
|
free(content);
|
|
cJSON_Delete(tags);
|
|
|
|
if (!event) {
|
|
log_message(LOG_ERROR, "Failed to create metadata event");
|
|
return -1;
|
|
}
|
|
|
|
// Get write-capable relays
|
|
const char** relay_urls = malloc(thrower->config->relay_count * sizeof(char*));
|
|
int relay_count = 0;
|
|
for (int i = 0; i < thrower->config->relay_count; i++) {
|
|
if (thrower->config->relays[i].write &&
|
|
strcmp(thrower->config->relays[i].auth_status, "no-auth") == 0) {
|
|
relay_urls[relay_count++] = thrower->config->relays[i].url;
|
|
}
|
|
}
|
|
|
|
if (relay_count == 0) {
|
|
log_message(LOG_WARN, "No write-capable relays for metadata");
|
|
free(relay_urls);
|
|
cJSON_Delete(event);
|
|
return -1;
|
|
}
|
|
|
|
nostr_relay_pool_publish_async(thrower->pool, relay_urls, relay_count,
|
|
event, publish_callback, thrower);
|
|
|
|
free(relay_urls);
|
|
cJSON_Delete(event);
|
|
|
|
log_message(LOG_INFO, "Metadata published to %d relays", relay_count);
|
|
return 0;
|
|
}
|
|
|
|
static int publish_relay_list(superball_thrower_t* thrower) {
|
|
log_message(LOG_INFO, "Publishing relay list (kind 10002)...");
|
|
|
|
// Create tags array with relay information
|
|
cJSON* tags = cJSON_CreateArray();
|
|
|
|
for (int i = 0; i < thrower->config->relay_count; i++) {
|
|
cJSON* relay_tag = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(relay_tag, cJSON_CreateString("r"));
|
|
cJSON_AddItemToArray(relay_tag, cJSON_CreateString(thrower->config->relays[i].url));
|
|
|
|
// Add read/write markers
|
|
if (thrower->config->relays[i].read && thrower->config->relays[i].write) {
|
|
// Both read and write - no marker needed (default)
|
|
} else if (thrower->config->relays[i].read) {
|
|
cJSON_AddItemToArray(relay_tag, cJSON_CreateString("read"));
|
|
} else if (thrower->config->relays[i].write) {
|
|
cJSON_AddItemToArray(relay_tag, cJSON_CreateString("write"));
|
|
}
|
|
|
|
cJSON_AddItemToArray(tags, relay_tag);
|
|
}
|
|
|
|
// Create kind 10002 event with empty content
|
|
cJSON* event = nostr_create_and_sign_event(10002, "", tags,
|
|
thrower->private_key, time(NULL));
|
|
cJSON_Delete(tags);
|
|
|
|
if (!event) {
|
|
log_message(LOG_ERROR, "Failed to create relay list event");
|
|
return -1;
|
|
}
|
|
|
|
// Get write-capable relays
|
|
const char** relay_urls = malloc(thrower->config->relay_count * sizeof(char*));
|
|
int relay_count = 0;
|
|
for (int i = 0; i < thrower->config->relay_count; i++) {
|
|
if (thrower->config->relays[i].write &&
|
|
strcmp(thrower->config->relays[i].auth_status, "no-auth") == 0) {
|
|
relay_urls[relay_count++] = thrower->config->relays[i].url;
|
|
}
|
|
}
|
|
|
|
if (relay_count == 0) {
|
|
log_message(LOG_WARN, "No write-capable relays for relay list");
|
|
free(relay_urls);
|
|
cJSON_Delete(event);
|
|
return -1;
|
|
}
|
|
|
|
nostr_relay_pool_publish_async(thrower->pool, relay_urls, relay_count,
|
|
event, publish_callback, thrower);
|
|
|
|
free(relay_urls);
|
|
cJSON_Delete(event);
|
|
|
|
log_message(LOG_INFO, "Relay list published to %d relays", relay_count);
|
|
return 0;
|
|
}
|
|
|
|
static void* auto_publish_thread_func(void* arg) {
|
|
superball_thrower_t* thrower = (superball_thrower_t*)arg;
|
|
|
|
log_message(LOG_INFO, "Auto-publish thread started (interval: %d seconds)",
|
|
thrower->config->refresh_rate);
|
|
|
|
int elapsed = 0;
|
|
while (thrower->auto_publish_running) {
|
|
// Sleep in 1-second intervals to allow responsive shutdown
|
|
sleep(1);
|
|
elapsed++;
|
|
|
|
if (elapsed >= thrower->config->refresh_rate && thrower->auto_publish_running) {
|
|
publish_thrower_info(thrower);
|
|
elapsed = 0;
|
|
}
|
|
}
|
|
|
|
log_message(LOG_INFO, "Auto-publish thread stopped");
|
|
return NULL;
|
|
}
|
|
|
|
// ============================================================================
|
|
// [12] MAIN FUNCTIONS
|
|
// ============================================================================
|
|
|
|
static void signal_handler(int signum) {
|
|
log_message(LOG_INFO, "Received signal %d, shutting down...", signum);
|
|
g_running = 0;
|
|
if (g_thrower) {
|
|
g_thrower->running = 0;
|
|
g_thrower->auto_publish_running = 0;
|
|
}
|
|
}
|
|
|
|
static superball_thrower_t* thrower_create(const char* config_path) {
|
|
superball_thrower_t* thrower = calloc(1, sizeof(superball_thrower_t));
|
|
if (!thrower) return NULL;
|
|
|
|
// Load configuration
|
|
thrower->config = config_load(config_path);
|
|
if (!thrower->config || !config_validate(thrower->config)) {
|
|
log_message(LOG_ERROR, "Failed to load or validate configuration");
|
|
free(thrower);
|
|
return NULL;
|
|
}
|
|
|
|
g_log_level = thrower->config->log_level;
|
|
|
|
// Parse private key
|
|
if (nostr_hex_to_bytes(thrower->config->private_key_hex, thrower->private_key, 32) != 0) {
|
|
log_message(LOG_ERROR, "Failed to parse private key");
|
|
config_free(thrower->config);
|
|
free(thrower);
|
|
return NULL;
|
|
}
|
|
|
|
// Derive public key
|
|
nostr_ec_public_key_from_private_key(thrower->private_key, thrower->public_key);
|
|
|
|
char pubkey_hex[65];
|
|
nostr_bytes_to_hex(thrower->public_key, 32, pubkey_hex);
|
|
log_message(LOG_INFO, "Thrower public key: %s", pubkey_hex);
|
|
|
|
// Create relay pool
|
|
thrower->pool = nostr_relay_pool_create(NULL);
|
|
if (!thrower->pool) {
|
|
log_message(LOG_ERROR, "Failed to create relay pool");
|
|
config_free(thrower->config);
|
|
free(thrower);
|
|
return NULL;
|
|
}
|
|
|
|
// Add relays to pool
|
|
for (int i = 0; i < thrower->config->relay_count; i++) {
|
|
nostr_relay_pool_add_relay(thrower->pool, thrower->config->relays[i].url);
|
|
log_message(LOG_INFO, "Added relay: %s", thrower->config->relays[i].url);
|
|
}
|
|
|
|
// Create event queue
|
|
thrower->queue = queue_create(thrower->config->max_queue_size);
|
|
if (!thrower->queue) {
|
|
log_message(LOG_ERROR, "Failed to create event queue");
|
|
nostr_relay_pool_destroy(thrower->pool);
|
|
config_free(thrower->config);
|
|
free(thrower);
|
|
return NULL;
|
|
}
|
|
|
|
thrower->running = 1;
|
|
thrower->auto_publish_running = 0;
|
|
thrower->processed_events = 0;
|
|
|
|
return thrower;
|
|
}
|
|
|
|
static int thrower_start(superball_thrower_t* thrower) {
|
|
log_message(LOG_INFO, "Starting Superball Thrower daemon...");
|
|
|
|
// Test relay authentication
|
|
relay_test_all(thrower);
|
|
|
|
// Start queue processor thread
|
|
if (pthread_create(&thrower->queue_processor_thread, NULL,
|
|
queue_processor_thread_func, thrower) != 0) {
|
|
log_message(LOG_ERROR, "Failed to create queue processor thread");
|
|
return -1;
|
|
}
|
|
|
|
// Subscribe to routing events
|
|
char pubkey_hex[65];
|
|
nostr_bytes_to_hex(thrower->public_key, 32, pubkey_hex);
|
|
|
|
cJSON* filter = cJSON_CreateObject();
|
|
cJSON* kinds = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(22222));
|
|
cJSON_AddItemToObject(filter, "kinds", kinds);
|
|
|
|
cJSON* p_tags = cJSON_CreateArray();
|
|
cJSON_AddItemToArray(p_tags, cJSON_CreateString(pubkey_hex));
|
|
cJSON_AddItemToObject(filter, "#p", p_tags);
|
|
|
|
cJSON_AddItemToObject(filter, "since", cJSON_CreateNumber(time(NULL)));
|
|
|
|
const char** relay_urls = malloc(thrower->config->relay_count * sizeof(char*));
|
|
for (int i = 0; i < thrower->config->relay_count; i++) {
|
|
relay_urls[i] = thrower->config->relays[i].url;
|
|
}
|
|
|
|
nostr_relay_pool_subscribe(thrower->pool, relay_urls, thrower->config->relay_count,
|
|
filter, on_routing_event, on_eose, thrower,
|
|
0, 1, NOSTR_POOL_EOSE_FULL_SET, 30, 60);
|
|
|
|
free(relay_urls);
|
|
cJSON_Delete(filter);
|
|
|
|
log_message(LOG_INFO, "Monitoring %d relays for routing events", thrower->config->relay_count);
|
|
|
|
// Publish initial metadata and relay list
|
|
publish_metadata(thrower);
|
|
publish_relay_list(thrower);
|
|
|
|
// Publish initial thrower info
|
|
publish_thrower_info(thrower);
|
|
|
|
// Start auto-publish thread
|
|
thrower->auto_publish_running = 1;
|
|
if (pthread_create(&thrower->auto_publish_thread, NULL,
|
|
auto_publish_thread_func, thrower) != 0) {
|
|
log_message(LOG_ERROR, "Failed to create auto-publish thread");
|
|
return -1;
|
|
}
|
|
|
|
log_message(LOG_INFO, "Superball Thrower daemon started successfully");
|
|
return 0;
|
|
}
|
|
|
|
static void thrower_stop(superball_thrower_t* thrower) {
|
|
if (!thrower) return;
|
|
|
|
log_message(LOG_INFO, "Stopping Superball Thrower daemon...");
|
|
|
|
thrower->running = 0;
|
|
thrower->auto_publish_running = 0;
|
|
|
|
// Wait for threads
|
|
pthread_join(thrower->queue_processor_thread, NULL);
|
|
pthread_join(thrower->auto_publish_thread, NULL);
|
|
|
|
log_message(LOG_INFO, "Superball Thrower daemon stopped (processed %d events)",
|
|
thrower->processed_events);
|
|
}
|
|
|
|
static void thrower_destroy(superball_thrower_t* thrower) {
|
|
if (!thrower) return;
|
|
|
|
if (thrower->pool) nostr_relay_pool_destroy(thrower->pool);
|
|
if (thrower->queue) queue_destroy(thrower->queue);
|
|
if (thrower->config) config_free(thrower->config);
|
|
|
|
free(thrower);
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAIN
|
|
// ============================================================================
|
|
|
|
int main(int argc, char* argv[]) {
|
|
const char* config_path = CONFIG_FILE;
|
|
|
|
// Parse command line arguments
|
|
if (argc > 1) {
|
|
if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0) {
|
|
printf("Superball Thrower - C Implementation\n\n");
|
|
printf("Usage: %s [config_file]\n\n", argv[0]);
|
|
printf("Options:\n");
|
|
printf(" config_file Path to configuration file (default: config.json)\n");
|
|
printf(" --help, -h Show this help message\n\n");
|
|
return 0;
|
|
}
|
|
config_path = argv[1];
|
|
}
|
|
|
|
// Initialize crypto
|
|
nostr_crypto_init();
|
|
|
|
// Seed random number generator
|
|
srand(time(NULL));
|
|
|
|
// Setup signal handlers
|
|
signal(SIGINT, signal_handler);
|
|
signal(SIGTERM, signal_handler);
|
|
|
|
// Create thrower
|
|
g_thrower = thrower_create(config_path);
|
|
if (!g_thrower) {
|
|
log_message(LOG_ERROR, "Failed to create thrower");
|
|
nostr_crypto_cleanup();
|
|
return 1;
|
|
}
|
|
|
|
// Start thrower
|
|
if (thrower_start(g_thrower) != 0) {
|
|
log_message(LOG_ERROR, "Failed to start thrower");
|
|
thrower_destroy(g_thrower);
|
|
nostr_crypto_cleanup();
|
|
return 1;
|
|
}
|
|
|
|
// Main event loop
|
|
while (g_running) {
|
|
nostr_relay_pool_poll(g_thrower->pool, 1000);
|
|
}
|
|
|
|
// Cleanup
|
|
thrower_stop(g_thrower);
|
|
thrower_destroy(g_thrower);
|
|
nostr_crypto_cleanup();
|
|
|
|
log_message(LOG_INFO, "Shutdown complete");
|
|
return 0;
|
|
} |