nip01 upload
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
nostr_core_lib/
|
||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "nostr_core_lib"]
|
||||||
|
path = nostr_core_lib
|
||||||
|
url = https://git.laantungir.net/laantungir/nostr_core_lib.git
|
||||||
72
Makefile
Normal file
72
Makefile
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# C-Relay Makefile
|
||||||
|
|
||||||
|
CC = gcc
|
||||||
|
CFLAGS = -Wall -Wextra -std=c99 -g -O2
|
||||||
|
INCLUDES = -I. -Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket
|
||||||
|
LIBS = -lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k1 -lssl -lcrypto -L/usr/local/lib -lcurl
|
||||||
|
|
||||||
|
# Source files
|
||||||
|
MAIN_SRC = src/main.c
|
||||||
|
NOSTR_CORE_LIB = nostr_core_lib/libnostr_core_x64.a
|
||||||
|
|
||||||
|
# Target binary
|
||||||
|
TARGET = src/main
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
all: $(TARGET)
|
||||||
|
|
||||||
|
# Check if nostr_core_lib is built
|
||||||
|
$(NOSTR_CORE_LIB):
|
||||||
|
@echo "Building nostr_core_lib..."
|
||||||
|
cd nostr_core_lib && ./build.sh
|
||||||
|
|
||||||
|
# Build the relay
|
||||||
|
$(TARGET): $(MAIN_SRC) $(NOSTR_CORE_LIB)
|
||||||
|
@echo "Compiling C-Relay..."
|
||||||
|
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(TARGET) $(NOSTR_CORE_LIB) $(LIBS)
|
||||||
|
@echo "Build complete: $(TARGET)"
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
test: $(TARGET)
|
||||||
|
@echo "Running tests..."
|
||||||
|
./tests/1_nip_test.sh
|
||||||
|
|
||||||
|
# Initialize database
|
||||||
|
init-db:
|
||||||
|
@echo "Initializing database..."
|
||||||
|
./db/init.sh --force
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
clean:
|
||||||
|
rm -f $(TARGET)
|
||||||
|
@echo "Clean complete"
|
||||||
|
|
||||||
|
# Clean everything including nostr_core_lib
|
||||||
|
clean-all: clean
|
||||||
|
cd nostr_core_lib && make clean 2>/dev/null || true
|
||||||
|
|
||||||
|
# Install dependencies (Ubuntu/Debian)
|
||||||
|
install-deps:
|
||||||
|
@echo "Installing dependencies..."
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y build-essential libsqlite3-dev libssl-dev libcurl4-openssl-dev libsecp256k1-dev zlib1g-dev jq curl
|
||||||
|
|
||||||
|
# Help
|
||||||
|
help:
|
||||||
|
@echo "C-Relay Build System"
|
||||||
|
@echo ""
|
||||||
|
@echo "Targets:"
|
||||||
|
@echo " all Build the relay (default)"
|
||||||
|
@echo " test Build and run tests"
|
||||||
|
@echo " init-db Initialize the database"
|
||||||
|
@echo " clean Clean build artifacts"
|
||||||
|
@echo " clean-all Clean everything including dependencies"
|
||||||
|
@echo " install-deps Install system dependencies"
|
||||||
|
@echo " help Show this help"
|
||||||
|
@echo ""
|
||||||
|
@echo "Usage:"
|
||||||
|
@echo " make # Build the relay"
|
||||||
|
@echo " make test # Run tests"
|
||||||
|
@echo " make init-db # Set up database"
|
||||||
|
|
||||||
|
.PHONY: all test init-db clean clean-all install-deps help
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
A nostr relay in C.
|
A nostr relay in C with sqlite on the back end.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
228
db/README.md
Normal file
228
db/README.md
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
# C Nostr Relay Database
|
||||||
|
|
||||||
|
This directory contains the SQLite database schema and initialization scripts for the C Nostr Relay implementation.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- **`schema.sql`** - Complete database schema based on nostr-rs-relay v18
|
||||||
|
- **`init.sh`** - Database initialization script
|
||||||
|
- **`c_nostr_relay.db`** - SQLite database file (created after running init.sh)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. **Initialize the database:**
|
||||||
|
```bash
|
||||||
|
cd db
|
||||||
|
./init.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Force reinitialize (removes existing database):**
|
||||||
|
```bash
|
||||||
|
./init.sh --force
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Initialize with optimization and info:**
|
||||||
|
```bash
|
||||||
|
./init.sh --info --optimize
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
The schema is fully compatible with the Nostr protocol and includes:
|
||||||
|
|
||||||
|
### Core Tables
|
||||||
|
|
||||||
|
- **`event`** - Main event storage with all Nostr event data
|
||||||
|
- **`tag`** - Denormalized tag index for efficient queries
|
||||||
|
- **`user_verification`** - NIP-05 verification tracking
|
||||||
|
- **`account`** - User account management (optional)
|
||||||
|
- **`invoice`** - Lightning payment tracking (optional)
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
- ✅ **NIP-01 compliant** - Full basic protocol support
|
||||||
|
- ✅ **Replaceable events** - Supports kinds 0, 3, 10000-19999
|
||||||
|
- ✅ **Parameterized replaceable** - Supports kinds 30000-39999 with `d` tags
|
||||||
|
- ✅ **Event deletion** - NIP-09 soft deletion with `hidden` column
|
||||||
|
- ✅ **Event expiration** - NIP-40 automatic cleanup
|
||||||
|
- ✅ **Authentication** - NIP-42 client authentication
|
||||||
|
- ✅ **NIP-05 verification** - Domain-based identity verification
|
||||||
|
- ✅ **Performance optimized** - Comprehensive indexing strategy
|
||||||
|
|
||||||
|
### Schema Version
|
||||||
|
|
||||||
|
Current version: **v18** (compatible with nostr-rs-relay v18)
|
||||||
|
|
||||||
|
## Database Structure
|
||||||
|
|
||||||
|
### Event Storage
|
||||||
|
```sql
|
||||||
|
CREATE TABLE event (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
event_hash BLOB NOT NULL, -- 32-byte SHA256 hash
|
||||||
|
first_seen INTEGER NOT NULL, -- relay receive timestamp
|
||||||
|
created_at INTEGER NOT NULL, -- event creation timestamp
|
||||||
|
expires_at INTEGER, -- NIP-40 expiration
|
||||||
|
author BLOB NOT NULL, -- 32-byte pubkey
|
||||||
|
delegated_by BLOB, -- NIP-26 delegator
|
||||||
|
kind INTEGER NOT NULL, -- event kind
|
||||||
|
hidden INTEGER DEFAULT FALSE, -- soft deletion flag
|
||||||
|
content TEXT NOT NULL -- complete JSON event
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tag Indexing
|
||||||
|
```sql
|
||||||
|
CREATE TABLE tag (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
event_id INTEGER NOT NULL,
|
||||||
|
name TEXT, -- tag name ("e", "p", etc.)
|
||||||
|
value TEXT, -- tag value
|
||||||
|
created_at INTEGER NOT NULL, -- denormalized for performance
|
||||||
|
kind INTEGER NOT NULL -- denormalized for performance
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Features
|
||||||
|
|
||||||
|
### Optimized Indexes
|
||||||
|
- **Hash-based lookups** - `event_hash_index` for O(1) event retrieval
|
||||||
|
- **Author queries** - `author_index`, `author_created_at_index`
|
||||||
|
- **Kind filtering** - `kind_index`, `kind_created_at_index`
|
||||||
|
- **Tag searching** - `tag_covering_index` for efficient tag queries
|
||||||
|
- **Composite queries** - Multi-column indexes for complex filters
|
||||||
|
|
||||||
|
### Query Optimization
|
||||||
|
- **Denormalized tags** - Includes `kind` and `created_at` in tag table
|
||||||
|
- **Binary storage** - BLOBs for hex data (pubkeys, hashes)
|
||||||
|
- **WAL mode** - Write-Ahead Logging for concurrent access
|
||||||
|
- **Automatic cleanup** - Triggers for data integrity
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic Operations
|
||||||
|
|
||||||
|
1. **Insert an event:**
|
||||||
|
```sql
|
||||||
|
INSERT INTO event (event_hash, first_seen, created_at, author, kind, content)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Query by author:**
|
||||||
|
```sql
|
||||||
|
SELECT content FROM event
|
||||||
|
WHERE author = ? AND hidden != TRUE
|
||||||
|
ORDER BY created_at DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Filter by tags:**
|
||||||
|
```sql
|
||||||
|
SELECT e.content FROM event e
|
||||||
|
JOIN tag t ON e.id = t.event_id
|
||||||
|
WHERE t.name = 'p' AND t.value = ? AND e.hidden != TRUE;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Queries
|
||||||
|
|
||||||
|
1. **Get replaceable event (latest only):**
|
||||||
|
```sql
|
||||||
|
SELECT content FROM event
|
||||||
|
WHERE author = ? AND kind = ? AND hidden != TRUE
|
||||||
|
ORDER BY created_at DESC LIMIT 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Tag-based filtering (NIP-01 filters):**
|
||||||
|
```sql
|
||||||
|
SELECT e.content FROM event e
|
||||||
|
WHERE e.id IN (
|
||||||
|
SELECT t.event_id FROM tag t
|
||||||
|
WHERE t.name = ? AND t.value IN (?, ?, ?)
|
||||||
|
) AND e.hidden != TRUE;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### Regular Operations
|
||||||
|
|
||||||
|
1. **Check database integrity:**
|
||||||
|
```bash
|
||||||
|
sqlite3 c_nostr_relay.db "PRAGMA integrity_check;"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Optimize database:**
|
||||||
|
```bash
|
||||||
|
sqlite3 c_nostr_relay.db "PRAGMA optimize; VACUUM; ANALYZE;"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Clean expired events:**
|
||||||
|
```sql
|
||||||
|
DELETE FROM event WHERE expires_at <= strftime('%s', 'now');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
|
||||||
|
1. **Database size:**
|
||||||
|
```bash
|
||||||
|
ls -lh c_nostr_relay.db
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Table statistics:**
|
||||||
|
```sql
|
||||||
|
SELECT name, COUNT(*) as count FROM (
|
||||||
|
SELECT 'events' as name FROM event UNION ALL
|
||||||
|
SELECT 'tags' as name FROM tag UNION ALL
|
||||||
|
SELECT 'verifications' as name FROM user_verification
|
||||||
|
) GROUP BY name;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Support
|
||||||
|
|
||||||
|
The schema includes a migration system for future updates:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE schema_info (
|
||||||
|
version INTEGER PRIMARY KEY,
|
||||||
|
applied_at INTEGER NOT NULL,
|
||||||
|
description TEXT
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Input validation** - Always validate event JSON and signatures
|
||||||
|
2. **Rate limiting** - Implement at application level
|
||||||
|
3. **Access control** - Use `account` table for permissions
|
||||||
|
4. **Backup strategy** - Regular database backups recommended
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
- **SQLite version** - Requires SQLite 3.8.0+
|
||||||
|
- **nostr-rs-relay** - Schema compatible with v18
|
||||||
|
- **NIPs supported** - 01, 02, 05, 09, 10, 11, 26, 40, 42
|
||||||
|
- **C libraries** - Compatible with sqlite3 C API
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Database locked error:**
|
||||||
|
- Ensure proper connection closing in your C code
|
||||||
|
- Check for long-running transactions
|
||||||
|
|
||||||
|
2. **Performance issues:**
|
||||||
|
- Run `PRAGMA optimize;` regularly
|
||||||
|
- Consider `VACUUM` if database grew significantly
|
||||||
|
|
||||||
|
3. **Schema errors:**
|
||||||
|
- Verify SQLite version compatibility
|
||||||
|
- Check foreign key constraints
|
||||||
|
|
||||||
|
### Getting Help
|
||||||
|
|
||||||
|
- Check the main project README for C implementation details
|
||||||
|
- Review nostr-rs-relay documentation for reference implementation
|
||||||
|
- Consult Nostr NIPs for protocol specifications
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This database schema is part of the C Nostr Relay project and follows the same license terms.
|
||||||
BIN
db/c_nostr_relay.db
Normal file
BIN
db/c_nostr_relay.db
Normal file
Binary file not shown.
BIN
db/c_nostr_relay.db-shm
Normal file
BIN
db/c_nostr_relay.db-shm
Normal file
Binary file not shown.
BIN
db/c_nostr_relay.db-wal
Normal file
BIN
db/c_nostr_relay.db-wal
Normal file
Binary file not shown.
234
db/init.sh
Executable file
234
db/init.sh
Executable file
@@ -0,0 +1,234 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# C Nostr Relay Database Initialization Script
|
||||||
|
# Creates and initializes the SQLite database with proper schema
|
||||||
|
|
||||||
|
set -e # Exit on any error
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
DB_DIR="$(dirname "$0")"
|
||||||
|
DB_NAME="c_nostr_relay.db"
|
||||||
|
DB_PATH="${DB_DIR}/${DB_NAME}"
|
||||||
|
SCHEMA_FILE="${DB_DIR}/schema.sql"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Logging functions
|
||||||
|
log_info() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warning() {
|
||||||
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if SQLite3 is installed
|
||||||
|
check_sqlite() {
|
||||||
|
if ! command -v sqlite3 &> /dev/null; then
|
||||||
|
log_error "sqlite3 is not installed. Please install it first:"
|
||||||
|
echo " Ubuntu/Debian: sudo apt-get install sqlite3"
|
||||||
|
echo " CentOS/RHEL: sudo yum install sqlite"
|
||||||
|
echo " macOS: brew install sqlite3"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local version=$(sqlite3 --version | cut -d' ' -f1)
|
||||||
|
log_info "Using SQLite version: $version"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create database directory if it doesn't exist
|
||||||
|
create_db_directory() {
|
||||||
|
if [ ! -d "$DB_DIR" ]; then
|
||||||
|
log_info "Creating database directory: $DB_DIR"
|
||||||
|
mkdir -p "$DB_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Backup existing database if it exists
|
||||||
|
backup_existing_db() {
|
||||||
|
if [ -f "$DB_PATH" ]; then
|
||||||
|
local backup_path="${DB_PATH}.backup.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
log_warning "Existing database found. Creating backup: $backup_path"
|
||||||
|
cp "$DB_PATH" "$backup_path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialize the database with schema
|
||||||
|
init_database() {
|
||||||
|
log_info "Initializing database: $DB_PATH"
|
||||||
|
|
||||||
|
if [ ! -f "$SCHEMA_FILE" ]; then
|
||||||
|
log_error "Schema file not found: $SCHEMA_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove existing database if --force flag is used
|
||||||
|
if [ "$1" = "--force" ] && [ -f "$DB_PATH" ]; then
|
||||||
|
log_warning "Force flag detected. Removing existing database."
|
||||||
|
rm -f "$DB_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create the database and apply schema
|
||||||
|
log_info "Applying schema from: $SCHEMA_FILE"
|
||||||
|
if sqlite3 "$DB_PATH" < "$SCHEMA_FILE"; then
|
||||||
|
log_success "Database schema applied successfully"
|
||||||
|
else
|
||||||
|
log_error "Failed to apply database schema"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify database integrity
|
||||||
|
verify_database() {
|
||||||
|
log_info "Verifying database integrity..."
|
||||||
|
|
||||||
|
# Check if database file exists and is not empty
|
||||||
|
if [ ! -s "$DB_PATH" ]; then
|
||||||
|
log_error "Database file is empty or doesn't exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run SQLite integrity check
|
||||||
|
local integrity_result=$(sqlite3 "$DB_PATH" "PRAGMA integrity_check;")
|
||||||
|
if [ "$integrity_result" = "ok" ]; then
|
||||||
|
log_success "Database integrity check passed"
|
||||||
|
else
|
||||||
|
log_error "Database integrity check failed: $integrity_result"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify schema version
|
||||||
|
local schema_version=$(sqlite3 "$DB_PATH" "PRAGMA user_version;")
|
||||||
|
log_info "Database schema version: $schema_version"
|
||||||
|
|
||||||
|
# Check that main tables exist
|
||||||
|
local table_count=$(sqlite3 "$DB_PATH" "SELECT count(*) FROM sqlite_master WHERE type='table' AND name IN ('event', 'tag');")
|
||||||
|
if [ "$table_count" -eq 2 ]; then
|
||||||
|
log_success "Core tables created successfully"
|
||||||
|
else
|
||||||
|
log_error "Missing core tables (expected 2, found $table_count)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Display database information
|
||||||
|
show_db_info() {
|
||||||
|
log_info "Database Information:"
|
||||||
|
echo " Location: $DB_PATH"
|
||||||
|
echo " Size: $(du -h "$DB_PATH" | cut -f1)"
|
||||||
|
|
||||||
|
log_info "Database Tables:"
|
||||||
|
sqlite3 "$DB_PATH" "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;" | sed 's/^/ - /'
|
||||||
|
|
||||||
|
log_info "Database Indexes:"
|
||||||
|
sqlite3 "$DB_PATH" "SELECT name FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%' ORDER BY name;" | sed 's/^/ - /'
|
||||||
|
|
||||||
|
log_info "Database Views:"
|
||||||
|
sqlite3 "$DB_PATH" "SELECT name FROM sqlite_master WHERE type='view' ORDER BY name;" | sed 's/^/ - /'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run database optimization
|
||||||
|
optimize_database() {
|
||||||
|
log_info "Running database optimization..."
|
||||||
|
sqlite3 "$DB_PATH" "PRAGMA optimize; VACUUM; ANALYZE;"
|
||||||
|
log_success "Database optimization completed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print usage information
|
||||||
|
print_usage() {
|
||||||
|
echo "Usage: $0 [OPTIONS]"
|
||||||
|
echo ""
|
||||||
|
echo "Initialize SQLite database for C Nostr Relay"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " --force Remove existing database before initialization"
|
||||||
|
echo " --info Show database information after initialization"
|
||||||
|
echo " --optimize Run database optimization after initialization"
|
||||||
|
echo " --help Show this help message"
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 # Initialize database (with backup if exists)"
|
||||||
|
echo " $0 --force # Force reinitialize database"
|
||||||
|
echo " $0 --info --optimize # Initialize with info and optimization"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main execution
|
||||||
|
main() {
|
||||||
|
local force_flag=false
|
||||||
|
local show_info=false
|
||||||
|
local optimize=false
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--force)
|
||||||
|
force_flag=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--info)
|
||||||
|
show_info=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--optimize)
|
||||||
|
optimize=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--help)
|
||||||
|
print_usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_error "Unknown option: $1"
|
||||||
|
print_usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
log_info "Starting C Nostr Relay database initialization..."
|
||||||
|
|
||||||
|
# Execute initialization steps
|
||||||
|
check_sqlite
|
||||||
|
create_db_directory
|
||||||
|
|
||||||
|
if [ "$force_flag" = false ]; then
|
||||||
|
backup_existing_db
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$force_flag" = true ]; then
|
||||||
|
init_database --force
|
||||||
|
else
|
||||||
|
init_database
|
||||||
|
fi
|
||||||
|
|
||||||
|
verify_database
|
||||||
|
|
||||||
|
if [ "$optimize" = true ]; then
|
||||||
|
optimize_database
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$show_info" = true ]; then
|
||||||
|
show_db_info
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Database initialization completed successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "Database ready at: $DB_PATH"
|
||||||
|
echo "You can now start your C Nostr Relay application."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Execute main function with all arguments
|
||||||
|
main "$@"
|
||||||
66
db/schema.sql
Normal file
66
db/schema.sql
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
-- C Nostr Relay Database Schema
|
||||||
|
-- Simplified schema with just event and tag tables
|
||||||
|
-- SQLite database for storing Nostr events
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- DATABASE SETTINGS
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
PRAGMA encoding = "UTF-8";
|
||||||
|
PRAGMA journal_mode = WAL;
|
||||||
|
PRAGMA auto_vacuum = FULL;
|
||||||
|
PRAGMA synchronous = NORMAL;
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- EVENT TABLE
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Main event table - stores all Nostr events
|
||||||
|
CREATE TABLE IF NOT EXISTS event (
|
||||||
|
id TEXT PRIMARY KEY, -- Nostr event ID (hex string)
|
||||||
|
pubkey TEXT NOT NULL, -- Public key of event author (hex string)
|
||||||
|
created_at INTEGER NOT NULL, -- Event creation timestamp (Unix timestamp)
|
||||||
|
kind INTEGER NOT NULL, -- Event kind (0-65535)
|
||||||
|
content TEXT NOT NULL, -- Event content (text content only)
|
||||||
|
sig TEXT NOT NULL -- Event signature (hex string)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Event indexes for performance
|
||||||
|
CREATE INDEX IF NOT EXISTS event_pubkey_index ON event(pubkey);
|
||||||
|
CREATE INDEX IF NOT EXISTS event_created_at_index ON event(created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS event_kind_index ON event(kind);
|
||||||
|
CREATE INDEX IF NOT EXISTS event_pubkey_created_at_index ON event(pubkey, created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS event_kind_created_at_index ON event(kind, created_at);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- TAG TABLE
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Tag table for storing event tags
|
||||||
|
CREATE TABLE IF NOT EXISTS tag (
|
||||||
|
id TEXT NOT NULL, -- Nostr event ID (references event.id)
|
||||||
|
name TEXT NOT NULL, -- Tag name (e.g., "e", "p", "d")
|
||||||
|
value TEXT NOT NULL, -- Tag value
|
||||||
|
parameters TEXT, -- Additional tag parameters (JSON string)
|
||||||
|
FOREIGN KEY(id) REFERENCES event(id) ON UPDATE CASCADE ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tag indexes for performance
|
||||||
|
CREATE INDEX IF NOT EXISTS tag_id_index ON tag(id);
|
||||||
|
CREATE INDEX IF NOT EXISTS tag_name_index ON tag(name);
|
||||||
|
CREATE INDEX IF NOT EXISTS tag_name_value_index ON tag(name, value);
|
||||||
|
CREATE INDEX IF NOT EXISTS tag_id_name_index ON tag(id, name);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- PERFORMANCE OPTIMIZATIONS
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Enable query planner optimizations
|
||||||
|
PRAGMA optimize;
|
||||||
|
|
||||||
|
-- Set recommended pragmas for performance
|
||||||
|
PRAGMA main.synchronous = NORMAL;
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
PRAGMA temp_store = 2; -- use memory for temp tables
|
||||||
|
PRAGMA main.cache_size = 10000; -- 40MB cache per connection
|
||||||
94
make_and_restart_relay.sh
Executable file
94
make_and_restart_relay.sh
Executable file
@@ -0,0 +1,94 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# C-Relay Build and Restart Script
|
||||||
|
# Builds the project first, then stops any running relay and starts a new one in the background
|
||||||
|
|
||||||
|
echo "=== C Nostr Relay Build and Restart Script ==="
|
||||||
|
|
||||||
|
# Build the project first
|
||||||
|
echo "Building project..."
|
||||||
|
make clean all
|
||||||
|
|
||||||
|
# Check if build was successful
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "ERROR: Build failed. Cannot restart relay."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if relay binary exists after build
|
||||||
|
if [ ! -f "./src/main" ]; then
|
||||||
|
echo "ERROR: Relay binary not found after build. Build may have failed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Build successful. Proceeding with relay restart..."
|
||||||
|
|
||||||
|
# Kill existing relay if running
|
||||||
|
echo "Stopping any existing relay servers..."
|
||||||
|
pkill -f "./src/main" 2>/dev/null
|
||||||
|
sleep 2 # Give time for shutdown
|
||||||
|
|
||||||
|
# Check if port is still bound
|
||||||
|
if lsof -i :8888 >/dev/null 2>&1; then
|
||||||
|
echo "Port 8888 still in use, force killing..."
|
||||||
|
fuser -k 8888/tcp 2>/dev/null || echo "No process on port 8888"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get any remaining processes
|
||||||
|
REMAINING_PIDS=$(pgrep -f "./src/main" || echo "")
|
||||||
|
if [ -n "$REMAINING_PIDS" ]; then
|
||||||
|
echo "Force killing remaining processes: $REMAINING_PIDS"
|
||||||
|
kill -9 $REMAINING_PIDS 2>/dev/null
|
||||||
|
sleep 1
|
||||||
|
else
|
||||||
|
echo "No existing relay found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up PID file
|
||||||
|
rm -f relay.pid
|
||||||
|
|
||||||
|
# Initialize database if needed
|
||||||
|
if [ ! -f "./db/c_nostr_relay.db" ]; then
|
||||||
|
echo "Initializing database..."
|
||||||
|
./db/init.sh --force >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start relay in background with output redirection
|
||||||
|
echo "Starting relay server..."
|
||||||
|
echo "Debug: Current processes: $(ps aux | grep './src/main' | grep -v grep || echo 'None')"
|
||||||
|
|
||||||
|
# Start relay in background and capture its PID
|
||||||
|
./src/main > relay.log 2>&1 &
|
||||||
|
RELAY_PID=$!
|
||||||
|
|
||||||
|
echo "Started with PID: $RELAY_PID"
|
||||||
|
|
||||||
|
# Check if server is still running after short delay
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Check if process is still alive
|
||||||
|
if ps -p "$RELAY_PID" >/dev/null 2>&1; then
|
||||||
|
echo "Relay started successfully!"
|
||||||
|
echo "PID: $RELAY_PID"
|
||||||
|
echo "WebSocket endpoint: ws://127.0.0.1:8888"
|
||||||
|
echo "Log file: relay.log"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Save PID for debugging
|
||||||
|
echo $RELAY_PID > relay.pid
|
||||||
|
|
||||||
|
echo "=== Relay server running in background ==="
|
||||||
|
echo "To kill relay: pkill -f './src/main'"
|
||||||
|
echo "To check status: ps aux | grep src/main"
|
||||||
|
echo "To view logs: tail -f relay.log"
|
||||||
|
echo "Ready for Nostr client connections!"
|
||||||
|
else
|
||||||
|
echo "ERROR: Relay failed to start"
|
||||||
|
echo "Debug: Check relay.log for error details:"
|
||||||
|
echo "--- Last 10 lines of relay.log ---"
|
||||||
|
tail -n 10 relay.log 2>/dev/null || echo "No log file found"
|
||||||
|
echo "--- End log ---"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
1
nips
Submodule
1
nips
Submodule
Submodule nips added at 8c45ff5d96
1
nostr_core_lib
Submodule
1
nostr_core_lib
Submodule
Submodule nostr_core_lib added at 33129d82fd
11
relay.log
Normal file
11
relay.log
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[34m[1m=== C Nostr Relay Server ===[0m
|
||||||
|
[32m[SUCCESS][0m Database connection established
|
||||||
|
[34m[INFO][0m Starting relay server...
|
||||||
|
[34m[INFO][0m Starting libwebsockets-based Nostr relay server...
|
||||||
|
[32m[SUCCESS][0m WebSocket relay started on ws://127.0.0.1:8888
|
||||||
|
[34m[INFO][0m WebSocket connection established
|
||||||
|
[34m[INFO][0m Received WebSocket message
|
||||||
|
[34m[INFO][0m Handling EVENT message
|
||||||
|
[32m[SUCCESS][0m Event stored in database
|
||||||
|
[32m[SUCCESS][0m Event stored successfully
|
||||||
|
[34m[INFO][0m WebSocket connection closed
|
||||||
562
src/main.c
Normal file
562
src/main.c
Normal file
@@ -0,0 +1,562 @@
|
|||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#include <libwebsockets.h>
|
||||||
|
|
||||||
|
// Include nostr_core_lib for Nostr functionality
|
||||||
|
#include "../nostr_core_lib/cjson/cJSON.h"
|
||||||
|
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
#define DEFAULT_PORT 8888
|
||||||
|
#define DEFAULT_HOST "127.0.0.1"
|
||||||
|
#define DATABASE_PATH "db/c_nostr_relay.db"
|
||||||
|
#define MAX_CLIENTS 100
|
||||||
|
|
||||||
|
// Global state
|
||||||
|
static sqlite3* g_db = NULL;
|
||||||
|
static int g_server_running = 1;
|
||||||
|
|
||||||
|
// Color constants for logging
|
||||||
|
#define RED "\033[31m"
|
||||||
|
#define GREEN "\033[32m"
|
||||||
|
#define YELLOW "\033[33m"
|
||||||
|
#define BLUE "\033[34m"
|
||||||
|
#define BOLD "\033[1m"
|
||||||
|
#define RESET "\033[0m"
|
||||||
|
|
||||||
|
// Logging functions
|
||||||
|
void log_info(const char* message) {
|
||||||
|
printf(BLUE "[INFO]" RESET " %s\n", message);
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
void log_success(const char* message) {
|
||||||
|
printf(GREEN "[SUCCESS]" RESET " %s\n", message);
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
void log_error(const char* message) {
|
||||||
|
printf(RED "[ERROR]" RESET " %s\n", message);
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
void log_warning(const char* message) {
|
||||||
|
printf(YELLOW "[WARNING]" RESET " %s\n", message);
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal handler for graceful shutdown
|
||||||
|
void signal_handler(int sig) {
|
||||||
|
if (sig == SIGINT || sig == SIGTERM) {
|
||||||
|
log_info("Received shutdown signal");
|
||||||
|
g_server_running = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize database connection
|
||||||
|
int init_database() {
|
||||||
|
int rc = sqlite3_open(DATABASE_PATH, &g_db);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
log_error("Cannot open database");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success("Database connection established");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close database connection
|
||||||
|
void close_database() {
|
||||||
|
if (g_db) {
|
||||||
|
sqlite3_close(g_db);
|
||||||
|
g_db = NULL;
|
||||||
|
log_info("Database connection closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store event in database
|
||||||
|
int store_event(cJSON* event) {
|
||||||
|
if (!g_db || !event) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract event fields
|
||||||
|
cJSON* id = cJSON_GetObjectItem(event, "id");
|
||||||
|
cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey");
|
||||||
|
cJSON* created_at = cJSON_GetObjectItem(event, "created_at");
|
||||||
|
cJSON* kind = cJSON_GetObjectItem(event, "kind");
|
||||||
|
cJSON* content = cJSON_GetObjectItem(event, "content");
|
||||||
|
cJSON* sig = cJSON_GetObjectItem(event, "sig");
|
||||||
|
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||||
|
|
||||||
|
if (!id || !pubkey || !created_at || !kind || !content || !sig) {
|
||||||
|
log_error("Invalid event - missing required fields");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare SQL statement for event insertion
|
||||||
|
const char* sql =
|
||||||
|
"INSERT INTO event (id, pubkey, created_at, kind, content, sig) "
|
||||||
|
"VALUES (?, ?, ?, ?, ?, ?)";
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
log_error("Failed to prepare event insert statement");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind parameters
|
||||||
|
sqlite3_bind_text(stmt, 1, cJSON_GetStringValue(id), -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(pubkey), -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_int64(stmt, 3, (sqlite3_int64)cJSON_GetNumberValue(created_at));
|
||||||
|
sqlite3_bind_int(stmt, 4, (int)cJSON_GetNumberValue(kind));
|
||||||
|
sqlite3_bind_text(stmt, 5, cJSON_GetStringValue(content), -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 6, cJSON_GetStringValue(sig), -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
// Execute statement
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
|
||||||
|
if (rc != SQLITE_DONE) {
|
||||||
|
if (rc == SQLITE_CONSTRAINT) {
|
||||||
|
log_warning("Event already exists in database");
|
||||||
|
return 0; // Not an error, just duplicate
|
||||||
|
}
|
||||||
|
char error_msg[256];
|
||||||
|
snprintf(error_msg, sizeof(error_msg), "Failed to insert event: %s", sqlite3_errmsg(g_db));
|
||||||
|
log_error(error_msg);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert tags if present
|
||||||
|
if (tags && cJSON_IsArray(tags)) {
|
||||||
|
const char* event_id = cJSON_GetStringValue(id);
|
||||||
|
cJSON* tag;
|
||||||
|
cJSON_ArrayForEach(tag, tags) {
|
||||||
|
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
|
||||||
|
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||||
|
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
|
||||||
|
|
||||||
|
if (cJSON_IsString(tag_name) && cJSON_IsString(tag_value)) {
|
||||||
|
// Collect additional tag parameters if present
|
||||||
|
char* parameters = NULL;
|
||||||
|
if (cJSON_GetArraySize(tag) > 2) {
|
||||||
|
cJSON* params_array = cJSON_CreateArray();
|
||||||
|
for (int i = 2; i < cJSON_GetArraySize(tag); i++) {
|
||||||
|
cJSON_AddItemToArray(params_array, cJSON_Duplicate(cJSON_GetArrayItem(tag, i), 1));
|
||||||
|
}
|
||||||
|
parameters = cJSON_Print(params_array);
|
||||||
|
cJSON_Delete(params_array);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* tag_sql =
|
||||||
|
"INSERT INTO tag (id, name, value, parameters) VALUES (?, ?, ?, ?)";
|
||||||
|
|
||||||
|
sqlite3_stmt* tag_stmt;
|
||||||
|
rc = sqlite3_prepare_v2(g_db, tag_sql, -1, &tag_stmt, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
sqlite3_bind_text(tag_stmt, 1, event_id, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(tag_stmt, 2, cJSON_GetStringValue(tag_name), -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(tag_stmt, 3, cJSON_GetStringValue(tag_value), -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(tag_stmt, 4, parameters, -1, SQLITE_TRANSIENT);
|
||||||
|
|
||||||
|
sqlite3_step(tag_stmt);
|
||||||
|
sqlite3_finalize(tag_stmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameters) free(parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success("Event stored in database");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve event from database
|
||||||
|
cJSON* retrieve_event(const char* event_id) {
|
||||||
|
if (!g_db || !event_id) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* sql =
|
||||||
|
"SELECT id, pubkey, created_at, kind, content, sig FROM event WHERE id = ?";
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
cJSON* event = NULL;
|
||||||
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
|
event = cJSON_CreateObject();
|
||||||
|
|
||||||
|
cJSON_AddStringToObject(event, "id", (char*)sqlite3_column_text(stmt, 0));
|
||||||
|
cJSON_AddStringToObject(event, "pubkey", (char*)sqlite3_column_text(stmt, 1));
|
||||||
|
cJSON_AddNumberToObject(event, "created_at", sqlite3_column_int64(stmt, 2));
|
||||||
|
cJSON_AddNumberToObject(event, "kind", sqlite3_column_int(stmt, 3));
|
||||||
|
cJSON_AddStringToObject(event, "content", (char*)sqlite3_column_text(stmt, 4));
|
||||||
|
cJSON_AddStringToObject(event, "sig", (char*)sqlite3_column_text(stmt, 5));
|
||||||
|
|
||||||
|
// Add tags array - retrieve from tag table
|
||||||
|
cJSON* tags_array = cJSON_CreateArray();
|
||||||
|
|
||||||
|
const char* tag_sql = "SELECT name, value, parameters FROM tag WHERE id = ?";
|
||||||
|
sqlite3_stmt* tag_stmt;
|
||||||
|
if (sqlite3_prepare_v2(g_db, tag_sql, -1, &tag_stmt, NULL) == SQLITE_OK) {
|
||||||
|
sqlite3_bind_text(tag_stmt, 1, event_id, -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
while (sqlite3_step(tag_stmt) == SQLITE_ROW) {
|
||||||
|
cJSON* tag = cJSON_CreateArray();
|
||||||
|
cJSON_AddItemToArray(tag, cJSON_CreateString((char*)sqlite3_column_text(tag_stmt, 0)));
|
||||||
|
cJSON_AddItemToArray(tag, cJSON_CreateString((char*)sqlite3_column_text(tag_stmt, 1)));
|
||||||
|
|
||||||
|
// Add parameters if they exist
|
||||||
|
const char* parameters = (char*)sqlite3_column_text(tag_stmt, 2);
|
||||||
|
if (parameters && strlen(parameters) > 0) {
|
||||||
|
cJSON* params = cJSON_Parse(parameters);
|
||||||
|
if (params && cJSON_IsArray(params)) {
|
||||||
|
int param_count = cJSON_GetArraySize(params);
|
||||||
|
for (int i = 0; i < param_count; i++) {
|
||||||
|
cJSON* param = cJSON_GetArrayItem(params, i);
|
||||||
|
cJSON_AddItemToArray(tag, cJSON_Duplicate(param, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (params) cJSON_Delete(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddItemToArray(tags_array, tag);
|
||||||
|
}
|
||||||
|
sqlite3_finalize(tag_stmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddItemToObject(event, "tags", tags_array);
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle REQ message (subscription)
|
||||||
|
int handle_req_message(const char* sub_id, cJSON* filters) {
|
||||||
|
log_info("Handling REQ message");
|
||||||
|
|
||||||
|
// For now, just handle simple event ID requests
|
||||||
|
if (cJSON_IsArray(filters)) {
|
||||||
|
cJSON* filter = cJSON_GetArrayItem(filters, 0);
|
||||||
|
if (filter) {
|
||||||
|
cJSON* ids = cJSON_GetObjectItem(filter, "ids");
|
||||||
|
if (ids && cJSON_IsArray(ids)) {
|
||||||
|
cJSON* event_id = cJSON_GetArrayItem(ids, 0);
|
||||||
|
if (event_id && cJSON_IsString(event_id)) {
|
||||||
|
cJSON* event = retrieve_event(cJSON_GetStringValue(event_id));
|
||||||
|
if (event) {
|
||||||
|
log_success("Found event for subscription");
|
||||||
|
cJSON_Delete(event);
|
||||||
|
return 1; // Found event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0; // No events found
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle EVENT message (publish)
|
||||||
|
int handle_event_message(cJSON* event) {
|
||||||
|
log_info("Handling EVENT message");
|
||||||
|
|
||||||
|
// Validate event structure (basic check)
|
||||||
|
cJSON* id = cJSON_GetObjectItem(event, "id");
|
||||||
|
if (!id || !cJSON_IsString(id)) {
|
||||||
|
log_error("Invalid event - no ID");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store event in database
|
||||||
|
if (store_event(event) == 0) {
|
||||||
|
log_success("Event stored successfully");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global WebSocket context
|
||||||
|
static struct lws_context *ws_context = NULL;
|
||||||
|
|
||||||
|
// Per-session data structure
|
||||||
|
struct per_session_data {
|
||||||
|
int authenticated;
|
||||||
|
char subscription_id[64];
|
||||||
|
};
|
||||||
|
|
||||||
|
// WebSocket callback function for Nostr relay protocol
|
||||||
|
static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reason,
|
||||||
|
void *user, void *in, size_t len) {
|
||||||
|
struct per_session_data *pss = (struct per_session_data *)user;
|
||||||
|
|
||||||
|
switch (reason) {
|
||||||
|
case LWS_CALLBACK_ESTABLISHED:
|
||||||
|
log_info("WebSocket connection established");
|
||||||
|
memset(pss, 0, sizeof(*pss));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LWS_CALLBACK_RECEIVE:
|
||||||
|
if (len > 0) {
|
||||||
|
char *message = malloc(len + 1);
|
||||||
|
if (message) {
|
||||||
|
memcpy(message, in, len);
|
||||||
|
message[len] = '\0';
|
||||||
|
|
||||||
|
log_info("Received WebSocket message");
|
||||||
|
|
||||||
|
// Parse JSON message
|
||||||
|
cJSON* json = cJSON_Parse(message);
|
||||||
|
if (json && cJSON_IsArray(json)) {
|
||||||
|
// Get message type
|
||||||
|
cJSON* type = cJSON_GetArrayItem(json, 0);
|
||||||
|
if (type && cJSON_IsString(type)) {
|
||||||
|
const char* msg_type = cJSON_GetStringValue(type);
|
||||||
|
|
||||||
|
if (strcmp(msg_type, "EVENT") == 0) {
|
||||||
|
// Handle EVENT message
|
||||||
|
cJSON* event = cJSON_GetArrayItem(json, 1);
|
||||||
|
if (event && cJSON_IsObject(event)) {
|
||||||
|
int result = handle_event_message(event);
|
||||||
|
|
||||||
|
// Send OK response
|
||||||
|
cJSON* event_id = cJSON_GetObjectItem(event, "id");
|
||||||
|
if (event_id && cJSON_IsString(event_id)) {
|
||||||
|
cJSON* response = cJSON_CreateArray();
|
||||||
|
cJSON_AddItemToArray(response, cJSON_CreateString("OK"));
|
||||||
|
cJSON_AddItemToArray(response, cJSON_CreateString(cJSON_GetStringValue(event_id)));
|
||||||
|
cJSON_AddItemToArray(response, cJSON_CreateBool(result == 0));
|
||||||
|
cJSON_AddItemToArray(response, cJSON_CreateString(result == 0 ? "" : "error: failed to store event"));
|
||||||
|
|
||||||
|
char *response_str = cJSON_Print(response);
|
||||||
|
if (response_str) {
|
||||||
|
size_t response_len = strlen(response_str);
|
||||||
|
unsigned char *buf = malloc(LWS_PRE + response_len);
|
||||||
|
if (buf) {
|
||||||
|
memcpy(buf + LWS_PRE, response_str, response_len);
|
||||||
|
lws_write(wsi, buf + LWS_PRE, response_len, LWS_WRITE_TEXT);
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
free(response_str);
|
||||||
|
}
|
||||||
|
cJSON_Delete(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (strcmp(msg_type, "REQ") == 0) {
|
||||||
|
// Handle REQ message
|
||||||
|
cJSON* sub_id = cJSON_GetArrayItem(json, 1);
|
||||||
|
cJSON* filters = cJSON_GetArrayItem(json, 2);
|
||||||
|
|
||||||
|
if (sub_id && cJSON_IsString(sub_id)) {
|
||||||
|
const char* subscription_id = cJSON_GetStringValue(sub_id);
|
||||||
|
strncpy(pss->subscription_id, subscription_id, sizeof(pss->subscription_id) - 1);
|
||||||
|
|
||||||
|
handle_req_message(subscription_id, filters);
|
||||||
|
|
||||||
|
// Send EOSE (End of Stored Events)
|
||||||
|
cJSON* eose_response = cJSON_CreateArray();
|
||||||
|
cJSON_AddItemToArray(eose_response, cJSON_CreateString("EOSE"));
|
||||||
|
cJSON_AddItemToArray(eose_response, cJSON_CreateString(subscription_id));
|
||||||
|
|
||||||
|
char *eose_str = cJSON_Print(eose_response);
|
||||||
|
if (eose_str) {
|
||||||
|
size_t eose_len = strlen(eose_str);
|
||||||
|
unsigned char *buf = malloc(LWS_PRE + eose_len);
|
||||||
|
if (buf) {
|
||||||
|
memcpy(buf + LWS_PRE, eose_str, eose_len);
|
||||||
|
lws_write(wsi, buf + LWS_PRE, eose_len, LWS_WRITE_TEXT);
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
free(eose_str);
|
||||||
|
}
|
||||||
|
cJSON_Delete(eose_response);
|
||||||
|
}
|
||||||
|
} else if (strcmp(msg_type, "CLOSE") == 0) {
|
||||||
|
// Handle CLOSE message
|
||||||
|
log_info("Subscription closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json) cJSON_Delete(json);
|
||||||
|
free(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LWS_CALLBACK_CLOSED:
|
||||||
|
log_info("WebSocket connection closed");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocket protocol definition
|
||||||
|
static struct lws_protocols protocols[] = {
|
||||||
|
{
|
||||||
|
"nostr-relay-protocol",
|
||||||
|
nostr_relay_callback,
|
||||||
|
sizeof(struct per_session_data),
|
||||||
|
4096, // rx buffer size
|
||||||
|
0, NULL, 0
|
||||||
|
},
|
||||||
|
{ NULL, NULL, 0, 0, 0, NULL, 0 } // terminator
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start libwebsockets-based WebSocket Nostr relay server
|
||||||
|
int start_websocket_relay() {
|
||||||
|
struct lws_context_creation_info info;
|
||||||
|
|
||||||
|
log_info("Starting libwebsockets-based Nostr relay server...");
|
||||||
|
|
||||||
|
memset(&info, 0, sizeof(info));
|
||||||
|
info.port = DEFAULT_PORT;
|
||||||
|
info.protocols = protocols;
|
||||||
|
info.gid = -1;
|
||||||
|
info.uid = -1;
|
||||||
|
|
||||||
|
// Minimal libwebsockets configuration
|
||||||
|
info.options = LWS_SERVER_OPTION_VALIDATE_UTF8;
|
||||||
|
|
||||||
|
// Remove interface restrictions - let system choose
|
||||||
|
// info.vhost_name = NULL;
|
||||||
|
// info.iface = NULL;
|
||||||
|
|
||||||
|
// Increase max connections for relay usage
|
||||||
|
info.max_http_header_pool = 16;
|
||||||
|
info.timeout_secs = 10;
|
||||||
|
|
||||||
|
// Max payload size for Nostr events
|
||||||
|
info.max_http_header_data = 4096;
|
||||||
|
|
||||||
|
ws_context = lws_create_context(&info);
|
||||||
|
if (!ws_context) {
|
||||||
|
log_error("Failed to create libwebsockets context");
|
||||||
|
perror("libwebsockets creation error");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success("WebSocket relay started on ws://127.0.0.1:8888");
|
||||||
|
|
||||||
|
// Main event loop with proper signal handling
|
||||||
|
fd_set rfds;
|
||||||
|
struct timeval tv;
|
||||||
|
|
||||||
|
while (g_server_running) {
|
||||||
|
FD_ZERO(&rfds);
|
||||||
|
tv.tv_sec = 1;
|
||||||
|
tv.tv_usec = 0;
|
||||||
|
|
||||||
|
int result = lws_service(ws_context, 1000);
|
||||||
|
|
||||||
|
if (result < 0) {
|
||||||
|
log_error("libwebsockets service error");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info("Shutting down WebSocket server...");
|
||||||
|
lws_context_destroy(ws_context);
|
||||||
|
ws_context = NULL;
|
||||||
|
|
||||||
|
log_success("WebSocket relay shut down cleanly");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print usage information
|
||||||
|
void print_usage(const char* program_name) {
|
||||||
|
printf("Usage: %s [OPTIONS]\n", program_name);
|
||||||
|
printf("\n");
|
||||||
|
printf("C Nostr Relay Server\n");
|
||||||
|
printf("\n");
|
||||||
|
printf("Options:\n");
|
||||||
|
printf(" -p, --port PORT Listen port (default: %d)\n", DEFAULT_PORT);
|
||||||
|
printf(" -h, --help Show this help message\n");
|
||||||
|
printf("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
int port = DEFAULT_PORT;
|
||||||
|
|
||||||
|
// Parse command line arguments
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 0;
|
||||||
|
} else if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) {
|
||||||
|
if (i + 1 < argc) {
|
||||||
|
port = atoi(argv[++i]);
|
||||||
|
if (port <= 0 || port > 65535) {
|
||||||
|
log_error("Invalid port number");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_error("Port argument requires a value");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_error("Unknown argument");
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up signal handlers
|
||||||
|
signal(SIGINT, signal_handler);
|
||||||
|
signal(SIGTERM, signal_handler);
|
||||||
|
|
||||||
|
printf(BLUE BOLD "=== C Nostr Relay Server ===" RESET "\n");
|
||||||
|
|
||||||
|
// Initialize database
|
||||||
|
if (init_database() != 0) {
|
||||||
|
log_error("Failed to initialize database");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize nostr library
|
||||||
|
if (nostr_init() != 0) {
|
||||||
|
log_error("Failed to initialize nostr library");
|
||||||
|
close_database();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info("Starting relay server...");
|
||||||
|
|
||||||
|
// Start WebSocket Nostr relay server
|
||||||
|
int result = start_websocket_relay();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
nostr_cleanup();
|
||||||
|
close_database();
|
||||||
|
|
||||||
|
if (result == 0) {
|
||||||
|
log_success("Server shutdown complete");
|
||||||
|
} else {
|
||||||
|
log_error("Server shutdown with errors");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
112
tests/1_nip_test.sh
Executable file
112
tests/1_nip_test.sh
Executable file
@@ -0,0 +1,112 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Simple C-Relay Test - Create type 1 event and upload to relay
|
||||||
|
# Uses nak to generate and publish a single event
|
||||||
|
|
||||||
|
set -e # Exit on any error
|
||||||
|
|
||||||
|
# Color constants
|
||||||
|
RED='\033[31m'
|
||||||
|
GREEN='\033[32m'
|
||||||
|
YELLOW='\033[33m'
|
||||||
|
BLUE='\033[34m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
RESET='\033[0m'
|
||||||
|
|
||||||
|
# Test configuration
|
||||||
|
RELAY_URL="ws://127.0.0.1:8888"
|
||||||
|
TEST_PRIVATE_KEY="nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99"
|
||||||
|
TEST_CONTENT="Hello from C-Relay test!"
|
||||||
|
|
||||||
|
# Print functions
|
||||||
|
print_header() {
|
||||||
|
echo -e "${BLUE}${BOLD}=== $1 ===${RESET}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_step() {
|
||||||
|
echo -e "${YELLOW}[STEP]${RESET} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}✓${RESET} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}✗${RESET} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_info() {
|
||||||
|
echo -e "${BLUE}[INFO]${RESET} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main test function
|
||||||
|
run_test() {
|
||||||
|
print_header "C-Relay Simple Test"
|
||||||
|
|
||||||
|
# Check if nak is available
|
||||||
|
print_step "Checking dependencies..."
|
||||||
|
if ! command -v nak &> /dev/null; then
|
||||||
|
print_error "nak command not found"
|
||||||
|
print_info "Please install nak: go install github.com/fiatjaf/nak@latest"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
print_success "nak found"
|
||||||
|
|
||||||
|
# Step 1: Create type 1 event with nak including tags
|
||||||
|
print_step "Creating type 1 event with nak and tags..."
|
||||||
|
|
||||||
|
local event_json
|
||||||
|
if ! event_json=$(nak event --sec "$TEST_PRIVATE_KEY" -c "$TEST_CONTENT" -k 1 --ts $(date +%s) -e "test_event_id" -p "test_pubkey" -t "subject=Test Event" 2>/dev/null); then
|
||||||
|
print_error "Failed to generate event with nak"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_success "Event created successfully"
|
||||||
|
print_header "FULL EVENT JSON"
|
||||||
|
echo "$event_json" | jq . 2>/dev/null || echo "$event_json"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Step 2: Upload to C-Relay
|
||||||
|
print_step "Uploading event to C-Relay at $RELAY_URL..."
|
||||||
|
|
||||||
|
# Create EVENT message in Nostr format
|
||||||
|
local event_message="[\"EVENT\",$event_json]"
|
||||||
|
|
||||||
|
# Use websocat or wscat to send to relay if available
|
||||||
|
local response=""
|
||||||
|
if command -v websocat &> /dev/null; then
|
||||||
|
print_info "Using websocat to connect to relay..."
|
||||||
|
response=$(echo "$event_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
|
||||||
|
elif command -v wscat &> /dev/null; then
|
||||||
|
print_info "Using wscat to connect to relay..."
|
||||||
|
response=$(echo "$event_message" | timeout 5s wscat -c "$RELAY_URL" 2>&1 || echo "Connection failed")
|
||||||
|
else
|
||||||
|
# Fallback: use nak publish
|
||||||
|
print_info "Using nak to publish event..."
|
||||||
|
response=$(echo "$event_json" | nak event --relay "$RELAY_URL" 2>&1 || echo "Publish failed")
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_header "FULL RELAY RESPONSE"
|
||||||
|
echo "$response"
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [[ "$response" == *"Connection failed"* ]] || [[ "$response" == *"Publish failed"* ]]; then
|
||||||
|
print_error "Failed to connect to relay or publish event"
|
||||||
|
print_info "Make sure the relay is running: ./make_and_restart_relay.sh"
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
print_success "Event uploaded to relay"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run the test
|
||||||
|
if run_test; then
|
||||||
|
echo
|
||||||
|
print_success "Test completed successfully"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo
|
||||||
|
print_error "Test failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user