Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c63fd04c92 |
Binary file not shown.
Binary file not shown.
313
clean_schema.sql
Normal file
313
clean_schema.sql
Normal file
@@ -0,0 +1,313 @@
|
||||
-- C Nostr Relay Database Schema
|
||||
-- SQLite schema for storing Nostr events with JSON tags support
|
||||
-- Configuration system using config table
|
||||
-- Schema version tracking
|
||||
PRAGMA user_version = 7;
|
||||
-- Enable foreign key support
|
||||
PRAGMA foreign_keys = ON;
|
||||
-- Optimize for performance
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA synchronous = NORMAL;
|
||||
PRAGMA cache_size = 10000;
|
||||
-- Core events table with hybrid single-table design
|
||||
CREATE TABLE events (
|
||||
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)
|
||||
event_type TEXT NOT NULL CHECK (event_type IN ('regular', 'replaceable', 'ephemeral', 'addressable')),
|
||||
content TEXT NOT NULL, -- Event content (text content only)
|
||||
sig TEXT NOT NULL, -- Event signature (hex string)
|
||||
tags JSON NOT NULL DEFAULT '[]', -- Event tags as JSON array
|
||||
first_seen INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) -- When relay received event
|
||||
);
|
||||
-- Core performance indexes
|
||||
CREATE INDEX idx_events_pubkey ON events(pubkey);
|
||||
CREATE INDEX idx_events_kind ON events(kind);
|
||||
CREATE INDEX idx_events_created_at ON events(created_at DESC);
|
||||
CREATE INDEX idx_events_event_type ON events(event_type);
|
||||
-- Composite indexes for common query patterns
|
||||
CREATE INDEX idx_events_kind_created_at ON events(kind, created_at DESC);
|
||||
CREATE INDEX idx_events_pubkey_created_at ON events(pubkey, created_at DESC);
|
||||
CREATE INDEX idx_events_pubkey_kind ON events(pubkey, kind);
|
||||
-- Schema information table
|
||||
CREATE TABLE schema_info (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
-- Insert schema metadata
|
||||
INSERT INTO schema_info (key, value) VALUES
|
||||
('version', '7'),
|
||||
('description', 'Hybrid Nostr relay schema with event-based and table-based configuration'),
|
||||
('created_at', strftime('%s', 'now'));
|
||||
-- Helper views for common queries
|
||||
CREATE VIEW recent_events AS
|
||||
SELECT id, pubkey, created_at, kind, event_type, content
|
||||
FROM events
|
||||
WHERE event_type != 'ephemeral'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1000;
|
||||
CREATE VIEW event_stats AS
|
||||
SELECT
|
||||
event_type,
|
||||
COUNT(*) as count,
|
||||
AVG(length(content)) as avg_content_length,
|
||||
MIN(created_at) as earliest,
|
||||
MAX(created_at) as latest
|
||||
FROM events
|
||||
GROUP BY event_type;
|
||||
-- Configuration events view (kind 33334)
|
||||
CREATE VIEW configuration_events AS
|
||||
SELECT
|
||||
id,
|
||||
pubkey as admin_pubkey,
|
||||
created_at,
|
||||
content,
|
||||
tags,
|
||||
sig
|
||||
FROM events
|
||||
WHERE kind = 33334
|
||||
ORDER BY created_at DESC;
|
||||
-- Optimization: Trigger for automatic cleanup of ephemeral events older than 1 hour
|
||||
CREATE TRIGGER cleanup_ephemeral_events
|
||||
AFTER INSERT ON events
|
||||
WHEN NEW.event_type = 'ephemeral'
|
||||
BEGIN
|
||||
DELETE FROM events
|
||||
WHERE event_type = 'ephemeral'
|
||||
AND first_seen < (strftime('%s', 'now') - 3600);
|
||||
END;
|
||||
-- Replaceable event handling trigger
|
||||
CREATE TRIGGER handle_replaceable_events
|
||||
AFTER INSERT ON events
|
||||
WHEN NEW.event_type = 'replaceable'
|
||||
BEGIN
|
||||
DELETE FROM events
|
||||
WHERE pubkey = NEW.pubkey
|
||||
AND kind = NEW.kind
|
||||
AND event_type = 'replaceable'
|
||||
AND id != NEW.id;
|
||||
END;
|
||||
-- Addressable event handling trigger (for kind 33334 configuration events)
|
||||
CREATE TRIGGER handle_addressable_events
|
||||
AFTER INSERT ON events
|
||||
WHEN NEW.event_type = 'addressable'
|
||||
BEGIN
|
||||
-- For kind 33334 (configuration), replace previous config from same admin
|
||||
DELETE FROM events
|
||||
WHERE pubkey = NEW.pubkey
|
||||
AND kind = NEW.kind
|
||||
AND event_type = 'addressable'
|
||||
AND id != NEW.id;
|
||||
END;
|
||||
-- Relay Private Key Secure Storage
|
||||
-- Stores the relay's private key separately from public configuration
|
||||
CREATE TABLE relay_seckey (
|
||||
private_key_hex TEXT NOT NULL CHECK (length(private_key_hex) = 64),
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
-- Authentication Rules Table for NIP-42 and Policy Enforcement
|
||||
-- Used by request_validator.c for unified validation
|
||||
CREATE TABLE auth_rules (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rule_type TEXT NOT NULL CHECK (rule_type IN ('whitelist', 'blacklist', 'rate_limit', 'auth_required')),
|
||||
pattern_type TEXT NOT NULL CHECK (pattern_type IN ('pubkey', 'kind', 'ip', 'global')),
|
||||
pattern_value TEXT,
|
||||
action TEXT NOT NULL CHECK (action IN ('allow', 'deny', 'require_auth', 'rate_limit')),
|
||||
parameters TEXT, -- JSON parameters for rate limiting, etc.
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
-- Indexes for auth_rules performance
|
||||
CREATE INDEX idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value);
|
||||
CREATE INDEX idx_auth_rules_type ON auth_rules(rule_type);
|
||||
CREATE INDEX idx_auth_rules_active ON auth_rules(active);
|
||||
-- Configuration Table for Table-Based Config Management
|
||||
-- Hybrid system supporting both event-based and table-based configuration
|
||||
CREATE TABLE config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
data_type TEXT NOT NULL CHECK (data_type IN ('string', 'integer', 'boolean', 'json')),
|
||||
description TEXT,
|
||||
category TEXT DEFAULT 'general',
|
||||
requires_restart INTEGER DEFAULT 0,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
-- Indexes for config table performance
|
||||
CREATE INDEX idx_config_category ON config(category);
|
||||
CREATE INDEX idx_config_restart ON config(requires_restart);
|
||||
CREATE INDEX idx_config_updated ON config(updated_at DESC);
|
||||
-- Trigger to update config timestamp on changes
|
||||
CREATE TRIGGER update_config_timestamp
|
||||
AFTER UPDATE ON config
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE config SET updated_at = strftime('%s', 'now') WHERE key = NEW.key;
|
||||
END;
|
||||
-- Insert default configuration values
|
||||
INSERT INTO config (key, value, data_type, description, category, requires_restart) VALUES
|
||||
('relay_description', 'A C Nostr Relay', 'string', 'Relay description', 'general', 0),
|
||||
('relay_contact', '', 'string', 'Relay contact information', 'general', 0),
|
||||
('relay_software', 'https://github.com/laanwj/c-relay', 'string', 'Relay software URL', 'general', 0),
|
||||
('relay_version', '1.0.0', 'string', 'Relay version', 'general', 0),
|
||||
('relay_port', '8888', 'integer', 'Relay port number', 'network', 1),
|
||||
('max_connections', '1000', 'integer', 'Maximum concurrent connections', 'network', 1),
|
||||
('auth_enabled', 'false', 'boolean', 'Enable NIP-42 authentication', 'auth', 0),
|
||||
('nip42_auth_required_events', 'false', 'boolean', 'Require auth for event publishing', 'auth', 0),
|
||||
('nip42_auth_required_subscriptions', 'false', 'boolean', 'Require auth for subscriptions', 'auth', 0),
|
||||
('nip42_auth_required_kinds', '[]', 'json', 'Event kinds requiring authentication', 'auth', 0),
|
||||
('nip42_challenge_expiration', '600', 'integer', 'Auth challenge expiration seconds', 'auth', 0),
|
||||
('pow_min_difficulty', '0', 'integer', 'Minimum proof-of-work difficulty', 'validation', 0),
|
||||
('pow_mode', 'optional', 'string', 'Proof-of-work mode', 'validation', 0),
|
||||
('nip40_expiration_enabled', 'true', 'boolean', 'Enable event expiration', 'validation', 0),
|
||||
('nip40_expiration_strict', 'false', 'boolean', 'Strict expiration mode', 'validation', 0),
|
||||
('nip40_expiration_filter', 'true', 'boolean', 'Filter expired events in queries', 'validation', 0),
|
||||
('nip40_expiration_grace_period', '60', 'integer', 'Expiration grace period seconds', 'validation', 0),
|
||||
('max_subscriptions_per_client', '25', 'integer', 'Maximum subscriptions per client', 'limits', 0),
|
||||
('max_total_subscriptions', '1000', 'integer', 'Maximum total subscriptions', 'limits', 0),
|
||||
('max_filters_per_subscription', '10', 'integer', 'Maximum filters per subscription', 'limits', 0),
|
||||
('max_event_tags', '2000', 'integer', 'Maximum tags per event', 'limits', 0),
|
||||
('max_content_length', '100000', 'integer', 'Maximum event content length', 'limits', 0),
|
||||
('max_message_length', '131072', 'integer', 'Maximum WebSocket message length', 'limits', 0),
|
||||
('default_limit', '100', 'integer', 'Default query limit', 'limits', 0),
|
||||
('max_limit', '5000', 'integer', 'Maximum query limit', 'limits', 0);
|
||||
-- Persistent Subscriptions Logging Tables (Phase 2)
|
||||
-- Optional database logging for subscription analytics and debugging
|
||||
-- Subscription events log
|
||||
CREATE TABLE subscription_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
subscription_id TEXT NOT NULL, -- Subscription ID from client
|
||||
client_ip TEXT NOT NULL, -- Client IP address
|
||||
event_type TEXT NOT NULL CHECK (event_type IN ('created', 'closed', 'expired', 'disconnected')),
|
||||
filter_json TEXT, -- JSON representation of filters (for created events)
|
||||
events_sent INTEGER DEFAULT 0, -- Number of events sent to this subscription
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
ended_at INTEGER, -- When subscription ended (for closed/expired/disconnected)
|
||||
duration INTEGER -- Computed: ended_at - created_at
|
||||
);
|
||||
-- Subscription metrics summary
|
||||
CREATE TABLE subscription_metrics (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
date TEXT NOT NULL, -- Date (YYYY-MM-DD)
|
||||
total_created INTEGER DEFAULT 0, -- Total subscriptions created
|
||||
total_closed INTEGER DEFAULT 0, -- Total subscriptions closed
|
||||
total_events_broadcast INTEGER DEFAULT 0, -- Total events broadcast
|
||||
avg_duration REAL DEFAULT 0, -- Average subscription duration
|
||||
peak_concurrent INTEGER DEFAULT 0, -- Peak concurrent subscriptions
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
UNIQUE(date)
|
||||
);
|
||||
-- Event broadcasting log (optional, for detailed analytics)
|
||||
CREATE TABLE event_broadcasts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
event_id TEXT NOT NULL, -- Event ID that was broadcast
|
||||
subscription_id TEXT NOT NULL, -- Subscription that received it
|
||||
client_ip TEXT NOT NULL, -- Client IP
|
||||
broadcast_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
FOREIGN KEY (event_id) REFERENCES events(id)
|
||||
);
|
||||
-- Indexes for subscription logging performance
|
||||
CREATE INDEX idx_subscription_events_id ON subscription_events(subscription_id);
|
||||
CREATE INDEX idx_subscription_events_type ON subscription_events(event_type);
|
||||
CREATE INDEX idx_subscription_events_created ON subscription_events(created_at DESC);
|
||||
CREATE INDEX idx_subscription_events_client ON subscription_events(client_ip);
|
||||
CREATE INDEX idx_subscription_metrics_date ON subscription_metrics(date DESC);
|
||||
CREATE INDEX idx_event_broadcasts_event ON event_broadcasts(event_id);
|
||||
CREATE INDEX idx_event_broadcasts_sub ON event_broadcasts(subscription_id);
|
||||
CREATE INDEX idx_event_broadcasts_time ON event_broadcasts(broadcast_at DESC);
|
||||
-- Trigger to update subscription duration when ended
|
||||
CREATE TRIGGER update_subscription_duration
|
||||
AFTER UPDATE OF ended_at ON subscription_events
|
||||
WHEN NEW.ended_at IS NOT NULL AND OLD.ended_at IS NULL
|
||||
BEGIN
|
||||
UPDATE subscription_events
|
||||
SET duration = NEW.ended_at - NEW.created_at
|
||||
WHERE id = NEW.id;
|
||||
END;
|
||||
-- View for subscription analytics
|
||||
CREATE VIEW subscription_analytics AS
|
||||
SELECT
|
||||
date(created_at, 'unixepoch') as date,
|
||||
COUNT(*) as subscriptions_created,
|
||||
COUNT(CASE WHEN ended_at IS NOT NULL THEN 1 END) as subscriptions_ended,
|
||||
AVG(CASE WHEN duration IS NOT NULL THEN duration END) as avg_duration_seconds,
|
||||
MAX(events_sent) as max_events_sent,
|
||||
AVG(events_sent) as avg_events_sent,
|
||||
COUNT(DISTINCT client_ip) as unique_clients
|
||||
FROM subscription_events
|
||||
GROUP BY date(created_at, 'unixepoch')
|
||||
ORDER BY date DESC;
|
||||
-- View for current active subscriptions (from log perspective)
|
||||
CREATE VIEW active_subscriptions_log AS
|
||||
SELECT
|
||||
subscription_id,
|
||||
client_ip,
|
||||
filter_json,
|
||||
events_sent,
|
||||
created_at,
|
||||
(strftime('%s', 'now') - created_at) as duration_seconds
|
||||
FROM subscription_events
|
||||
WHERE event_type = 'created'
|
||||
AND subscription_id NOT IN (
|
||||
SELECT subscription_id FROM subscription_events
|
||||
WHERE event_type IN ('closed', 'expired', 'disconnected')
|
||||
);
|
||||
-- Database Statistics Views for Admin API
|
||||
-- Event kinds distribution view
|
||||
CREATE VIEW event_kinds_view AS
|
||||
SELECT
|
||||
kind,
|
||||
COUNT(*) as count,
|
||||
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM events), 2) as percentage
|
||||
FROM events
|
||||
GROUP BY kind
|
||||
ORDER BY count DESC;
|
||||
-- Top pubkeys by event count view
|
||||
CREATE VIEW top_pubkeys_view AS
|
||||
SELECT
|
||||
pubkey,
|
||||
COUNT(*) as event_count,
|
||||
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM events), 2) as percentage
|
||||
FROM events
|
||||
GROUP BY pubkey
|
||||
ORDER BY event_count DESC
|
||||
LIMIT 10;
|
||||
-- Time-based statistics view
|
||||
CREATE VIEW time_stats_view AS
|
||||
SELECT
|
||||
'total' as period,
|
||||
COUNT(*) as total_events,
|
||||
COUNT(DISTINCT pubkey) as unique_pubkeys,
|
||||
MIN(created_at) as oldest_event,
|
||||
MAX(created_at) as newest_event
|
||||
FROM events
|
||||
UNION ALL
|
||||
SELECT
|
||||
'24h' as period,
|
||||
COUNT(*) as total_events,
|
||||
COUNT(DISTINCT pubkey) as unique_pubkeys,
|
||||
MIN(created_at) as oldest_event,
|
||||
MAX(created_at) as newest_event
|
||||
FROM events
|
||||
WHERE created_at >= (strftime('%s', 'now') - 86400)
|
||||
UNION ALL
|
||||
SELECT
|
||||
'7d' as period,
|
||||
COUNT(*) as total_events,
|
||||
COUNT(DISTINCT pubkey) as unique_pubkeys,
|
||||
MIN(created_at) as oldest_event,
|
||||
MAX(created_at) as newest_event
|
||||
FROM events
|
||||
WHERE created_at >= (strftime('%s', 'now') - 604800)
|
||||
UNION ALL
|
||||
SELECT
|
||||
'30d' as period,
|
||||
COUNT(*) as total_events,
|
||||
COUNT(DISTINCT pubkey) as unique_pubkeys,
|
||||
MIN(created_at) as oldest_event,
|
||||
MAX(created_at) as newest_event
|
||||
FROM events
|
||||
WHERE created_at >= (strftime('%s', 'now') - 2592000);
|
||||
Submodule nostr_core_lib updated: 55e2a9c68e...c0784fc890
696
schema.sql
Normal file
696
schema.sql
Normal file
@@ -0,0 +1,696 @@
|
||||
|
||||
-- C Nostr Relay Database Schema
|
||||
\
|
||||
-- SQLite schema for storing Nostr events with JSON tags support
|
||||
\
|
||||
-- Configuration system using config table
|
||||
\
|
||||
|
||||
\
|
||||
-- Schema version tracking
|
||||
\
|
||||
PRAGMA user_version = 7;
|
||||
\
|
||||
|
||||
\
|
||||
-- Enable foreign key support
|
||||
\
|
||||
PRAGMA foreign_keys = ON;
|
||||
\
|
||||
|
||||
\
|
||||
-- Optimize for performance
|
||||
\
|
||||
PRAGMA journal_mode = WAL;
|
||||
\
|
||||
PRAGMA synchronous = NORMAL;
|
||||
\
|
||||
PRAGMA cache_size = 10000;
|
||||
\
|
||||
|
||||
\
|
||||
-- Core events table with hybrid single-table design
|
||||
\
|
||||
CREATE TABLE events (
|
||||
\
|
||||
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)
|
||||
\
|
||||
event_type TEXT NOT NULL CHECK (event_type IN ('regular', 'replaceable', 'ephemeral', 'addressable')),
|
||||
\
|
||||
content TEXT NOT NULL, -- Event content (text content only)
|
||||
\
|
||||
sig TEXT NOT NULL, -- Event signature (hex string)
|
||||
\
|
||||
tags JSON NOT NULL DEFAULT '[]', -- Event tags as JSON array
|
||||
\
|
||||
first_seen INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) -- When relay received event
|
||||
\
|
||||
);
|
||||
\
|
||||
|
||||
\
|
||||
-- Core performance indexes
|
||||
\
|
||||
CREATE INDEX idx_events_pubkey ON events(pubkey);
|
||||
\
|
||||
CREATE INDEX idx_events_kind ON events(kind);
|
||||
\
|
||||
CREATE INDEX idx_events_created_at ON events(created_at DESC);
|
||||
\
|
||||
CREATE INDEX idx_events_event_type ON events(event_type);
|
||||
\
|
||||
|
||||
\
|
||||
-- Composite indexes for common query patterns
|
||||
\
|
||||
CREATE INDEX idx_events_kind_created_at ON events(kind, created_at DESC);
|
||||
\
|
||||
CREATE INDEX idx_events_pubkey_created_at ON events(pubkey, created_at DESC);
|
||||
\
|
||||
CREATE INDEX idx_events_pubkey_kind ON events(pubkey, kind);
|
||||
\
|
||||
|
||||
\
|
||||
-- Schema information table
|
||||
\
|
||||
CREATE TABLE schema_info (
|
||||
\
|
||||
key TEXT PRIMARY KEY,
|
||||
\
|
||||
value TEXT NOT NULL,
|
||||
\
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
\
|
||||
);
|
||||
\
|
||||
|
||||
\
|
||||
-- Insert schema metadata
|
||||
\
|
||||
INSERT INTO schema_info (key, value) VALUES
|
||||
\
|
||||
('version', '7'),
|
||||
\
|
||||
('description', 'Hybrid Nostr relay schema with event-based and table-based configuration'),
|
||||
\
|
||||
('created_at', strftime('%s', 'now'));
|
||||
\
|
||||
|
||||
\
|
||||
-- Helper views for common queries
|
||||
\
|
||||
CREATE VIEW recent_events AS
|
||||
\
|
||||
SELECT id, pubkey, created_at, kind, event_type, content
|
||||
\
|
||||
FROM events
|
||||
\
|
||||
WHERE event_type != 'ephemeral'
|
||||
\
|
||||
ORDER BY created_at DESC
|
||||
\
|
||||
LIMIT 1000;
|
||||
\
|
||||
|
||||
\
|
||||
CREATE VIEW event_stats AS
|
||||
\
|
||||
SELECT
|
||||
\
|
||||
event_type,
|
||||
\
|
||||
COUNT(*) as count,
|
||||
\
|
||||
AVG(length(content)) as avg_content_length,
|
||||
\
|
||||
MIN(created_at) as earliest,
|
||||
\
|
||||
MAX(created_at) as latest
|
||||
\
|
||||
FROM events
|
||||
\
|
||||
GROUP BY event_type;
|
||||
\
|
||||
|
||||
\
|
||||
-- Configuration events view (kind 33334)
|
||||
\
|
||||
CREATE VIEW configuration_events AS
|
||||
\
|
||||
SELECT
|
||||
\
|
||||
id,
|
||||
\
|
||||
pubkey as admin_pubkey,
|
||||
\
|
||||
created_at,
|
||||
\
|
||||
content,
|
||||
\
|
||||
tags,
|
||||
\
|
||||
sig
|
||||
\
|
||||
FROM events
|
||||
\
|
||||
WHERE kind = 33334
|
||||
\
|
||||
ORDER BY created_at DESC;
|
||||
\
|
||||
|
||||
\
|
||||
-- Optimization: Trigger for automatic cleanup of ephemeral events older than 1 hour
|
||||
\
|
||||
CREATE TRIGGER cleanup_ephemeral_events
|
||||
\
|
||||
AFTER INSERT ON events
|
||||
\
|
||||
WHEN NEW.event_type = 'ephemeral'
|
||||
\
|
||||
BEGIN
|
||||
\
|
||||
DELETE FROM events
|
||||
\
|
||||
WHERE event_type = 'ephemeral'
|
||||
\
|
||||
AND first_seen < (strftime('%s', 'now') - 3600);
|
||||
\
|
||||
END;
|
||||
\
|
||||
|
||||
\
|
||||
-- Replaceable event handling trigger
|
||||
\
|
||||
CREATE TRIGGER handle_replaceable_events
|
||||
\
|
||||
AFTER INSERT ON events
|
||||
\
|
||||
WHEN NEW.event_type = 'replaceable'
|
||||
\
|
||||
BEGIN
|
||||
\
|
||||
DELETE FROM events
|
||||
\
|
||||
WHERE pubkey = NEW.pubkey
|
||||
\
|
||||
AND kind = NEW.kind
|
||||
\
|
||||
AND event_type = 'replaceable'
|
||||
\
|
||||
AND id != NEW.id;
|
||||
\
|
||||
END;
|
||||
\
|
||||
|
||||
\
|
||||
-- Addressable event handling trigger (for kind 33334 configuration events)
|
||||
\
|
||||
CREATE TRIGGER handle_addressable_events
|
||||
\
|
||||
AFTER INSERT ON events
|
||||
\
|
||||
WHEN NEW.event_type = 'addressable'
|
||||
\
|
||||
BEGIN
|
||||
\
|
||||
-- For kind 33334 (configuration), replace previous config from same admin
|
||||
\
|
||||
DELETE FROM events
|
||||
\
|
||||
WHERE pubkey = NEW.pubkey
|
||||
\
|
||||
AND kind = NEW.kind
|
||||
\
|
||||
AND event_type = 'addressable'
|
||||
\
|
||||
AND id != NEW.id;
|
||||
\
|
||||
END;
|
||||
\
|
||||
|
||||
\
|
||||
-- Relay Private Key Secure Storage
|
||||
\
|
||||
-- Stores the relay's private key separately from public configuration
|
||||
\
|
||||
CREATE TABLE relay_seckey (
|
||||
\
|
||||
private_key_hex TEXT NOT NULL CHECK (length(private_key_hex) = 64),
|
||||
\
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
\
|
||||
);
|
||||
\
|
||||
|
||||
\
|
||||
-- Authentication Rules Table for NIP-42 and Policy Enforcement
|
||||
\
|
||||
-- Used by request_validator.c for unified validation
|
||||
\
|
||||
CREATE TABLE auth_rules (
|
||||
\
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
\
|
||||
rule_type TEXT NOT NULL CHECK (rule_type IN ('whitelist', 'blacklist', 'rate_limit', 'auth_required')),
|
||||
\
|
||||
pattern_type TEXT NOT NULL CHECK (pattern_type IN ('pubkey', 'kind', 'ip', 'global')),
|
||||
\
|
||||
pattern_value TEXT,
|
||||
\
|
||||
action TEXT NOT NULL CHECK (action IN ('allow', 'deny', 'require_auth', 'rate_limit')),
|
||||
\
|
||||
parameters TEXT, -- JSON parameters for rate limiting, etc.
|
||||
\
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
\
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
\
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
\
|
||||
);
|
||||
\
|
||||
|
||||
\
|
||||
-- Indexes for auth_rules performance
|
||||
\
|
||||
CREATE INDEX idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value);
|
||||
\
|
||||
CREATE INDEX idx_auth_rules_type ON auth_rules(rule_type);
|
||||
\
|
||||
CREATE INDEX idx_auth_rules_active ON auth_rules(active);
|
||||
\
|
||||
|
||||
\
|
||||
-- Configuration Table for Table-Based Config Management
|
||||
\
|
||||
-- Hybrid system supporting both event-based and table-based configuration
|
||||
\
|
||||
CREATE TABLE config (
|
||||
\
|
||||
key TEXT PRIMARY KEY,
|
||||
\
|
||||
value TEXT NOT NULL,
|
||||
\
|
||||
data_type TEXT NOT NULL CHECK (data_type IN ('string', 'integer', 'boolean', 'json')),
|
||||
\
|
||||
description TEXT,
|
||||
\
|
||||
category TEXT DEFAULT 'general',
|
||||
\
|
||||
requires_restart INTEGER DEFAULT 0,
|
||||
\
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
\
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
\
|
||||
);
|
||||
\
|
||||
|
||||
\
|
||||
-- Indexes for config table performance
|
||||
\
|
||||
CREATE INDEX idx_config_category ON config(category);
|
||||
\
|
||||
CREATE INDEX idx_config_restart ON config(requires_restart);
|
||||
\
|
||||
CREATE INDEX idx_config_updated ON config(updated_at DESC);
|
||||
\
|
||||
|
||||
\
|
||||
-- Trigger to update config timestamp on changes
|
||||
\
|
||||
CREATE TRIGGER update_config_timestamp
|
||||
\
|
||||
AFTER UPDATE ON config
|
||||
\
|
||||
FOR EACH ROW
|
||||
\
|
||||
BEGIN
|
||||
\
|
||||
UPDATE config SET updated_at = strftime('%s', 'now') WHERE key = NEW.key;
|
||||
\
|
||||
END;
|
||||
\
|
||||
|
||||
\
|
||||
-- Insert default configuration values
|
||||
\
|
||||
INSERT INTO config (key, value, data_type, description, category, requires_restart) VALUES
|
||||
\
|
||||
('relay_description', 'A C Nostr Relay', 'string', 'Relay description', 'general', 0),
|
||||
\
|
||||
('relay_contact', '', 'string', 'Relay contact information', 'general', 0),
|
||||
\
|
||||
('relay_software', 'https://github.com/laanwj/c-relay', 'string', 'Relay software URL', 'general', 0),
|
||||
\
|
||||
('relay_version', '1.0.0', 'string', 'Relay version', 'general', 0),
|
||||
\
|
||||
('relay_port', '8888', 'integer', 'Relay port number', 'network', 1),
|
||||
\
|
||||
('max_connections', '1000', 'integer', 'Maximum concurrent connections', 'network', 1),
|
||||
\
|
||||
('auth_enabled', 'false', 'boolean', 'Enable NIP-42 authentication', 'auth', 0),
|
||||
\
|
||||
('nip42_auth_required_events', 'false', 'boolean', 'Require auth for event publishing', 'auth', 0),
|
||||
\
|
||||
('nip42_auth_required_subscriptions', 'false', 'boolean', 'Require auth for subscriptions', 'auth', 0),
|
||||
\
|
||||
('nip42_auth_required_kinds', '[]', 'json', 'Event kinds requiring authentication', 'auth', 0),
|
||||
\
|
||||
('nip42_challenge_expiration', '600', 'integer', 'Auth challenge expiration seconds', 'auth', 0),
|
||||
\
|
||||
('pow_min_difficulty', '0', 'integer', 'Minimum proof-of-work difficulty', 'validation', 0),
|
||||
\
|
||||
('pow_mode', 'optional', 'string', 'Proof-of-work mode', 'validation', 0),
|
||||
\
|
||||
('nip40_expiration_enabled', 'true', 'boolean', 'Enable event expiration', 'validation', 0),
|
||||
\
|
||||
('nip40_expiration_strict', 'false', 'boolean', 'Strict expiration mode', 'validation', 0),
|
||||
\
|
||||
('nip40_expiration_filter', 'true', 'boolean', 'Filter expired events in queries', 'validation', 0),
|
||||
\
|
||||
('nip40_expiration_grace_period', '60', 'integer', 'Expiration grace period seconds', 'validation', 0),
|
||||
\
|
||||
('max_subscriptions_per_client', '25', 'integer', 'Maximum subscriptions per client', 'limits', 0),
|
||||
\
|
||||
('max_total_subscriptions', '1000', 'integer', 'Maximum total subscriptions', 'limits', 0),
|
||||
\
|
||||
('max_filters_per_subscription', '10', 'integer', 'Maximum filters per subscription', 'limits', 0),
|
||||
\
|
||||
('max_event_tags', '2000', 'integer', 'Maximum tags per event', 'limits', 0),
|
||||
\
|
||||
('max_content_length', '100000', 'integer', 'Maximum event content length', 'limits', 0),
|
||||
\
|
||||
('max_message_length', '131072', 'integer', 'Maximum WebSocket message length', 'limits', 0),
|
||||
\
|
||||
('default_limit', '100', 'integer', 'Default query limit', 'limits', 0),
|
||||
\
|
||||
('max_limit', '5000', 'integer', 'Maximum query limit', 'limits', 0);
|
||||
\
|
||||
|
||||
\
|
||||
-- Persistent Subscriptions Logging Tables (Phase 2)
|
||||
\
|
||||
-- Optional database logging for subscription analytics and debugging
|
||||
\
|
||||
|
||||
\
|
||||
-- Subscription events log
|
||||
\
|
||||
CREATE TABLE subscription_events (
|
||||
\
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
\
|
||||
subscription_id TEXT NOT NULL, -- Subscription ID from client
|
||||
\
|
||||
client_ip TEXT NOT NULL, -- Client IP address
|
||||
\
|
||||
event_type TEXT NOT NULL CHECK (event_type IN ('created', 'closed', 'expired', 'disconnected')),
|
||||
\
|
||||
filter_json TEXT, -- JSON representation of filters (for created events)
|
||||
\
|
||||
events_sent INTEGER DEFAULT 0, -- Number of events sent to this subscription
|
||||
\
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
\
|
||||
ended_at INTEGER, -- When subscription ended (for closed/expired/disconnected)
|
||||
\
|
||||
duration INTEGER -- Computed: ended_at - created_at
|
||||
\
|
||||
);
|
||||
\
|
||||
|
||||
\
|
||||
-- Subscription metrics summary
|
||||
\
|
||||
CREATE TABLE subscription_metrics (
|
||||
\
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
\
|
||||
date TEXT NOT NULL, -- Date (YYYY-MM-DD)
|
||||
\
|
||||
total_created INTEGER DEFAULT 0, -- Total subscriptions created
|
||||
\
|
||||
total_closed INTEGER DEFAULT 0, -- Total subscriptions closed
|
||||
\
|
||||
total_events_broadcast INTEGER DEFAULT 0, -- Total events broadcast
|
||||
\
|
||||
avg_duration REAL DEFAULT 0, -- Average subscription duration
|
||||
\
|
||||
peak_concurrent INTEGER DEFAULT 0, -- Peak concurrent subscriptions
|
||||
\
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
\
|
||||
UNIQUE(date)
|
||||
\
|
||||
);
|
||||
\
|
||||
|
||||
\
|
||||
-- Event broadcasting log (optional, for detailed analytics)
|
||||
\
|
||||
CREATE TABLE event_broadcasts (
|
||||
\
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
\
|
||||
event_id TEXT NOT NULL, -- Event ID that was broadcast
|
||||
\
|
||||
subscription_id TEXT NOT NULL, -- Subscription that received it
|
||||
\
|
||||
client_ip TEXT NOT NULL, -- Client IP
|
||||
\
|
||||
broadcast_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
\
|
||||
FOREIGN KEY (event_id) REFERENCES events(id)
|
||||
\
|
||||
);
|
||||
\
|
||||
|
||||
\
|
||||
-- Indexes for subscription logging performance
|
||||
\
|
||||
CREATE INDEX idx_subscription_events_id ON subscription_events(subscription_id);
|
||||
\
|
||||
CREATE INDEX idx_subscription_events_type ON subscription_events(event_type);
|
||||
\
|
||||
CREATE INDEX idx_subscription_events_created ON subscription_events(created_at DESC);
|
||||
\
|
||||
CREATE INDEX idx_subscription_events_client ON subscription_events(client_ip);
|
||||
\
|
||||
|
||||
\
|
||||
CREATE INDEX idx_subscription_metrics_date ON subscription_metrics(date DESC);
|
||||
\
|
||||
|
||||
\
|
||||
CREATE INDEX idx_event_broadcasts_event ON event_broadcasts(event_id);
|
||||
\
|
||||
CREATE INDEX idx_event_broadcasts_sub ON event_broadcasts(subscription_id);
|
||||
\
|
||||
CREATE INDEX idx_event_broadcasts_time ON event_broadcasts(broadcast_at DESC);
|
||||
\
|
||||
|
||||
\
|
||||
-- Trigger to update subscription duration when ended
|
||||
\
|
||||
CREATE TRIGGER update_subscription_duration
|
||||
\
|
||||
AFTER UPDATE OF ended_at ON subscription_events
|
||||
\
|
||||
WHEN NEW.ended_at IS NOT NULL AND OLD.ended_at IS NULL
|
||||
\
|
||||
BEGIN
|
||||
\
|
||||
UPDATE subscription_events
|
||||
\
|
||||
SET duration = NEW.ended_at - NEW.created_at
|
||||
\
|
||||
WHERE id = NEW.id;
|
||||
\
|
||||
END;
|
||||
\
|
||||
|
||||
\
|
||||
-- View for subscription analytics
|
||||
\
|
||||
CREATE VIEW subscription_analytics AS
|
||||
\
|
||||
SELECT
|
||||
\
|
||||
date(created_at, 'unixepoch') as date,
|
||||
\
|
||||
COUNT(*) as subscriptions_created,
|
||||
\
|
||||
COUNT(CASE WHEN ended_at IS NOT NULL THEN 1 END) as subscriptions_ended,
|
||||
\
|
||||
AVG(CASE WHEN duration IS NOT NULL THEN duration END) as avg_duration_seconds,
|
||||
\
|
||||
MAX(events_sent) as max_events_sent,
|
||||
\
|
||||
AVG(events_sent) as avg_events_sent,
|
||||
\
|
||||
COUNT(DISTINCT client_ip) as unique_clients
|
||||
\
|
||||
FROM subscription_events
|
||||
\
|
||||
GROUP BY date(created_at, 'unixepoch')
|
||||
\
|
||||
ORDER BY date DESC;
|
||||
\
|
||||
|
||||
\
|
||||
-- View for current active subscriptions (from log perspective)
|
||||
\
|
||||
CREATE VIEW active_subscriptions_log AS
|
||||
\
|
||||
SELECT
|
||||
\
|
||||
subscription_id,
|
||||
\
|
||||
client_ip,
|
||||
\
|
||||
filter_json,
|
||||
\
|
||||
events_sent,
|
||||
\
|
||||
created_at,
|
||||
\
|
||||
(strftime('%s', 'now') - created_at) as duration_seconds
|
||||
\
|
||||
FROM subscription_events
|
||||
\
|
||||
WHERE event_type = 'created'
|
||||
\
|
||||
AND subscription_id NOT IN (
|
||||
\
|
||||
SELECT subscription_id FROM subscription_events
|
||||
\
|
||||
WHERE event_type IN ('closed', 'expired', 'disconnected')
|
||||
\
|
||||
);
|
||||
\
|
||||
|
||||
\
|
||||
-- Database Statistics Views for Admin API
|
||||
\
|
||||
-- Event kinds distribution view
|
||||
\
|
||||
CREATE VIEW event_kinds_view AS
|
||||
\
|
||||
SELECT
|
||||
\
|
||||
kind,
|
||||
\
|
||||
COUNT(*) as count,
|
||||
\
|
||||
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM events), 2) as percentage
|
||||
\
|
||||
FROM events
|
||||
\
|
||||
GROUP BY kind
|
||||
\
|
||||
ORDER BY count DESC;
|
||||
\
|
||||
|
||||
\
|
||||
-- Top pubkeys by event count view
|
||||
\
|
||||
CREATE VIEW top_pubkeys_view AS
|
||||
\
|
||||
SELECT
|
||||
\
|
||||
pubkey,
|
||||
\
|
||||
COUNT(*) as event_count,
|
||||
\
|
||||
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM events), 2) as percentage
|
||||
\
|
||||
FROM events
|
||||
\
|
||||
GROUP BY pubkey
|
||||
\
|
||||
ORDER BY event_count DESC
|
||||
\
|
||||
LIMIT 10;
|
||||
\
|
||||
|
||||
\
|
||||
-- Time-based statistics view
|
||||
\
|
||||
CREATE VIEW time_stats_view AS
|
||||
\
|
||||
SELECT
|
||||
\
|
||||
'total' as period,
|
||||
\
|
||||
COUNT(*) as total_events,
|
||||
\
|
||||
COUNT(DISTINCT pubkey) as unique_pubkeys,
|
||||
\
|
||||
MIN(created_at) as oldest_event,
|
||||
\
|
||||
MAX(created_at) as newest_event
|
||||
\
|
||||
FROM events
|
||||
\
|
||||
UNION ALL
|
||||
\
|
||||
SELECT
|
||||
\
|
||||
'24h' as period,
|
||||
\
|
||||
COUNT(*) as total_events,
|
||||
\
|
||||
COUNT(DISTINCT pubkey) as unique_pubkeys,
|
||||
\
|
||||
MIN(created_at) as oldest_event,
|
||||
\
|
||||
MAX(created_at) as newest_event
|
||||
\
|
||||
FROM events
|
||||
\
|
||||
WHERE created_at >= (strftime('%s', 'now') - 86400)
|
||||
\
|
||||
UNION ALL
|
||||
\
|
||||
SELECT
|
||||
\
|
||||
'7d' as period,
|
||||
\
|
||||
COUNT(*) as total_events,
|
||||
\
|
||||
COUNT(DISTINCT pubkey) as unique_pubkeys,
|
||||
\
|
||||
MIN(created_at) as oldest_event,
|
||||
\
|
||||
MAX(created_at) as newest_event
|
||||
\
|
||||
FROM events
|
||||
\
|
||||
WHERE created_at >= (strftime('%s', 'now') - 604800)
|
||||
\
|
||||
UNION ALL
|
||||
\
|
||||
SELECT
|
||||
\
|
||||
'30d' as period,
|
||||
\
|
||||
COUNT(*) as total_events,
|
||||
\
|
||||
COUNT(DISTINCT pubkey) as unique_pubkeys,
|
||||
\
|
||||
MIN(created_at) as oldest_event,
|
||||
\
|
||||
MAX(created_at) as newest_event
|
||||
\
|
||||
FROM events
|
||||
\
|
||||
WHERE created_at >= (strftime('%s', 'now') - 2592000);
|
||||
|
||||
#endif /* SQL_SCHEMA_H */
|
||||
447
src/api.c
447
src/api.c
@@ -1,7 +1,7 @@
|
||||
// Define _GNU_SOURCE to ensure all POSIX features are available
|
||||
#define _GNU_SOURCE
|
||||
|
||||
// API module for serving embedded web content
|
||||
// API module for serving embedded web content and NIP-17 admin messaging
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -9,6 +9,18 @@
|
||||
#include <libwebsockets.h>
|
||||
#include "api.h"
|
||||
#include "embedded_web_content.h"
|
||||
#include "../nostr_core_lib/nostr_core/nip017.h"
|
||||
#include "../nostr_core_lib/nostr_core/nip044.h"
|
||||
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
#include "config.h"
|
||||
|
||||
// Forward declarations for event creation and signing
|
||||
cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags,
|
||||
const unsigned char* privkey_bytes, time_t created_at);
|
||||
|
||||
// Forward declaration for stats generation
|
||||
char* generate_stats_json(void);
|
||||
|
||||
|
||||
// Forward declarations for logging functions
|
||||
void log_info(const char* message);
|
||||
@@ -16,6 +28,9 @@ void log_success(const char* message);
|
||||
void log_error(const char* message);
|
||||
void log_warning(const char* message);
|
||||
|
||||
// Forward declarations for database functions
|
||||
int store_event(cJSON* event);
|
||||
|
||||
// Handle HTTP request for embedded files (assumes GET)
|
||||
int handle_embedded_file_request(struct lws* wsi, const char* requested_uri) {
|
||||
log_info("Handling embedded file request");
|
||||
@@ -162,4 +177,434 @@ int handle_embedded_file_writeable(struct lws* wsi) {
|
||||
|
||||
log_success("Embedded file served successfully");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// NIP-17 GIFT WRAP ADMIN MESSAGING FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
// Check if an event is a NIP-17 gift wrap addressed to this relay
|
||||
int is_nip17_gift_wrap_for_relay(cJSON* event) {
|
||||
if (!event || !cJSON_IsObject(event)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check kind
|
||||
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||
if (!kind_obj || !cJSON_IsNumber(kind_obj) || (int)cJSON_GetNumberValue(kind_obj) != 1059) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check tags for "p" tag with relay pubkey
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* relay_pubkey = get_relay_pubkey_cached();
|
||||
if (!relay_pubkey) {
|
||||
log_error("NIP-17: Could not get relay pubkey for validation");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Look for "p" tag with relay pubkey
|
||||
cJSON* tag = NULL;
|
||||
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 (tag_name && cJSON_IsString(tag_name) &&
|
||||
strcmp(cJSON_GetStringValue(tag_name), "p") == 0 &&
|
||||
tag_value && cJSON_IsString(tag_value) &&
|
||||
strcmp(cJSON_GetStringValue(tag_value), relay_pubkey) == 0) {
|
||||
return 1; // Found matching p tag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // No matching p tag found
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Process NIP-17 admin command from decrypted DM content
|
||||
int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t error_size, struct lws* wsi) {
|
||||
if (!dm_event || !error_message) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Extract content from DM
|
||||
cJSON* content_obj = cJSON_GetObjectItem(dm_event, "content");
|
||||
if (!content_obj || !cJSON_IsString(content_obj)) {
|
||||
strncpy(error_message, "NIP-17: DM missing content", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* dm_content = cJSON_GetStringValue(content_obj);
|
||||
log_info("NIP-17: Processing admin command from DM content");
|
||||
|
||||
// Parse DM content as JSON array of commands
|
||||
cJSON* command_array = cJSON_Parse(dm_content);
|
||||
if (!command_array || !cJSON_IsArray(command_array)) {
|
||||
strncpy(error_message, "NIP-17: DM content is not valid JSON array", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check if this is a "stats" command
|
||||
if (cJSON_GetArraySize(command_array) > 0) {
|
||||
cJSON* first_item = cJSON_GetArrayItem(command_array, 0);
|
||||
if (cJSON_IsString(first_item) && strcmp(cJSON_GetStringValue(first_item), "stats") == 0) {
|
||||
log_info("NIP-17: Processing 'stats' command directly");
|
||||
|
||||
// Generate stats JSON
|
||||
char* stats_json = generate_stats_json();
|
||||
if (!stats_json) {
|
||||
cJSON_Delete(command_array);
|
||||
strncpy(error_message, "NIP-17: Failed to generate stats", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Get sender pubkey for response
|
||||
cJSON* sender_pubkey_obj = cJSON_GetObjectItem(dm_event, "pubkey");
|
||||
if (!sender_pubkey_obj || !cJSON_IsString(sender_pubkey_obj)) {
|
||||
free(stats_json);
|
||||
cJSON_Delete(command_array);
|
||||
strncpy(error_message, "NIP-17: DM missing sender pubkey", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
const char* sender_pubkey = cJSON_GetStringValue(sender_pubkey_obj);
|
||||
|
||||
// Get relay keys for signing
|
||||
const char* relay_pubkey = get_relay_pubkey_cached();
|
||||
char* relay_privkey_hex = get_relay_private_key();
|
||||
if (!relay_pubkey || !relay_privkey_hex) {
|
||||
free(stats_json);
|
||||
cJSON_Delete(command_array);
|
||||
if (relay_privkey_hex) free(relay_privkey_hex);
|
||||
strncpy(error_message, "NIP-17: Could not get relay keys", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Convert relay private key to bytes
|
||||
unsigned char relay_privkey[32];
|
||||
if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) {
|
||||
free(stats_json);
|
||||
free(relay_privkey_hex);
|
||||
cJSON_Delete(command_array);
|
||||
strncpy(error_message, "NIP-17: Failed to convert relay private key", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
free(relay_privkey_hex);
|
||||
|
||||
// Create DM response event using library function
|
||||
cJSON* dm_response = nostr_nip17_create_chat_event(
|
||||
stats_json, // message content
|
||||
(const char**)&sender_pubkey, // recipient pubkeys
|
||||
1, // num recipients
|
||||
NULL, // subject (optional)
|
||||
NULL, // reply_to_event_id (optional)
|
||||
NULL, // reply_relay_url (optional)
|
||||
relay_pubkey // sender pubkey
|
||||
);
|
||||
|
||||
free(stats_json);
|
||||
|
||||
if (!dm_response) {
|
||||
cJSON_Delete(command_array);
|
||||
strncpy(error_message, "NIP-17: Failed to create DM response event", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Create and sign gift wrap using library function
|
||||
cJSON* gift_wraps[1];
|
||||
int send_result = nostr_nip17_send_dm(
|
||||
dm_response, // dm_event
|
||||
(const char**)&sender_pubkey, // recipient_pubkeys
|
||||
1, // num_recipients
|
||||
relay_privkey, // sender_private_key
|
||||
gift_wraps, // gift_wraps_out
|
||||
1 // max_gift_wraps
|
||||
);
|
||||
|
||||
cJSON_Delete(dm_response);
|
||||
|
||||
if (send_result != 1 || !gift_wraps[0]) {
|
||||
cJSON_Delete(command_array);
|
||||
strncpy(error_message, "NIP-17: Failed to create and sign response gift wrap", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Store the gift wrap in database
|
||||
int store_result = store_event(gift_wraps[0]);
|
||||
cJSON_Delete(gift_wraps[0]);
|
||||
|
||||
if (store_result != 0) {
|
||||
cJSON_Delete(command_array);
|
||||
strncpy(error_message, "NIP-17: Failed to store response gift wrap", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_Delete(command_array);
|
||||
log_success("NIP-17: Stats command processed successfully");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// For other commands, delegate to existing admin processing
|
||||
// Create a synthetic kind 23456 event with the DM content
|
||||
cJSON* synthetic_event = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(synthetic_event, "kind", 23456);
|
||||
cJSON_AddStringToObject(synthetic_event, "content", dm_content);
|
||||
|
||||
// Copy pubkey from DM
|
||||
cJSON* pubkey_obj = cJSON_GetObjectItem(dm_event, "pubkey");
|
||||
if (pubkey_obj && cJSON_IsString(pubkey_obj)) {
|
||||
cJSON_AddStringToObject(synthetic_event, "pubkey", cJSON_GetStringValue(pubkey_obj));
|
||||
}
|
||||
|
||||
// Copy tags from DM
|
||||
cJSON* tags = cJSON_GetObjectItem(dm_event, "tags");
|
||||
if (tags) {
|
||||
cJSON_AddItemToObject(synthetic_event, "tags", cJSON_Duplicate(tags, 1));
|
||||
}
|
||||
|
||||
// Process as regular admin event
|
||||
int result = process_admin_event_in_config(synthetic_event, error_message, error_size, wsi);
|
||||
|
||||
cJSON_Delete(synthetic_event);
|
||||
cJSON_Delete(command_array);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Generate stats JSON from database queries
|
||||
char* generate_stats_json(void) {
|
||||
extern sqlite3* g_db;
|
||||
if (!g_db) {
|
||||
log_error("Database not available for stats generation");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
log_info("Generating stats JSON from database");
|
||||
|
||||
// Build response with database statistics
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(response, "query_type", "stats_query");
|
||||
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
|
||||
|
||||
// Get database file size
|
||||
extern char g_database_path[512];
|
||||
struct stat db_stat;
|
||||
long long db_size = 0;
|
||||
if (stat(g_database_path, &db_stat) == 0) {
|
||||
db_size = db_stat.st_size;
|
||||
}
|
||||
cJSON_AddNumberToObject(response, "database_size_bytes", db_size);
|
||||
|
||||
// Query total events count
|
||||
sqlite3_stmt* stmt;
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM events", -1, &stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
cJSON_AddNumberToObject(response, "total_events", sqlite3_column_int64(stmt, 0));
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
// Query event kinds distribution
|
||||
cJSON* event_kinds = cJSON_CreateArray();
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT kind, count, percentage FROM event_kinds_view ORDER BY count DESC", -1, &stmt, NULL) == SQLITE_OK) {
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
cJSON* kind_obj = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(kind_obj, "kind", sqlite3_column_int(stmt, 0));
|
||||
cJSON_AddNumberToObject(kind_obj, "count", sqlite3_column_int64(stmt, 1));
|
||||
cJSON_AddNumberToObject(kind_obj, "percentage", sqlite3_column_double(stmt, 2));
|
||||
cJSON_AddItemToArray(event_kinds, kind_obj);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
cJSON_AddItemToObject(response, "event_kinds", event_kinds);
|
||||
|
||||
// Query time-based statistics
|
||||
cJSON* time_stats = cJSON_CreateObject();
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT period, total_events FROM time_stats_view", -1, &stmt, NULL) == SQLITE_OK) {
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
const char* period = (const char*)sqlite3_column_text(stmt, 0);
|
||||
sqlite3_int64 count = sqlite3_column_int64(stmt, 1);
|
||||
|
||||
if (strcmp(period, "total") == 0) {
|
||||
cJSON_AddNumberToObject(time_stats, "total", count);
|
||||
} else if (strcmp(period, "24h") == 0) {
|
||||
cJSON_AddNumberToObject(time_stats, "last_24h", count);
|
||||
} else if (strcmp(period, "7d") == 0) {
|
||||
cJSON_AddNumberToObject(time_stats, "last_7d", count);
|
||||
} else if (strcmp(period, "30d") == 0) {
|
||||
cJSON_AddNumberToObject(time_stats, "last_30d", count);
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
cJSON_AddItemToObject(response, "time_stats", time_stats);
|
||||
|
||||
// Query top pubkeys
|
||||
cJSON* top_pubkeys = cJSON_CreateArray();
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT pubkey, event_count, percentage FROM top_pubkeys_view ORDER BY event_count DESC LIMIT 10", -1, &stmt, NULL) == SQLITE_OK) {
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
cJSON* pubkey_obj = cJSON_CreateObject();
|
||||
const char* pubkey = (const char*)sqlite3_column_text(stmt, 0);
|
||||
cJSON_AddStringToObject(pubkey_obj, "pubkey", pubkey ? pubkey : "");
|
||||
cJSON_AddNumberToObject(pubkey_obj, "event_count", sqlite3_column_int64(stmt, 1));
|
||||
cJSON_AddNumberToObject(pubkey_obj, "percentage", sqlite3_column_double(stmt, 2));
|
||||
cJSON_AddItemToArray(top_pubkeys, pubkey_obj);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
cJSON_AddItemToObject(response, "top_pubkeys", top_pubkeys);
|
||||
|
||||
// Get database creation timestamp (oldest event)
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT MIN(created_at) FROM events", -1, &stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
sqlite3_int64 oldest_timestamp = sqlite3_column_int64(stmt, 0);
|
||||
if (oldest_timestamp > 0) {
|
||||
cJSON_AddNumberToObject(response, "database_created_at", (double)oldest_timestamp);
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
// Get latest event timestamp
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT MAX(created_at) FROM events", -1, &stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
sqlite3_int64 latest_timestamp = sqlite3_column_int64(stmt, 0);
|
||||
if (latest_timestamp > 0) {
|
||||
cJSON_AddNumberToObject(response, "latest_event_at", (double)latest_timestamp);
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
// Convert to JSON string
|
||||
char* json_string = cJSON_Print(response);
|
||||
cJSON_Delete(response);
|
||||
|
||||
if (json_string) {
|
||||
log_success("Stats JSON generated successfully");
|
||||
} else {
|
||||
log_error("Failed to generate stats JSON");
|
||||
}
|
||||
|
||||
return json_string;
|
||||
}
|
||||
|
||||
// Main NIP-17 processing function
|
||||
int process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message, size_t error_size, struct lws* wsi) {
|
||||
if (!gift_wrap_event || !error_message) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Step 1: Validate it's addressed to us
|
||||
if (!is_nip17_gift_wrap_for_relay(gift_wrap_event)) {
|
||||
strncpy(error_message, "NIP-17: Event is not a valid gift wrap for this relay", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Step 2: Get relay private key for decryption
|
||||
char* relay_privkey_hex = get_relay_private_key();
|
||||
if (!relay_privkey_hex) {
|
||||
strncpy(error_message, "NIP-17: Could not get relay private key for decryption", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Convert hex private key to bytes
|
||||
unsigned char relay_privkey[32];
|
||||
if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) {
|
||||
log_error("NIP-17: Failed to convert relay private key from hex");
|
||||
free(relay_privkey_hex);
|
||||
strncpy(error_message, "NIP-17: Failed to convert relay private key", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
free(relay_privkey_hex);
|
||||
|
||||
// Step 3: Decrypt and parse inner event using library function
|
||||
log_info("NIP-17: Attempting to decrypt gift wrap with nostr_nip17_receive_dm");
|
||||
cJSON* inner_dm = nostr_nip17_receive_dm(gift_wrap_event, relay_privkey);
|
||||
if (!inner_dm) {
|
||||
log_error("NIP-17: nostr_nip17_receive_dm returned NULL");
|
||||
// Debug: Print the gift wrap event
|
||||
char* gift_wrap_debug = cJSON_Print(gift_wrap_event);
|
||||
if (gift_wrap_debug) {
|
||||
char debug_msg[1024];
|
||||
snprintf(debug_msg, sizeof(debug_msg), "NIP-17: Gift wrap event: %.500s", gift_wrap_debug);
|
||||
log_error(debug_msg);
|
||||
free(gift_wrap_debug);
|
||||
}
|
||||
// Debug: Check if private key is valid
|
||||
char privkey_hex[65];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
sprintf(privkey_hex + (i * 2), "%02x", relay_privkey[i]);
|
||||
}
|
||||
privkey_hex[64] = '\0';
|
||||
char privkey_msg[128];
|
||||
snprintf(privkey_msg, sizeof(privkey_msg), "NIP-17: Using relay private key: %.16s...", privkey_hex);
|
||||
log_info(privkey_msg);
|
||||
|
||||
strncpy(error_message, "NIP-17: Failed to decrypt and parse inner DM event", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
log_info("NIP-17: Successfully decrypted gift wrap");
|
||||
|
||||
// Step 4: Process admin command
|
||||
int result = process_nip17_admin_command(inner_dm, error_message, error_size, wsi);
|
||||
|
||||
// Step 5: Create response if command was processed successfully
|
||||
if (result == 0) {
|
||||
// Get sender pubkey for response
|
||||
cJSON* sender_pubkey_obj = cJSON_GetObjectItem(gift_wrap_event, "pubkey");
|
||||
if (sender_pubkey_obj && cJSON_IsString(sender_pubkey_obj)) {
|
||||
const char* sender_pubkey = cJSON_GetStringValue(sender_pubkey_obj);
|
||||
|
||||
// Create success response using library function
|
||||
char response_content[1024];
|
||||
snprintf(response_content, sizeof(response_content),
|
||||
"[\"command_processed\", \"success\", \"%s\"]", "NIP-17 admin command executed");
|
||||
|
||||
// Get relay pubkey for creating DM event
|
||||
const char* relay_pubkey = get_relay_pubkey_cached();
|
||||
if (relay_pubkey) {
|
||||
cJSON* success_dm = nostr_nip17_create_chat_event(
|
||||
response_content, // message content
|
||||
(const char**)&sender_pubkey, // recipient pubkeys
|
||||
1, // num recipients
|
||||
NULL, // subject (optional)
|
||||
NULL, // reply_to_event_id (optional)
|
||||
NULL, // reply_relay_url (optional)
|
||||
relay_pubkey // sender pubkey
|
||||
);
|
||||
|
||||
if (success_dm) {
|
||||
cJSON* success_gift_wraps[1];
|
||||
int send_result = nostr_nip17_send_dm(
|
||||
success_dm, // dm_event
|
||||
(const char**)&sender_pubkey, // recipient_pubkeys
|
||||
1, // num_recipients
|
||||
relay_privkey, // sender_private_key
|
||||
success_gift_wraps, // gift_wraps_out
|
||||
1 // max_gift_wraps
|
||||
);
|
||||
|
||||
cJSON_Delete(success_dm);
|
||||
|
||||
if (send_result == 1 && success_gift_wraps[0]) {
|
||||
store_event(success_gift_wraps[0]);
|
||||
cJSON_Delete(success_gift_wraps[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(inner_dm);
|
||||
return result;
|
||||
}
|
||||
@@ -17,4 +17,7 @@ struct embedded_file_session_data {
|
||||
// Handle HTTP request for embedded API files
|
||||
int handle_embedded_file_request(struct lws* wsi, const char* requested_uri);
|
||||
|
||||
// Generate stats JSON from database queries
|
||||
char* generate_stats_json(void);
|
||||
|
||||
#endif // API_H
|
||||
142
src/config.c
142
src/config.c
@@ -2956,21 +2956,54 @@ int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_si
|
||||
log_info("DEBUG: NIP-44 decryption successful");
|
||||
printf(" Decrypted content: %s\n", decrypted_text);
|
||||
printf(" Decrypted length: %zu\n", strlen(decrypted_text));
|
||||
|
||||
// Parse decrypted content as JSON array
|
||||
log_info("DEBUG: Parsing decrypted content as JSON");
|
||||
decrypted_content = cJSON_Parse(decrypted_text);
|
||||
|
||||
if (!decrypted_content || !cJSON_IsArray(decrypted_content)) {
|
||||
log_error("DEBUG: Decrypted content is not valid JSON array");
|
||||
|
||||
// Parse decrypted content as inner event JSON (NIP-17)
|
||||
log_info("DEBUG: Parsing decrypted content as inner event JSON");
|
||||
cJSON* inner_event = cJSON_Parse(decrypted_text);
|
||||
|
||||
if (!inner_event || !cJSON_IsObject(inner_event)) {
|
||||
log_error("DEBUG: Decrypted content is not valid inner event JSON");
|
||||
printf(" Decrypted content type: %s\n",
|
||||
decrypted_content ? (cJSON_IsArray(decrypted_content) ? "array" : "other") : "null");
|
||||
snprintf(error_message, error_size, "error: decrypted content is not valid JSON array");
|
||||
inner_event ? (cJSON_IsObject(inner_event) ? "object" : "other") : "null");
|
||||
cJSON_Delete(inner_event);
|
||||
snprintf(error_message, error_size, "error: decrypted content is not valid inner event JSON");
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("DEBUG: Decrypted content parsed successfully as JSON array");
|
||||
|
||||
log_info("DEBUG: Inner event parsed successfully");
|
||||
printf(" Inner event kind: %d\n", (int)cJSON_GetNumberValue(cJSON_GetObjectItem(inner_event, "kind")));
|
||||
|
||||
// Extract content from inner event
|
||||
cJSON* inner_content_obj = cJSON_GetObjectItem(inner_event, "content");
|
||||
if (!inner_content_obj || !cJSON_IsString(inner_content_obj)) {
|
||||
log_error("DEBUG: Inner event missing content field");
|
||||
cJSON_Delete(inner_event);
|
||||
snprintf(error_message, error_size, "error: inner event missing content field");
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* inner_content = cJSON_GetStringValue(inner_content_obj);
|
||||
log_info("DEBUG: Extracted inner content");
|
||||
printf(" Inner content: %s\n", inner_content);
|
||||
|
||||
// Parse inner content as JSON array (the command array)
|
||||
log_info("DEBUG: Parsing inner content as command JSON array");
|
||||
decrypted_content = cJSON_Parse(inner_content);
|
||||
|
||||
if (!decrypted_content || !cJSON_IsArray(decrypted_content)) {
|
||||
log_error("DEBUG: Inner content is not valid JSON array");
|
||||
printf(" Inner content type: %s\n",
|
||||
decrypted_content ? (cJSON_IsArray(decrypted_content) ? "array" : "other") : "null");
|
||||
cJSON_Delete(inner_event);
|
||||
snprintf(error_message, error_size, "error: inner content is not valid JSON array");
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("DEBUG: Inner content parsed successfully as JSON array");
|
||||
printf(" Array size: %d\n", cJSON_GetArraySize(decrypted_content));
|
||||
|
||||
// Clean up inner event
|
||||
cJSON_Delete(inner_event);
|
||||
|
||||
// Replace event content with decrypted command array for processing
|
||||
log_info("DEBUG: Replacing event content with decrypted marker");
|
||||
@@ -2979,16 +3012,11 @@ int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_si
|
||||
|
||||
// Create synthetic tags from decrypted command array
|
||||
log_info("DEBUG: Creating synthetic tags from decrypted command array");
|
||||
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
|
||||
if (!tags_obj) {
|
||||
log_info("DEBUG: No existing tags, creating new tags array");
|
||||
tags_obj = cJSON_CreateArray();
|
||||
cJSON_AddItemToObject(event, "tags", tags_obj);
|
||||
} else {
|
||||
log_info("DEBUG: Using existing tags array");
|
||||
printf(" Existing tags count: %d\n", cJSON_GetArraySize(tags_obj));
|
||||
}
|
||||
|
||||
printf(" Decrypted content array size: %d\n", cJSON_GetArraySize(decrypted_content));
|
||||
|
||||
// Create new tags array with command tag first
|
||||
cJSON* new_tags = cJSON_CreateArray();
|
||||
|
||||
// Add decrypted command as first tag
|
||||
if (cJSON_GetArraySize(decrypted_content) > 0) {
|
||||
log_info("DEBUG: Adding decrypted command as synthetic tag");
|
||||
@@ -2997,10 +3025,10 @@ int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_si
|
||||
const char* command_name = cJSON_GetStringValue(first_item);
|
||||
log_info("DEBUG: Creating command tag");
|
||||
printf(" Command: %s\n", command_name ? command_name : "null");
|
||||
|
||||
|
||||
cJSON* command_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(command_tag, cJSON_Duplicate(first_item, 1));
|
||||
|
||||
|
||||
// Add remaining items as tag values
|
||||
for (int i = 1; i < cJSON_GetArraySize(decrypted_content); i++) {
|
||||
cJSON* item = cJSON_GetArrayItem(decrypted_content, i);
|
||||
@@ -3013,17 +3041,31 @@ int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_si
|
||||
cJSON_AddItemToArray(command_tag, cJSON_Duplicate(item, 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Insert at beginning of tags array
|
||||
cJSON_InsertItemInArray(tags_obj, 0, command_tag);
|
||||
log_info("DEBUG: Synthetic command tag created and inserted");
|
||||
printf(" Final tag array size: %d\n", cJSON_GetArraySize(tags_obj));
|
||||
|
||||
cJSON_AddItemToArray(new_tags, command_tag);
|
||||
log_info("DEBUG: Synthetic command tag added to new tags array");
|
||||
printf(" New tags after adding command: %d\n", cJSON_GetArraySize(new_tags));
|
||||
} else {
|
||||
log_error("DEBUG: First item in decrypted array is not a string");
|
||||
}
|
||||
} else {
|
||||
log_error("DEBUG: Decrypted array is empty");
|
||||
}
|
||||
|
||||
// Add existing tags
|
||||
cJSON* existing_tags = cJSON_GetObjectItem(event, "tags");
|
||||
if (existing_tags && cJSON_IsArray(existing_tags)) {
|
||||
printf(" Existing tags count: %d\n", cJSON_GetArraySize(existing_tags));
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, existing_tags) {
|
||||
cJSON_AddItemToArray(new_tags, cJSON_Duplicate(tag, 1));
|
||||
}
|
||||
printf(" New tags after adding existing: %d\n", cJSON_GetArraySize(new_tags));
|
||||
}
|
||||
|
||||
// Replace event tags with new tags
|
||||
cJSON_ReplaceItemInObject(event, "tags", new_tags);
|
||||
printf(" Final tag array size: %d\n", cJSON_GetArraySize(new_tags));
|
||||
|
||||
cJSON_Delete(decrypted_content);
|
||||
} else {
|
||||
@@ -3034,19 +3076,29 @@ int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_si
|
||||
|
||||
// Parse first tag to determine action type (now from decrypted content if applicable)
|
||||
log_info("DEBUG: Parsing first tag to determine action type");
|
||||
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
|
||||
if (tags_obj && cJSON_IsArray(tags_obj)) {
|
||||
printf(" Tags array size: %d\n", cJSON_GetArraySize(tags_obj));
|
||||
for (int i = 0; i < cJSON_GetArraySize(tags_obj); i++) {
|
||||
cJSON* tag = cJSON_GetArrayItem(tags_obj, i);
|
||||
if (tag && cJSON_IsArray(tag) && cJSON_GetArraySize(tag) > 0) {
|
||||
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||
if (tag_name && cJSON_IsString(tag_name)) {
|
||||
printf(" Tag %d: %s\n", i, cJSON_GetStringValue(tag_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printf(" No tags array found\n");
|
||||
}
|
||||
|
||||
const char* action_type = get_first_tag_name(event);
|
||||
if (!action_type) {
|
||||
log_error("DEBUG: Missing or invalid first tag after processing");
|
||||
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
|
||||
if (tags_obj && cJSON_IsArray(tags_obj)) {
|
||||
printf(" Tags array size: %d\n", cJSON_GetArraySize(tags_obj));
|
||||
} else {
|
||||
printf(" No tags array found\n");
|
||||
}
|
||||
snprintf(error_message, error_size, "invalid: missing or invalid first tag");
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
log_info("DEBUG: Action type determined");
|
||||
printf(" Action type: %s\n", action_type);
|
||||
|
||||
@@ -3831,12 +3883,20 @@ int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_s
|
||||
|
||||
// Query time-based statistics
|
||||
cJSON* time_stats = cJSON_CreateObject();
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT total_events, events_24h, events_7d, events_30d FROM time_stats_view", -1, &stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
cJSON_AddNumberToObject(time_stats, "total", sqlite3_column_int64(stmt, 0));
|
||||
cJSON_AddNumberToObject(time_stats, "last_24h", sqlite3_column_int64(stmt, 1));
|
||||
cJSON_AddNumberToObject(time_stats, "last_7d", sqlite3_column_int64(stmt, 2));
|
||||
cJSON_AddNumberToObject(time_stats, "last_30d", sqlite3_column_int64(stmt, 3));
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT period, total_events FROM time_stats_view", -1, &stmt, NULL) == SQLITE_OK) {
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
const char* period = (const char*)sqlite3_column_text(stmt, 0);
|
||||
sqlite3_int64 count = sqlite3_column_int64(stmt, 1);
|
||||
|
||||
if (strcmp(period, "total") == 0) {
|
||||
cJSON_AddNumberToObject(time_stats, "total", count);
|
||||
} else if (strcmp(period, "24h") == 0) {
|
||||
cJSON_AddNumberToObject(time_stats, "last_24h", count);
|
||||
} else if (strcmp(period, "7d") == 0) {
|
||||
cJSON_AddNumberToObject(time_stats, "last_7d", count);
|
||||
} else if (strcmp(period, "30d") == 0) {
|
||||
cJSON_AddNumberToObject(time_stats, "last_30d", count);
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
@@ -57,15 +57,7 @@ extern int get_config_int(const char* key, int default_value);
|
||||
// NIP-42 constants (from nostr_core_lib)
|
||||
#define NOSTR_NIP42_AUTH_EVENT_KIND 22242
|
||||
|
||||
// NIP-42 error codes (from nostr_core_lib)
|
||||
#define NOSTR_ERROR_NIP42_CHALLENGE_NOT_FOUND -200
|
||||
#define NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED -201
|
||||
#define NOSTR_ERROR_NIP42_INVALID_CHALLENGE -202
|
||||
#define NOSTR_ERROR_NIP42_URL_MISMATCH -203
|
||||
#define NOSTR_ERROR_NIP42_TIME_TOLERANCE -204
|
||||
#define NOSTR_ERROR_NIP42_AUTH_EVENT_INVALID -205
|
||||
#define NOSTR_ERROR_NIP42_INVALID_RELAY_URL -206
|
||||
#define NOSTR_ERROR_NIP42_NOT_CONFIGURED -207
|
||||
// NIP-42 error codes (from nostr_core_lib - already defined in nostr_common.h)
|
||||
|
||||
// Forward declarations for NIP-42 functions (simple implementations for C-relay)
|
||||
int nostr_nip42_generate_challenge(char *challenge_buffer, size_t buffer_size);
|
||||
|
||||
284
src/websockets.c
284
src/websockets.c
@@ -68,6 +68,13 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length);
|
||||
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
int is_authorized_admin_event(cJSON* event, char* error_message, size_t error_size);
|
||||
|
||||
// Forward declarations for NIP-17 admin messaging
|
||||
int process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
|
||||
// Forward declarations for DM stats command handling
|
||||
int process_dm_stats_command(cJSON* dm_event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
|
||||
|
||||
// Forward declarations for NIP-09 deletion request handling
|
||||
int handle_deletion_request(cJSON* event, char* error_message, size_t error_size);
|
||||
|
||||
@@ -314,13 +321,38 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
// Check if NIP-42 authentication is required for this event kind or globally
|
||||
int auth_required = is_nip42_auth_globally_required() || is_nip42_auth_required_for_kind(event_kind);
|
||||
|
||||
// Special case: allow kind 14 DMs addressed to relay to bypass auth (admin commands)
|
||||
int bypass_auth = 0;
|
||||
if (event_kind == 14 && event_obj && cJSON_IsObject(event_obj)) {
|
||||
cJSON* tags = cJSON_GetObjectItem(event_obj, "tags");
|
||||
if (tags && cJSON_IsArray(tags)) {
|
||||
const char* relay_pubkey = get_relay_pubkey_cached();
|
||||
if (relay_pubkey) {
|
||||
cJSON* tag = NULL;
|
||||
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 (tag_name && cJSON_IsString(tag_name) &&
|
||||
strcmp(cJSON_GetStringValue(tag_name), "p") == 0 &&
|
||||
tag_value && cJSON_IsString(tag_value) &&
|
||||
strcmp(cJSON_GetStringValue(tag_value), relay_pubkey) == 0) {
|
||||
bypass_auth = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
char debug_auth_msg[256];
|
||||
snprintf(debug_auth_msg, sizeof(debug_auth_msg),
|
||||
"DEBUG AUTH: auth_required=%d, pss->authenticated=%d, event_kind=%d",
|
||||
auth_required, pss ? pss->authenticated : -1, event_kind);
|
||||
"DEBUG AUTH: auth_required=%d, bypass_auth=%d, pss->authenticated=%d, event_kind=%d",
|
||||
auth_required, bypass_auth, pss ? pss->authenticated : -1, event_kind);
|
||||
log_info(debug_auth_msg);
|
||||
|
||||
if (pss && auth_required && !pss->authenticated) {
|
||||
if (pss && auth_required && !pss->authenticated && !bypass_auth) {
|
||||
if (!pss->auth_challenge_sent) {
|
||||
log_info("DEBUG AUTH: Sending NIP-42 authentication challenge");
|
||||
send_nip42_auth_challenge(wsi, pss);
|
||||
@@ -606,6 +638,78 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
// Admin events are processed by the admin API, not broadcast to subscriptions
|
||||
}
|
||||
}
|
||||
} else if (event_kind == 1059) {
|
||||
// Check for NIP-17 gift wrap admin messages
|
||||
log_info("DEBUG NIP17: Detected kind 1059 gift wrap event");
|
||||
|
||||
char nip17_error[512] = {0};
|
||||
int nip17_result = process_nip17_admin_message(event, nip17_error, sizeof(nip17_error), wsi);
|
||||
|
||||
if (nip17_result != 0) {
|
||||
log_error("DEBUG NIP17: NIP-17 admin message processing failed");
|
||||
result = -1;
|
||||
size_t error_len = strlen(nip17_error);
|
||||
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
|
||||
memcpy(error_message, nip17_error, copy_len);
|
||||
error_message[copy_len] = '\0';
|
||||
|
||||
char debug_nip17_error_msg[600];
|
||||
snprintf(debug_nip17_error_msg, sizeof(debug_nip17_error_msg),
|
||||
"DEBUG NIP17 ERROR: %.400s", nip17_error);
|
||||
log_error(debug_nip17_error_msg);
|
||||
} else {
|
||||
log_success("DEBUG NIP17: NIP-17 admin message processed successfully");
|
||||
// Store the gift wrap event in database (unlike kind 23456)
|
||||
if (store_event(event) != 0) {
|
||||
log_error("DEBUG NIP17: Failed to store gift wrap event in database");
|
||||
result = -1;
|
||||
strncpy(error_message, "error: failed to store gift wrap event", sizeof(error_message) - 1);
|
||||
} else {
|
||||
log_info("DEBUG NIP17: Gift wrap event stored successfully in database");
|
||||
// Broadcast gift wrap event to matching persistent subscriptions
|
||||
int broadcast_count = broadcast_event_to_subscriptions(event);
|
||||
char debug_broadcast_msg[128];
|
||||
snprintf(debug_broadcast_msg, sizeof(debug_broadcast_msg),
|
||||
"DEBUG NIP17 BROADCAST: Gift wrap event broadcast to %d subscriptions", broadcast_count);
|
||||
log_info(debug_broadcast_msg);
|
||||
}
|
||||
}
|
||||
} else if (event_kind == 14) {
|
||||
// Check for DM stats commands addressed to relay
|
||||
log_info("DEBUG DM: Detected kind 14 DM event");
|
||||
|
||||
char dm_error[512] = {0};
|
||||
int dm_result = process_dm_stats_command(event, dm_error, sizeof(dm_error), wsi);
|
||||
|
||||
if (dm_result != 0) {
|
||||
log_error("DEBUG DM: DM stats command processing failed");
|
||||
result = -1;
|
||||
size_t error_len = strlen(dm_error);
|
||||
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
|
||||
memcpy(error_message, dm_error, copy_len);
|
||||
error_message[copy_len] = '\0';
|
||||
|
||||
char debug_dm_error_msg[600];
|
||||
snprintf(debug_dm_error_msg, sizeof(debug_dm_error_msg),
|
||||
"DEBUG DM ERROR: %.400s", dm_error);
|
||||
log_error(debug_dm_error_msg);
|
||||
} else {
|
||||
log_success("DEBUG DM: DM stats command processed successfully");
|
||||
// Store the DM event in database
|
||||
if (store_event(event) != 0) {
|
||||
log_error("DEBUG DM: Failed to store DM event in database");
|
||||
result = -1;
|
||||
strncpy(error_message, "error: failed to store DM event", sizeof(error_message) - 1);
|
||||
} else {
|
||||
log_info("DEBUG DM: DM event stored successfully in database");
|
||||
// Broadcast DM event to matching persistent subscriptions
|
||||
int broadcast_count = broadcast_event_to_subscriptions(event);
|
||||
char debug_broadcast_msg[128];
|
||||
snprintf(debug_broadcast_msg, sizeof(debug_broadcast_msg),
|
||||
"DEBUG DM BROADCAST: DM event broadcast to %d subscriptions", broadcast_count);
|
||||
log_info(debug_broadcast_msg);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Regular event - store in database and broadcast
|
||||
log_info("DEBUG STORAGE: Regular event - storing in database");
|
||||
@@ -1041,6 +1145,180 @@ int start_websocket_relay(int port_override, int strict_port) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Process DM stats command
|
||||
int process_dm_stats_command(cJSON* dm_event, char* error_message, size_t error_size, struct lws* wsi) {
|
||||
// Suppress unused parameter warning
|
||||
(void)wsi;
|
||||
|
||||
if (!dm_event || !error_message) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check if DM is addressed to relay
|
||||
cJSON* tags = cJSON_GetObjectItem(dm_event, "tags");
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
strncpy(error_message, "DM missing or invalid tags", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* relay_pubkey = get_relay_pubkey_cached();
|
||||
if (!relay_pubkey) {
|
||||
strncpy(error_message, "Could not get relay pubkey", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Look for "p" tag with relay pubkey
|
||||
int addressed_to_relay = 0;
|
||||
cJSON* tag = NULL;
|
||||
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 (tag_name && cJSON_IsString(tag_name) &&
|
||||
strcmp(cJSON_GetStringValue(tag_name), "p") == 0 &&
|
||||
tag_value && cJSON_IsString(tag_value) &&
|
||||
strcmp(cJSON_GetStringValue(tag_value), relay_pubkey) == 0) {
|
||||
addressed_to_relay = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!addressed_to_relay) {
|
||||
// Not addressed to relay, allow normal processing
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Get sender pubkey
|
||||
cJSON* pubkey_obj = cJSON_GetObjectItem(dm_event, "pubkey");
|
||||
if (!pubkey_obj || !cJSON_IsString(pubkey_obj)) {
|
||||
strncpy(error_message, "DM missing sender pubkey", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
const char* sender_pubkey = cJSON_GetStringValue(pubkey_obj);
|
||||
|
||||
// Check if sender is admin
|
||||
const char* admin_pubkey = get_admin_pubkey_cached();
|
||||
if (!admin_pubkey || strlen(admin_pubkey) == 0 ||
|
||||
strcmp(sender_pubkey, admin_pubkey) != 0) {
|
||||
strncpy(error_message, "Unauthorized: not admin", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Get relay private key for decryption
|
||||
char* relay_privkey_hex = get_relay_private_key();
|
||||
if (!relay_privkey_hex) {
|
||||
strncpy(error_message, "Could not get relay private key", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Convert relay private key to bytes
|
||||
unsigned char relay_privkey[32];
|
||||
if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) {
|
||||
free(relay_privkey_hex);
|
||||
strncpy(error_message, "Failed to convert relay private key", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
free(relay_privkey_hex);
|
||||
|
||||
// Convert sender pubkey to bytes
|
||||
unsigned char sender_pubkey_bytes[32];
|
||||
if (nostr_hex_to_bytes(sender_pubkey, sender_pubkey_bytes, sizeof(sender_pubkey_bytes)) != 0) {
|
||||
strncpy(error_message, "Failed to convert sender pubkey", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Get encrypted content
|
||||
cJSON* content_obj = cJSON_GetObjectItem(dm_event, "content");
|
||||
if (!content_obj || !cJSON_IsString(content_obj)) {
|
||||
strncpy(error_message, "DM missing content", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
const char* encrypted_content = cJSON_GetStringValue(content_obj);
|
||||
|
||||
// Decrypt content
|
||||
char decrypted_content[4096];
|
||||
int decrypt_result = nostr_nip44_decrypt(relay_privkey, sender_pubkey_bytes,
|
||||
encrypted_content, decrypted_content, sizeof(decrypted_content));
|
||||
|
||||
if (decrypt_result != NOSTR_SUCCESS) {
|
||||
char decrypt_error[256];
|
||||
snprintf(decrypt_error, sizeof(decrypt_error), "NIP-44 decryption failed: %d", decrypt_result);
|
||||
strncpy(error_message, decrypt_error, error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check if content is "stats"
|
||||
if (strcmp(decrypted_content, "stats") != 0) {
|
||||
// Not a stats command, allow normal processing
|
||||
return 0;
|
||||
}
|
||||
|
||||
log_info("Processing DM stats command from admin");
|
||||
|
||||
// Generate stats JSON
|
||||
char* stats_json = generate_stats_json();
|
||||
if (!stats_json) {
|
||||
strncpy(error_message, "Failed to generate stats", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Encrypt stats for response
|
||||
char encrypted_response[4096];
|
||||
int encrypt_result = nostr_nip44_encrypt(relay_privkey, sender_pubkey_bytes,
|
||||
stats_json, encrypted_response, sizeof(encrypted_response));
|
||||
|
||||
free(stats_json);
|
||||
|
||||
if (encrypt_result != NOSTR_SUCCESS) {
|
||||
char encrypt_error[256];
|
||||
snprintf(encrypt_error, sizeof(encrypt_error), "NIP-44 encryption failed: %d", encrypt_result);
|
||||
strncpy(error_message, encrypt_error, error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Create DM response event
|
||||
cJSON* dm_response = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(dm_response, "id", ""); // Will be set by event creation
|
||||
cJSON_AddStringToObject(dm_response, "pubkey", relay_pubkey);
|
||||
cJSON_AddNumberToObject(dm_response, "created_at", (double)time(NULL));
|
||||
cJSON_AddNumberToObject(dm_response, "kind", 14);
|
||||
cJSON_AddStringToObject(dm_response, "content", encrypted_response);
|
||||
|
||||
// Add tags: p tag for recipient (admin)
|
||||
cJSON* response_tags = cJSON_CreateArray();
|
||||
cJSON* p_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(p_tag, cJSON_CreateString("p"));
|
||||
cJSON_AddItemToArray(p_tag, cJSON_CreateString(sender_pubkey));
|
||||
cJSON_AddItemToArray(response_tags, p_tag);
|
||||
cJSON_AddItemToObject(dm_response, "tags", response_tags);
|
||||
|
||||
// Add signature placeholder
|
||||
cJSON_AddStringToObject(dm_response, "sig", ""); // Will be set by event creation/signing
|
||||
|
||||
// Store and broadcast the DM response
|
||||
int store_result = store_event(dm_response);
|
||||
if (store_result != 0) {
|
||||
cJSON_Delete(dm_response);
|
||||
strncpy(error_message, "Failed to store DM response", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Broadcast to subscriptions
|
||||
int broadcast_count = broadcast_event_to_subscriptions(dm_response);
|
||||
char broadcast_msg[128];
|
||||
snprintf(broadcast_msg, sizeof(broadcast_msg),
|
||||
"DM stats response broadcast to %d subscriptions", broadcast_count);
|
||||
log_info(broadcast_msg);
|
||||
|
||||
cJSON_Delete(dm_response);
|
||||
|
||||
log_success("DM stats command processed successfully");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// Handle NIP-45 COUNT message
|
||||
int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss) {
|
||||
(void)pss; // Suppress unused parameter warning
|
||||
|
||||
348
temp_schema.sql
Normal file
348
temp_schema.sql
Normal file
@@ -0,0 +1,348 @@
|
||||
|
||||
-- C Nostr Relay Database Schema\n\
|
||||
-- SQLite schema for storing Nostr events with JSON tags support\n\
|
||||
-- Configuration system using config table\n\
|
||||
\n\
|
||||
-- Schema version tracking\n\
|
||||
PRAGMA user_version = 7;\n\
|
||||
\n\
|
||||
-- Enable foreign key support\n\
|
||||
PRAGMA foreign_keys = ON;\n\
|
||||
\n\
|
||||
-- Optimize for performance\n\
|
||||
PRAGMA journal_mode = WAL;\n\
|
||||
PRAGMA synchronous = NORMAL;\n\
|
||||
PRAGMA cache_size = 10000;\n\
|
||||
\n\
|
||||
-- Core events table with hybrid single-table design\n\
|
||||
CREATE TABLE events (\n\
|
||||
id TEXT PRIMARY KEY, -- Nostr event ID (hex string)\n\
|
||||
pubkey TEXT NOT NULL, -- Public key of event author (hex string)\n\
|
||||
created_at INTEGER NOT NULL, -- Event creation timestamp (Unix timestamp)\n\
|
||||
kind INTEGER NOT NULL, -- Event kind (0-65535)\n\
|
||||
event_type TEXT NOT NULL CHECK (event_type IN ('regular', 'replaceable', 'ephemeral', 'addressable')),\n\
|
||||
content TEXT NOT NULL, -- Event content (text content only)\n\
|
||||
sig TEXT NOT NULL, -- Event signature (hex string)\n\
|
||||
tags JSON NOT NULL DEFAULT '[]', -- Event tags as JSON array\n\
|
||||
first_seen INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) -- When relay received event\n\
|
||||
);\n\
|
||||
\n\
|
||||
-- Core performance indexes\n\
|
||||
CREATE INDEX idx_events_pubkey ON events(pubkey);\n\
|
||||
CREATE INDEX idx_events_kind ON events(kind);\n\
|
||||
CREATE INDEX idx_events_created_at ON events(created_at DESC);\n\
|
||||
CREATE INDEX idx_events_event_type ON events(event_type);\n\
|
||||
\n\
|
||||
-- Composite indexes for common query patterns\n\
|
||||
CREATE INDEX idx_events_kind_created_at ON events(kind, created_at DESC);\n\
|
||||
CREATE INDEX idx_events_pubkey_created_at ON events(pubkey, created_at DESC);\n\
|
||||
CREATE INDEX idx_events_pubkey_kind ON events(pubkey, kind);\n\
|
||||
\n\
|
||||
-- Schema information table\n\
|
||||
CREATE TABLE schema_info (\n\
|
||||
key TEXT PRIMARY KEY,\n\
|
||||
value TEXT NOT NULL,\n\
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n\
|
||||
);\n\
|
||||
\n\
|
||||
-- Insert schema metadata\n\
|
||||
INSERT INTO schema_info (key, value) VALUES\n\
|
||||
('version', '7'),\n\
|
||||
('description', 'Hybrid Nostr relay schema with event-based and table-based configuration'),\n\
|
||||
('created_at', strftime('%s', 'now'));\n\
|
||||
\n\
|
||||
-- Helper views for common queries\n\
|
||||
CREATE VIEW recent_events AS\n\
|
||||
SELECT id, pubkey, created_at, kind, event_type, content\n\
|
||||
FROM events\n\
|
||||
WHERE event_type != 'ephemeral'\n\
|
||||
ORDER BY created_at DESC\n\
|
||||
LIMIT 1000;\n\
|
||||
\n\
|
||||
CREATE VIEW event_stats AS\n\
|
||||
SELECT \n\
|
||||
event_type,\n\
|
||||
COUNT(*) as count,\n\
|
||||
AVG(length(content)) as avg_content_length,\n\
|
||||
MIN(created_at) as earliest,\n\
|
||||
MAX(created_at) as latest\n\
|
||||
FROM events\n\
|
||||
GROUP BY event_type;\n\
|
||||
\n\
|
||||
-- Configuration events view (kind 33334)\n\
|
||||
CREATE VIEW configuration_events AS\n\
|
||||
SELECT \n\
|
||||
id,\n\
|
||||
pubkey as admin_pubkey,\n\
|
||||
created_at,\n\
|
||||
content,\n\
|
||||
tags,\n\
|
||||
sig\n\
|
||||
FROM events\n\
|
||||
WHERE kind = 33334\n\
|
||||
ORDER BY created_at DESC;\n\
|
||||
\n\
|
||||
-- Optimization: Trigger for automatic cleanup of ephemeral events older than 1 hour\n\
|
||||
CREATE TRIGGER cleanup_ephemeral_events\n\
|
||||
AFTER INSERT ON events\n\
|
||||
WHEN NEW.event_type = 'ephemeral'\n\
|
||||
BEGIN\n\
|
||||
DELETE FROM events \n\
|
||||
WHERE event_type = 'ephemeral' \n\
|
||||
AND first_seen < (strftime('%s', 'now') - 3600);\n\
|
||||
END;\n\
|
||||
\n\
|
||||
-- Replaceable event handling trigger\n\
|
||||
CREATE TRIGGER handle_replaceable_events\n\
|
||||
AFTER INSERT ON events\n\
|
||||
WHEN NEW.event_type = 'replaceable'\n\
|
||||
BEGIN\n\
|
||||
DELETE FROM events \n\
|
||||
WHERE pubkey = NEW.pubkey \n\
|
||||
AND kind = NEW.kind \n\
|
||||
AND event_type = 'replaceable'\n\
|
||||
AND id != NEW.id;\n\
|
||||
END;\n\
|
||||
\n\
|
||||
-- Addressable event handling trigger (for kind 33334 configuration events)\n\
|
||||
CREATE TRIGGER handle_addressable_events\n\
|
||||
AFTER INSERT ON events\n\
|
||||
WHEN NEW.event_type = 'addressable'\n\
|
||||
BEGIN\n\
|
||||
-- For kind 33334 (configuration), replace previous config from same admin\n\
|
||||
DELETE FROM events \n\
|
||||
WHERE pubkey = NEW.pubkey \n\
|
||||
AND kind = NEW.kind \n\
|
||||
AND event_type = 'addressable'\n\
|
||||
AND id != NEW.id;\n\
|
||||
END;\n\
|
||||
\n\
|
||||
-- Relay Private Key Secure Storage\n\
|
||||
-- Stores the relay's private key separately from public configuration\n\
|
||||
CREATE TABLE relay_seckey (\n\
|
||||
private_key_hex TEXT NOT NULL CHECK (length(private_key_hex) = 64),\n\
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n\
|
||||
);\n\
|
||||
\n\
|
||||
-- Authentication Rules Table for NIP-42 and Policy Enforcement\n\
|
||||
-- Used by request_validator.c for unified validation\n\
|
||||
CREATE TABLE auth_rules (\n\
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,\n\
|
||||
rule_type TEXT NOT NULL CHECK (rule_type IN ('whitelist', 'blacklist', 'rate_limit', 'auth_required')),\n\
|
||||
pattern_type TEXT NOT NULL CHECK (pattern_type IN ('pubkey', 'kind', 'ip', 'global')),\n\
|
||||
pattern_value TEXT,\n\
|
||||
action TEXT NOT NULL CHECK (action IN ('allow', 'deny', 'require_auth', 'rate_limit')),\n\
|
||||
parameters TEXT, -- JSON parameters for rate limiting, etc.\n\
|
||||
active INTEGER NOT NULL DEFAULT 1,\n\
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n\
|
||||
);\n\
|
||||
\n\
|
||||
-- Indexes for auth_rules performance\n\
|
||||
CREATE INDEX idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value);\n\
|
||||
CREATE INDEX idx_auth_rules_type ON auth_rules(rule_type);\n\
|
||||
CREATE INDEX idx_auth_rules_active ON auth_rules(active);\n\
|
||||
\n\
|
||||
-- Configuration Table for Table-Based Config Management\n\
|
||||
-- Hybrid system supporting both event-based and table-based configuration\n\
|
||||
CREATE TABLE config (\n\
|
||||
key TEXT PRIMARY KEY,\n\
|
||||
value TEXT NOT NULL,\n\
|
||||
data_type TEXT NOT NULL CHECK (data_type IN ('string', 'integer', 'boolean', 'json')),\n\
|
||||
description TEXT,\n\
|
||||
category TEXT DEFAULT 'general',\n\
|
||||
requires_restart INTEGER DEFAULT 0,\n\
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n\
|
||||
);\n\
|
||||
\n\
|
||||
-- Indexes for config table performance\n\
|
||||
CREATE INDEX idx_config_category ON config(category);\n\
|
||||
CREATE INDEX idx_config_restart ON config(requires_restart);\n\
|
||||
CREATE INDEX idx_config_updated ON config(updated_at DESC);\n\
|
||||
\n\
|
||||
-- Trigger to update config timestamp on changes\n\
|
||||
CREATE TRIGGER update_config_timestamp\n\
|
||||
AFTER UPDATE ON config\n\
|
||||
FOR EACH ROW\n\
|
||||
BEGIN\n\
|
||||
UPDATE config SET updated_at = strftime('%s', 'now') WHERE key = NEW.key;\n\
|
||||
END;\n\
|
||||
\n\
|
||||
-- Insert default configuration values\n\
|
||||
INSERT INTO config (key, value, data_type, description, category, requires_restart) VALUES\n\
|
||||
('relay_description', 'A C Nostr Relay', 'string', 'Relay description', 'general', 0),\n\
|
||||
('relay_contact', '', 'string', 'Relay contact information', 'general', 0),\n\
|
||||
('relay_software', 'https://github.com/laanwj/c-relay', 'string', 'Relay software URL', 'general', 0),\n\
|
||||
('relay_version', '1.0.0', 'string', 'Relay version', 'general', 0),\n\
|
||||
('relay_port', '8888', 'integer', 'Relay port number', 'network', 1),\n\
|
||||
('max_connections', '1000', 'integer', 'Maximum concurrent connections', 'network', 1),\n\
|
||||
('auth_enabled', 'false', 'boolean', 'Enable NIP-42 authentication', 'auth', 0),\n\
|
||||
('nip42_auth_required_events', 'false', 'boolean', 'Require auth for event publishing', 'auth', 0),\n\
|
||||
('nip42_auth_required_subscriptions', 'false', 'boolean', 'Require auth for subscriptions', 'auth', 0),\n\
|
||||
('nip42_auth_required_kinds', '[]', 'json', 'Event kinds requiring authentication', 'auth', 0),\n\
|
||||
('nip42_challenge_expiration', '600', 'integer', 'Auth challenge expiration seconds', 'auth', 0),\n\
|
||||
('pow_min_difficulty', '0', 'integer', 'Minimum proof-of-work difficulty', 'validation', 0),\n\
|
||||
('pow_mode', 'optional', 'string', 'Proof-of-work mode', 'validation', 0),\n\
|
||||
('nip40_expiration_enabled', 'true', 'boolean', 'Enable event expiration', 'validation', 0),\n\
|
||||
('nip40_expiration_strict', 'false', 'boolean', 'Strict expiration mode', 'validation', 0),\n\
|
||||
('nip40_expiration_filter', 'true', 'boolean', 'Filter expired events in queries', 'validation', 0),\n\
|
||||
('nip40_expiration_grace_period', '60', 'integer', 'Expiration grace period seconds', 'validation', 0),\n\
|
||||
('max_subscriptions_per_client', '25', 'integer', 'Maximum subscriptions per client', 'limits', 0),\n\
|
||||
('max_total_subscriptions', '1000', 'integer', 'Maximum total subscriptions', 'limits', 0),\n\
|
||||
('max_filters_per_subscription', '10', 'integer', 'Maximum filters per subscription', 'limits', 0),\n\
|
||||
('max_event_tags', '2000', 'integer', 'Maximum tags per event', 'limits', 0),\n\
|
||||
('max_content_length', '100000', 'integer', 'Maximum event content length', 'limits', 0),\n\
|
||||
('max_message_length', '131072', 'integer', 'Maximum WebSocket message length', 'limits', 0),\n\
|
||||
('default_limit', '100', 'integer', 'Default query limit', 'limits', 0),\n\
|
||||
('max_limit', '5000', 'integer', 'Maximum query limit', 'limits', 0);\n\
|
||||
\n\
|
||||
-- Persistent Subscriptions Logging Tables (Phase 2)\n\
|
||||
-- Optional database logging for subscription analytics and debugging\n\
|
||||
\n\
|
||||
-- Subscription events log\n\
|
||||
CREATE TABLE subscription_events (\n\
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,\n\
|
||||
subscription_id TEXT NOT NULL, -- Subscription ID from client\n\
|
||||
client_ip TEXT NOT NULL, -- Client IP address\n\
|
||||
event_type TEXT NOT NULL CHECK (event_type IN ('created', 'closed', 'expired', 'disconnected')),\n\
|
||||
filter_json TEXT, -- JSON representation of filters (for created events)\n\
|
||||
events_sent INTEGER DEFAULT 0, -- Number of events sent to this subscription\n\
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\
|
||||
ended_at INTEGER, -- When subscription ended (for closed/expired/disconnected)\n\
|
||||
duration INTEGER -- Computed: ended_at - created_at\n\
|
||||
);\n\
|
||||
\n\
|
||||
-- Subscription metrics summary\n\
|
||||
CREATE TABLE subscription_metrics (\n\
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,\n\
|
||||
date TEXT NOT NULL, -- Date (YYYY-MM-DD)\n\
|
||||
total_created INTEGER DEFAULT 0, -- Total subscriptions created\n\
|
||||
total_closed INTEGER DEFAULT 0, -- Total subscriptions closed\n\
|
||||
total_events_broadcast INTEGER DEFAULT 0, -- Total events broadcast\n\
|
||||
avg_duration REAL DEFAULT 0, -- Average subscription duration\n\
|
||||
peak_concurrent INTEGER DEFAULT 0, -- Peak concurrent subscriptions\n\
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\
|
||||
UNIQUE(date)\n\
|
||||
);\n\
|
||||
\n\
|
||||
-- Event broadcasting log (optional, for detailed analytics)\n\
|
||||
CREATE TABLE event_broadcasts (\n\
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,\n\
|
||||
event_id TEXT NOT NULL, -- Event ID that was broadcast\n\
|
||||
subscription_id TEXT NOT NULL, -- Subscription that received it\n\
|
||||
client_ip TEXT NOT NULL, -- Client IP\n\
|
||||
broadcast_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\
|
||||
FOREIGN KEY (event_id) REFERENCES events(id)\n\
|
||||
);\n\
|
||||
\n\
|
||||
-- Indexes for subscription logging performance\n\
|
||||
CREATE INDEX idx_subscription_events_id ON subscription_events(subscription_id);\n\
|
||||
CREATE INDEX idx_subscription_events_type ON subscription_events(event_type);\n\
|
||||
CREATE INDEX idx_subscription_events_created ON subscription_events(created_at DESC);\n\
|
||||
CREATE INDEX idx_subscription_events_client ON subscription_events(client_ip);\n\
|
||||
\n\
|
||||
CREATE INDEX idx_subscription_metrics_date ON subscription_metrics(date DESC);\n\
|
||||
\n\
|
||||
CREATE INDEX idx_event_broadcasts_event ON event_broadcasts(event_id);\n\
|
||||
CREATE INDEX idx_event_broadcasts_sub ON event_broadcasts(subscription_id);\n\
|
||||
CREATE INDEX idx_event_broadcasts_time ON event_broadcasts(broadcast_at DESC);\n\
|
||||
\n\
|
||||
-- Trigger to update subscription duration when ended\n\
|
||||
CREATE TRIGGER update_subscription_duration\n\
|
||||
AFTER UPDATE OF ended_at ON subscription_events\n\
|
||||
WHEN NEW.ended_at IS NOT NULL AND OLD.ended_at IS NULL\n\
|
||||
BEGIN\n\
|
||||
UPDATE subscription_events\n\
|
||||
SET duration = NEW.ended_at - NEW.created_at\n\
|
||||
WHERE id = NEW.id;\n\
|
||||
END;\n\
|
||||
\n\
|
||||
-- View for subscription analytics\n\
|
||||
CREATE VIEW subscription_analytics AS\n\
|
||||
SELECT\n\
|
||||
date(created_at, 'unixepoch') as date,\n\
|
||||
COUNT(*) as subscriptions_created,\n\
|
||||
COUNT(CASE WHEN ended_at IS NOT NULL THEN 1 END) as subscriptions_ended,\n\
|
||||
AVG(CASE WHEN duration IS NOT NULL THEN duration END) as avg_duration_seconds,\n\
|
||||
MAX(events_sent) as max_events_sent,\n\
|
||||
AVG(events_sent) as avg_events_sent,\n\
|
||||
COUNT(DISTINCT client_ip) as unique_clients\n\
|
||||
FROM subscription_events\n\
|
||||
GROUP BY date(created_at, 'unixepoch')\n\
|
||||
ORDER BY date DESC;\n\
|
||||
\n\
|
||||
-- View for current active subscriptions (from log perspective)\n\
|
||||
CREATE VIEW active_subscriptions_log AS\n\
|
||||
SELECT\n\
|
||||
subscription_id,\n\
|
||||
client_ip,\n\
|
||||
filter_json,\n\
|
||||
events_sent,\n\
|
||||
created_at,\n\
|
||||
(strftime('%s', 'now') - created_at) as duration_seconds\n\
|
||||
FROM subscription_events\n\
|
||||
WHERE event_type = 'created'\n\
|
||||
AND subscription_id NOT IN (\n\
|
||||
SELECT subscription_id FROM subscription_events\n\
|
||||
WHERE event_type IN ('closed', 'expired', 'disconnected')\n\
|
||||
);\n\
|
||||
\n\
|
||||
-- Database Statistics Views for Admin API\n\
|
||||
-- Event kinds distribution view\n\
|
||||
CREATE VIEW event_kinds_view AS\n\
|
||||
SELECT\n\
|
||||
kind,\n\
|
||||
COUNT(*) as count,\n\
|
||||
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM events), 2) as percentage\n\
|
||||
FROM events\n\
|
||||
GROUP BY kind\n\
|
||||
ORDER BY count DESC;\n\
|
||||
\n\
|
||||
-- Top pubkeys by event count view\n\
|
||||
CREATE VIEW top_pubkeys_view AS\n\
|
||||
SELECT\n\
|
||||
pubkey,\n\
|
||||
COUNT(*) as event_count,\n\
|
||||
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM events), 2) as percentage\n\
|
||||
FROM events\n\
|
||||
GROUP BY pubkey\n\
|
||||
ORDER BY event_count DESC\n\
|
||||
LIMIT 10;\n\
|
||||
\n\
|
||||
-- Time-based statistics view\n\
|
||||
CREATE VIEW time_stats_view AS\n\
|
||||
SELECT\n\
|
||||
'total' as period,\n\
|
||||
COUNT(*) as total_events,\n\
|
||||
COUNT(DISTINCT pubkey) as unique_pubkeys,\n\
|
||||
MIN(created_at) as oldest_event,\n\
|
||||
MAX(created_at) as newest_event\n\
|
||||
FROM events\n\
|
||||
UNION ALL\n\
|
||||
SELECT\n\
|
||||
'24h' as period,\n\
|
||||
COUNT(*) as total_events,\n\
|
||||
COUNT(DISTINCT pubkey) as unique_pubkeys,\n\
|
||||
MIN(created_at) as oldest_event,\n\
|
||||
MAX(created_at) as newest_event\n\
|
||||
FROM events\n\
|
||||
WHERE created_at >= (strftime('%s', 'now') - 86400)\n\
|
||||
UNION ALL\n\
|
||||
SELECT\n\
|
||||
'7d' as period,\n\
|
||||
COUNT(*) as total_events,\n\
|
||||
COUNT(DISTINCT pubkey) as unique_pubkeys,\n\
|
||||
MIN(created_at) as oldest_event,\n\
|
||||
MAX(created_at) as newest_event\n\
|
||||
FROM events\n\
|
||||
WHERE created_at >= (strftime('%s', 'now') - 604800)\n\
|
||||
UNION ALL\n\
|
||||
SELECT\n\
|
||||
'30d' as period,\n\
|
||||
COUNT(*) as total_events,\n\
|
||||
COUNT(DISTINCT pubkey) as unique_pubkeys,\n\
|
||||
MIN(created_at) as oldest_event,\n\
|
||||
MAX(created_at) as newest_event\n\
|
||||
FROM events\n\
|
||||
WHERE created_at >= (strftime('%s', 'now') - 2592000);
|
||||
26
test_stats_query.sh
Executable file
26
test_stats_query.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test script for stats query functionality
|
||||
# Uses the admin private key generated during startup
|
||||
|
||||
ADMIN_PRIVKEY="5f43e99864c3b2a3d10fa6aa25d3042936017e929c6f82d2b4c974af4502af21"
|
||||
ADMIN_PUBKEY="8f0306d7d4e0ddadf43caeb72791e1a2c6185eec2301f56655f666adab153226"
|
||||
RELAY_PUBKEY="df5248728b4dfe4fa7cf760b2efa58fcd284111e7df2b9ddef09a11f17ffa0d0"
|
||||
|
||||
echo "Testing stats query with NIP-17 encryption..."
|
||||
echo "Admin pubkey: $ADMIN_PUBKEY"
|
||||
echo "Relay pubkey: $RELAY_PUBKEY"
|
||||
|
||||
# Create the command array for stats_query
|
||||
COMMAND='["stats_query"]'
|
||||
|
||||
echo "Command to encrypt: $COMMAND"
|
||||
|
||||
# For now, let's just check if the relay is running and can accept connections
|
||||
echo "Checking if relay is running..."
|
||||
curl -s -H "Accept: application/nostr+json" http://localhost:8888 | head -20
|
||||
|
||||
echo -e "\nTesting WebSocket connection..."
|
||||
timeout 5 wscat -c ws://localhost:8888 <<< '{"type": "REQ", "id": "test", "filters": []}' || echo "WebSocket test completed"
|
||||
|
||||
echo "Stats query test completed."
|
||||
114
tests/17_nip_test.sh
Executable file
114
tests/17_nip_test.sh
Executable file
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# nip17_stats_dm_test.sh - Test NIP-17 DM "stats" command functionality
|
||||
# Sends a DM with content "stats" to the relay and checks for response
|
||||
|
||||
# Test key configuration (from make_and_restart_relay.sh -t)
|
||||
ADMIN_PRIVATE_KEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
ADMIN_PUBLIC_KEY="6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3"
|
||||
RELAY_PUBLIC_KEY="4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"
|
||||
RELAY_URL="ws://localhost:8888"
|
||||
|
||||
echo "=== NIP-17 DM Stats Test ==="
|
||||
echo "Admin pubkey: $ADMIN_PUBLIC_KEY"
|
||||
echo "Relay pubkey: $RELAY_PUBLIC_KEY"
|
||||
echo "Relay URL: $RELAY_URL"
|
||||
echo ""
|
||||
|
||||
# Check if nak is available
|
||||
if ! command -v nak &> /dev/null; then
|
||||
echo "ERROR: nak command not found!"
|
||||
echo "Please install nak from https://github.com/fiatjaf/nak"
|
||||
echo "Or ensure it's in your PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ nak command found"
|
||||
|
||||
# Check if relay is running by testing connection
|
||||
echo "Testing relay connection..."
|
||||
if ! timeout 5 bash -c "</dev/tcp/localhost/8888" 2>/dev/null; then
|
||||
echo "ERROR: Relay does not appear to be running on localhost:8888"
|
||||
echo "Please start the relay first with: ./make_and_restart_relay.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Relay appears to be running"
|
||||
|
||||
# Create inner DM event JSON
|
||||
INNER_DM_JSON=$(cat <<EOF
|
||||
{
|
||||
"kind": 14,
|
||||
"pubkey": "$ADMIN_PUBLIC_KEY",
|
||||
"created_at": $(date +%s),
|
||||
"tags": [["p", "$RELAY_PUBLIC_KEY"]],
|
||||
"content": "[\"stats\"]"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "Inner DM JSON:"
|
||||
echo "$INNER_DM_JSON"
|
||||
|
||||
# Encrypt the inner DM JSON with NIP-44 using relay pubkey
|
||||
echo ""
|
||||
echo "Encrypting inner DM with NIP-44..."
|
||||
ENCRYPTED_CONTENT=$(nak encrypt -p "$RELAY_PUBLIC_KEY" --sec "$ADMIN_PRIVATE_KEY" "$INNER_DM_JSON" 2>&1)
|
||||
ENCRYPT_EXIT_CODE=$?
|
||||
|
||||
if [ $ENCRYPT_EXIT_CODE -ne 0 ]; then
|
||||
echo "ERROR: Failed to encrypt inner DM"
|
||||
echo "nak output: $ENCRYPTED_CONTENT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Inner DM encrypted successfully"
|
||||
echo "Encrypted content: $ENCRYPTED_CONTENT"
|
||||
|
||||
# Send NIP-17 gift wrap event
|
||||
echo ""
|
||||
echo "Sending NIP-17 gift wrap with encrypted DM..."
|
||||
echo "Command: nak event -k 1059 -p $RELAY_PUBLIC_KEY -c '$ENCRYPTED_CONTENT' --sec $ADMIN_PRIVATE_KEY $RELAY_URL"
|
||||
|
||||
DM_RESULT=$(nak event -k 1059 -p "$RELAY_PUBLIC_KEY" -c "$ENCRYPTED_CONTENT" --sec "$ADMIN_PRIVATE_KEY" "$RELAY_URL" 2>&1)
|
||||
DM_EXIT_CODE=$?
|
||||
|
||||
if [ $DM_EXIT_CODE -ne 0 ]; then
|
||||
echo "ERROR: Failed to send gift wrap"
|
||||
echo "nak output: $DM_RESULT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Gift wrap sent successfully"
|
||||
echo "nak output: $DM_RESULT"
|
||||
|
||||
# Wait a moment for processing
|
||||
echo ""
|
||||
echo "Waiting 3 seconds for relay to process and respond..."
|
||||
sleep 3
|
||||
|
||||
# Query for gift wrap responses from the relay (kind 1059 events authored by relay)
|
||||
echo ""
|
||||
echo "Querying for gift wrap responses from relay..."
|
||||
echo "Command: nak req -k 1059 --authors $RELAY_PUBLIC_KEY $RELAY_URL"
|
||||
|
||||
# Capture the output and filter for events
|
||||
RESPONSE_OUTPUT=$(nak req -k 1059 --authors "$RELAY_PUBLIC_KEY" "$RELAY_URL" 2>&1)
|
||||
REQ_EXIT_CODE=$?
|
||||
|
||||
echo ""
|
||||
echo "=== Relay DM Response ==="
|
||||
if [ $REQ_EXIT_CODE -eq 0 ]; then
|
||||
# Try to parse and pretty-print the JSON response
|
||||
echo "$RESPONSE_OUTPUT" | jq . 2>/dev/null || echo "$RESPONSE_OUTPUT"
|
||||
else
|
||||
echo "ERROR: Failed to query DMs"
|
||||
echo "nak output: $RESPONSE_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Test Complete ==="
|
||||
echo "If you see a gift wrap event above with encrypted content containing stats data,"
|
||||
echo "then the NIP-17 DM 'stats' command is working correctly."
|
||||
Reference in New Issue
Block a user