Compare commits

...

5 Commits

23 changed files with 1882 additions and 630 deletions

View File

@@ -1 +1 @@
src/embedded_web_content.c
src/embedded_web_content.c

View File

@@ -195,6 +195,9 @@ All commands are sent as NIP-44 encrypted JSON arrays in the event content. The
- `pow_min_difficulty`: Minimum proof-of-work difficulty
- `nip40_expiration_enabled`: Enable event expiration (`true`/`false`)
**Monitoring Settings:**
- `kind_24567_reporting_throttle_sec`: Minimum seconds between monitoring events (default: 5)
### Dynamic Configuration Updates
C-Relay supports **dynamic configuration updates** without requiring a restart for most settings. Configuration parameters are categorized as either **dynamic** (can be updated immediately) or **restart-required** (require relay restart to take effect).
@@ -391,6 +394,68 @@ SELECT
## Real-time Monitoring System
C-Relay includes a subscription-based monitoring system that broadcasts real-time relay statistics using ephemeral events (kind 24567).
### Activation
The monitoring system activates automatically when clients subscribe to kind 24567 events:
```json
["REQ", "monitoring-sub", {"kinds": [24567]}]
```
For specific monitoring types, use d-tag filters:
```json
["REQ", "event-kinds-sub", {"kinds": [24567], "#d": ["event_kinds"]}]
["REQ", "time-stats-sub", {"kinds": [24567], "#d": ["time_stats"]}]
["REQ", "top-pubkeys-sub", {"kinds": [24567], "#d": ["top_pubkeys"]}]
```
When no subscriptions exist, monitoring is dormant to conserve resources.
### Monitoring Event Types
| Type | d Tag | Description |
|------|-------|-------------|
| Event Distribution | `event_kinds` | Event count by kind with percentages |
| Time Statistics | `time_stats` | Events in last 24h, 7d, 30d |
| Top Publishers | `top_pubkeys` | Top 10 pubkeys by event count |
| Active Subscriptions | `active_subscriptions` | Current subscription details (admin only) |
| Subscription Details | `subscription_details` | Detailed subscription info (admin only) |
| CPU Metrics | `cpu_metrics` | Process CPU and memory usage |
### Event Structure
```json
{
"kind": 24567,
"pubkey": "<relay_pubkey>",
"created_at": <timestamp>,
"content": "{\"data_type\":\"event_kinds\",\"timestamp\":1234567890,...}",
"tags": [
["d", "event_kinds"]
]
}
```
### Configuration
- `kind_24567_reporting_throttle_sec`: Minimum seconds between monitoring events (default: 5)
### Web Dashboard Integration
The built-in web dashboard (`/api/`) automatically subscribes to monitoring events and displays real-time statistics.
### Performance Considerations
- Monitoring events are ephemeral (not stored in database)
- Throttling prevents excessive event generation
- Automatic activation/deactivation based on subscriptions
- Minimal overhead when no clients are monitoring
## Direct Messaging Admin System
In addition to the above admin API, c-relay allows the administrator to direct message the relay to get information or control some settings. As long as the administrator is signed in with any nostr client that allows sending nip-17 direct messages (DMs), they can control the relay.

View File

@@ -285,7 +285,7 @@ h1 {
border-bottom: var(--border-width) solid var(--border-color);
padding-bottom: 10px;
margin-bottom: 30px;
font-weight: normal;
font-weight: bold;
font-size: 24px;
font-family: var(--font-family);
color: var(--primary-color);
@@ -293,12 +293,32 @@ h1 {
h2 {
font-weight: normal;
padding-left: 10px;
text-align: center;
font-size: 16px;
font-family: var(--font-family);
color: var(--primary-color);
}
h3 {
font-weight: normal;
font-size: 12px;
font-family: var(--font-family);
color: var(--primary-color);
padding-bottom: 10px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: lighter;
font-size: 10px;
font-family: var(--font-family);
color: var(--primary-color);
}
.section {
background: var(--secondary-color);
border: var(--border-width) solid var(--border-color);
@@ -309,18 +329,21 @@ h2 {
margin-right:5px;
}
.section-header {
display: flex;
justify-content: center;
align-items: center;
padding-bottom: 15px;
}
.input-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
font-size: 14px;
font-family: var(--font-family);
color: var(--primary-color);
}
input,
textarea,
@@ -493,6 +516,24 @@ button:disabled {
border-radius: 0;
}
/* Relay Events Styles */
.status-message {
margin-top: 10px;
padding: 8px;
border-radius: var(--border-radius);
font-size: 14px;
font-family: var(--font-family);
text-align: center;
}
.relay-entry {
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
padding: 10px;
margin-bottom: 10px;
background: var(--secondary-color);
}
.config-value-input:focus {
border: 1px solid var(--accent-color);
background: var(--secondary-color);
@@ -662,14 +703,7 @@ button:disabled {
display: none;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
/* margin-bottom: 15px; */
/* border-bottom: var(--border-width) solid var(--border-color); */
/* padding-bottom: 10px; */
}
.countdown-btn {
width: auto;
@@ -950,10 +984,8 @@ button:disabled {
padding: 6px 8px;
text-align: left;
font-family: var(--font-family);
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 100px;
}
.sql-results-table th {

View File

@@ -16,6 +16,7 @@
<li><button class="nav-item" data-page="subscriptions">Subscriptions</button></li>
<li><button class="nav-item" data-page="configuration">Configuration</button></li>
<li><button class="nav-item" data-page="authorization">Authorization</button></li>
<li><button class="nav-item" data-page="relay-events">Relay Events</button></li>
<li><button class="nav-item" data-page="dm">DM</button></li>
<li><button class="nav-item" data-page="database">Database Query</button></li>
</ul>
@@ -31,31 +32,31 @@
<!-- Header with title and profile display -->
<div class="section">
<div class="header-content">
<div class="header-title clickable" id="header-title">
<span class="relay-letter" data-letter="R">R</span>
<span class="relay-letter" data-letter="E">E</span>
<span class="relay-letter" data-letter="L">L</span>
<span class="relay-letter" data-letter="A">A</span>
<span class="relay-letter" data-letter="Y">Y</span>
</div>
<div class="relay-info">
<div id="relay-name" class="relay-name">C-Relay</div>
<div id="relay-description" class="relay-description">Loading...</div>
<div id="relay-pubkey-container" class="relay-pubkey-container">
<div id="relay-pubkey" class="relay-pubkey">Loading...</div>
</div>
</div>
<div class="profile-area" id="profile-area" style="display: none;">
<div class="admin-label">admin</div>
<div class="profile-container">
<img id="header-user-image" class="header-user-image" alt="Profile" style="display: none;">
<span id="header-user-name" class="header-user-name">Loading...</span>
</div>
<!-- Logout dropdown -->
<!-- Dropdown menu removed - buttons moved to sidebar -->
<div class="header-content">
<div class="header-title clickable" id="header-title">
<span class="relay-letter" data-letter="R">R</span>
<span class="relay-letter" data-letter="E">E</span>
<span class="relay-letter" data-letter="L">L</span>
<span class="relay-letter" data-letter="A">A</span>
<span class="relay-letter" data-letter="Y">Y</span>
</div>
<div class="relay-info">
<div id="relay-name" class="relay-name">C-Relay</div>
<div id="relay-description" class="relay-description">Loading...</div>
<div id="relay-pubkey-container" class="relay-pubkey-container">
<div id="relay-pubkey" class="relay-pubkey">Loading...</div>
</div>
</div>
<div class="profile-area" id="profile-area" style="display: none;">
<div class="admin-label">admin</div>
<div class="profile-container">
<img id="header-user-image" class="header-user-image" alt="Profile" style="display: none;">
<span id="header-user-name" class="header-user-name">Loading...</span>
</div>
<!-- Logout dropdown -->
<!-- Dropdown menu removed - buttons moved to sidebar -->
</div>
</div>
</div>
@@ -67,11 +68,10 @@
</div>
<!-- DATABASE STATISTICS Section -->
<!-- Subscribe to kind 24567 events to receive real-time monitoring data -->
<div class="section flex-section" id="databaseStatisticsSection" style="display: none;">
<div class="section-header">
<h2>DATABASE STATISTICS</h2>
<!-- Monitoring is now subscription-based - no toggle button needed -->
<!-- Subscribe to kind 24567 events to receive real-time monitoring data -->
DATABASE STATISTICS
</div>
<!-- Event Rate Graph Container -->
@@ -206,7 +206,7 @@
<!-- SUBSCRIPTION DETAILS Section (Admin Only) -->
<div class="section flex-section" id="subscriptionDetailsSection" style="display: none;">
<div class="section-header">
<h2>ACTIVE SUBSCRIPTION DETAILS</h2>
ACTIVE SUBSCRIPTION DETAILS
</div>
<div class="input-group">
@@ -233,7 +233,9 @@
<!-- Testing Section -->
<div id="div_config" class="section flex-section" style="display: none;">
<h2>RELAY CONFIGURATION</h2>
<div class="section-header">
RELAY CONFIGURATION
</div>
<div id="config-display" class="hidden">
<div class="config-table-container">
<table class="config-table" id="config-table">
@@ -260,7 +262,7 @@
<!-- Auth Rules Management - Moved after configuration -->
<div class="section flex-section" id="authRulesSection" style="display: none;">
<div class="section-header">
<h2>AUTH RULES MANAGEMENT</h2>
AUTH RULES MANAGEMENT
</div>
<!-- Auth Rules Table -->
@@ -286,23 +288,23 @@
<!-- Combined Pubkey Auth Rule Section -->
<div class="input-group">
<label for="authRulePubkey">Pubkey (nsec or hex):</label>
<input type="text" id="authRulePubkey" placeholder="nsec1... or 64-character hex pubkey">
<div class="input-group">
<label for="authRulePubkey">Pubkey (nsec or hex):</label>
<input type="text" id="authRulePubkey" placeholder="nsec1... or 64-character hex pubkey">
</div>
<div id="whitelistWarning" class="warning-box" style="display: none;">
<strong>⚠️ WARNING:</strong> Adding whitelist rules changes relay behavior to whitelist-only
mode.
Only whitelisted users will be able to interact with the relay.
</div>
<div class="inline-buttons">
<button type="button" id="addWhitelistBtn" onclick="addWhitelistRule()">ADD TO
WHITELIST</button>
<button type="button" id="addBlacklistBtn" onclick="addBlacklistRule()">ADD TO
BLACKLIST</button>
<button type="button" id="refreshAuthRulesBtn">REFRESH</button>
</div>
</div>
<div id="whitelistWarning" class="warning-box" style="display: none;">
<strong>⚠️ WARNING:</strong> Adding whitelist rules changes relay behavior to whitelist-only
mode.
Only whitelisted users will be able to interact with the relay.
</div>
<div class="inline-buttons">
<button type="button" id="addWhitelistBtn" onclick="addWhitelistRule()">ADD TO
WHITELIST</button>
<button type="button" id="addBlacklistBtn" onclick="addBlacklistRule()">ADD TO
BLACKLIST</button>
<button type="button" id="refreshAuthRulesBtn">REFRESH</button>
</div>
</div>
@@ -322,7 +324,7 @@
</div>
<!-- Outbox -->
<div class="input-group">
<div>
<label for="dm-outbox">Send Message to Relay:</label>
<textarea id="dm-outbox" rows="4" placeholder="Enter your message to send to the relay..."></textarea>
</div>
@@ -341,6 +343,72 @@
</div>
</div>
<!-- RELAY EVENTS Section -->
<div class="section" id="relayEventsSection" style="display: none;">
<div class="section-header">
RELAY EVENTS MANAGEMENT
</div>
<!-- Kind 0: User Metadata -->
<div class="input-group">
<h3>Kind 0: User Metadata</h3>
<div class="form-group">
<label for="kind0-name">Name:</label>
<input type="text" id="kind0-name" placeholder="Relay Name">
</div>
<div class="form-group">
<label for="kind0-about">About:</label>
<textarea id="kind0-about" rows="3" placeholder="Relay Description"></textarea>
</div>
<div class="form-group">
<label for="kind0-picture">Picture URL:</label>
<input type="url" id="kind0-picture" placeholder="https://example.com/logo.png">
</div>
<div class="form-group">
<label for="kind0-banner">Banner URL:</label>
<input type="url" id="kind0-banner" placeholder="https://example.com/banner.png">
</div>
<div class="form-group">
<label for="kind0-nip05">NIP-05:</label>
<input type="text" id="kind0-nip05" placeholder="relay@example.com">
</div>
<div class="form-group">
<label for="kind0-website">Website:</label>
<input type="url" id="kind0-website" placeholder="https://example.com">
</div>
<div class="inline-buttons">
<button type="button" id="submit-kind0-btn">UPDATE METADATA</button>
</div>
<div id="kind0-status" class="status-message"></div>
</div>
<!-- Kind 10050: DM Relay List -->
<div class="input-group">
<h3>Kind 10050: DM Relay List</h3>
<div class="form-group">
<label for="kind10050-relays">Relay URLs (one per line):</label>
<textarea id="kind10050-relays" rows="4" placeholder="wss://relay1.com&#10;wss://relay2.com"></textarea>
</div>
<div class="inline-buttons">
<button type="button" id="submit-kind10050-btn">UPDATE DM RELAYS</button>
</div>
<div id="kind10050-status" class="status-message"></div>
</div>
<!-- Kind 10002: Relay List -->
<div class="input-group">
<h3>Kind 10002: Relay List</h3>
<div id="kind10002-relay-entries">
<!-- Dynamic relay entries will be added here -->
</div>
<div class="inline-buttons">
<button type="button" id="add-relay-entry-btn">ADD RELAY</button>
<button type="button" id="submit-kind10002-btn">UPDATE RELAYS</button>
</div>
<div id="kind10002-status" class="status-message"></div>
</div>
</div>
<!-- SQL QUERY Section -->
<div class="section" id="sqlQuerySection" style="display: none;">
<div class="section-header">

View File

@@ -46,6 +46,8 @@ let pendingSqlQueries = new Map();
let eventRateChart = null;
let previousTotalEvents = 0; // Track previous total for rate calculation
// Relay Events state - now handled by main subscription
// DOM elements
const loginModal = document.getElementById('login-modal');
const loginModalContainer = document.getElementById('login-modal-container');
@@ -1048,9 +1050,9 @@ async function subscribeToConfiguration() {
// Mark as subscribed BEFORE calling subscribeMany to prevent race conditions
isSubscribed = true;
// Subscribe to kind 23457 events (admin response events), kind 4 (NIP-04 DMs), kind 1059 (NIP-17 GiftWrap), and kind 24567 (ephemeral monitoring events)
console.log('🔔 Calling relayPool.subscribeMany...');
// Subscribe to kind 23457 events (admin response events), kind 4 (NIP-04 DMs), kind 1059 (NIP-17 GiftWrap), kind 24567 (ephemeral monitoring events), and relay events (kinds 0, 10050, 10002)
console.log('🔔 Calling relayPool.subscribeMany with all filters...');
const subscription = relayPool.subscribeMany([url], [{
since: Math.floor(Date.now() / 1000) - 5, // Look back 5 seconds to avoid race condition
kinds: [23457],
@@ -1064,7 +1066,7 @@ async function subscribeToConfiguration() {
"#p": [userPubkey], // Only DMs directed to this user
limit: 50
}, {
since: Math.floor(Date.now() / 1000), // Start from current time
since: Math.floor(Date.now() / 1000) - (2 * 24 * 60 * 60), // Look back 2 days for NIP-59 randomized timestamps
kinds: [1059], // NIP-17 GiftWrap events
"#p": [userPubkey], // Only GiftWrap events addressed to this user
limit: 50
@@ -1072,15 +1074,20 @@ async function subscribeToConfiguration() {
since: Math.floor(Date.now() / 1000), // Start from current time
kinds: [24567], // Real-time ephemeral monitoring events
authors: [getRelayPubkey()], // Only listen to monitoring events from the relay
"#d": isLoggedIn ? ["event_kinds", "time_stats", "top_pubkeys", "active_subscriptions", "subscription_details", "cpu_metrics"] : ["event_kinds", "time_stats", "top_pubkeys", "active_subscriptions", "cpu_metrics"], // Include subscription_details only when authenticated, cpu_metrics available to all
"#d": isLoggedIn ? ["event_kinds", "time_stats", "top_pubkeys", "subscription_details", "cpu_metrics"] : ["event_kinds", "time_stats", "top_pubkeys", "cpu_metrics"], // Include subscription_details only when authenticated, cpu_metrics available to all
limit: 50
}, {
since: Math.floor(Date.now() / 1000) - (24 * 60 * 60), // Look back 24 hours for relay events
kinds: [0, 10050, 10002], // Relay events: metadata, DM relays, relay list
authors: [getRelayPubkey()], // Only listen to relay's own events
limit: 10
}], {
async onevent(event) {
// Simplified logging - one line per event
if (event.kind === 24567) {
const dTag = event.tags.find(tag => tag[0] === 'd');
const dataType = dTag ? dTag[1] : 'unknown';
console.log(`📊 Monitoring event: ${dataType}`);
// console.log(`📊 Monitoring event: ${dataType}`);
} else {
console.log(`📨 Event received: kind ${event.kind}`);
}
@@ -1111,20 +1118,32 @@ async function subscribeToConfiguration() {
// Handle NIP-17 GiftWrap DMs
if (event.kind === 1059) {
console.log(`📨 RECEIVED KIND 1059 EVENT:`, {
id: event.id,
pubkey: event.pubkey,
created_at: event.created_at,
content: event.content.substring(0, 100) + '...',
tags: event.tags
});
try {
// Step 1: Unwrap gift wrap to get seal
const sealJson = await window.nostr.nip44.decrypt(event.pubkey, event.content);
console.log(`🔓 STEP 1 - Unwrapped gift wrap:`, sealJson.substring(0, 100) + '...');
const seal = safeJsonParse(sealJson);
if (!seal || seal.kind !== 13) {
throw new Error('Unwrapped content is not a valid seal (kind 13)');
}
console.log(`✅ Seal validated:`, { kind: seal.kind, pubkey: seal.pubkey.substring(0, 16) + '...' });
// Step 2: Unseal to get rumor
const rumorJson = await window.nostr.nip44.decrypt(seal.pubkey, seal.content);
console.log(`🔓 STEP 2 - Unsealed rumor:`, rumorJson.substring(0, 100) + '...');
const rumor = safeJsonParse(rumorJson);
if (!rumor || rumor.kind !== 14) {
throw new Error('Unsealed content is not a valid rumor (kind 14)');
}
console.log(`✅ Rumor validated:`, { kind: rumor.kind, pubkey: rumor.pubkey.substring(0, 16) + '...', content: rumor.content.substring(0, 50) + '...' });
log(`Received NIP-17 DM from relay: ${rumor.content.substring(0, 50)}...`, 'INFO');
@@ -1137,6 +1156,7 @@ async function subscribeToConfiguration() {
logTestEvent('RECV', `NIP-17 DM: ${rumor.content}`, 'DM');
}
} catch (unwrapError) {
console.error(`❌ NIP-17 DM UNWRAP FAILED:`, unwrapError);
log(`Failed to unwrap NIP-17 DM: ${unwrapError.message}`, 'ERROR');
if (typeof logTestEvent === 'function') {
logTestEvent('ERROR', `Failed to unwrap DM: ${unwrapError.message}`, 'DM');
@@ -1161,6 +1181,11 @@ async function subscribeToConfiguration() {
// Process monitoring event (logging done above)
processMonitoringEvent(event);
}
// Handle relay events (kinds 0, 10050, 10002)
if ([0, 10050, 10002].includes(event.kind)) {
handleRelayEventReceived(event);
}
},
oneose() {
console.log('EOSE received - End of stored events');
@@ -1227,9 +1252,19 @@ async function processAdminResponse(event) {
console.log('Decrypted admin response:', decryptedContent);
// Parse the decrypted JSON response
const responseData = JSON.parse(decryptedContent);
console.log('Parsed response data:', responseData);
// Try to parse as JSON first, if it fails treat as plain text
let responseData;
try {
responseData = JSON.parse(decryptedContent);
console.log('Parsed response data:', responseData);
} catch (parseError) {
// Not JSON - treat as plain text response
console.log('Response is plain text, not JSON');
responseData = {
plain_text: true,
message: decryptedContent
};
}
// Log the response for testing
if (typeof logTestEvent === 'function') {
@@ -1387,14 +1422,15 @@ async function processMonitoringEvent(event) {
updateStatsFromTopPubkeysMonitoringEvent(monitoringData);
break;
case 'active_subscriptions':
updateStatsFromActiveSubscriptionsMonitoringEvent(monitoringData);
break;
case 'subscription_details':
// Only process subscription details if user is authenticated
if (isLoggedIn) {
updateStatsFromSubscriptionDetailsMonitoringEvent(monitoringData);
// Also update the active subscriptions count from this data
if (monitoringData.data && monitoringData.data.subscriptions) {
updateStatsCell('active-subscriptions', monitoringData.data.subscriptions.length.toString());
}
}
break;
@@ -1419,6 +1455,25 @@ function handleAdminResponseData(responseData) {
console.log('Response data:', responseData);
console.log('Response query_type:', responseData.query_type);
// Handle plain text responses (from create_relay_event and other commands)
if (responseData.plain_text) {
console.log('Handling plain text response');
log(responseData.message, 'INFO');
// Show the message in relay events status if we're on that page
if (currentPage === 'relay-events') {
// Try to determine which kind based on message content
if (responseData.message.includes('Kind: 0')) {
showStatus('kind0-status', responseData.message, 'success');
} else if (responseData.message.includes('Kind: 10050')) {
showStatus('kind10050-status', responseData.message, 'success');
} else if (responseData.message.includes('Kind: 10002')) {
showStatus('kind10002-status', responseData.message, 'success');
}
}
return;
}
// Handle auth query responses - updated to match backend response types
if (responseData.query_type &&
(responseData.query_type.includes('auth_rules') ||
@@ -2106,8 +2161,8 @@ if (relayPubkeyContainer) {
const relayPubkeyElement = document.getElementById('relay-pubkey');
if (relayPubkeyElement && relayPubkeyElement.textContent !== 'Loading...') {
try {
// Get the full npub (remove line breaks for clipboard)
const fullNpub = relayPubkeyElement.textContent.replace(/\n/g, '');
// Get the full npub (remove all whitespace for continuous string)
const fullNpub = relayPubkeyElement.textContent.replace(/\s/g, '');
await navigator.clipboard.writeText(fullNpub);
@@ -3536,6 +3591,13 @@ async function sendNIP17DM() {
throw new Error('Failed to sign gift wrap event');
}
// DEBUG: Log NIP-17 event details when created
console.log('=== NIP-17 EVENT CREATED ===');
console.log('Full event:', JSON.stringify(signedGiftWrap, null, 2));
console.log('Timestamp:', signedGiftWrap.created_at);
console.log('Local date time:', new Date(signedGiftWrap.created_at * 1000).toLocaleString());
console.log('=== END NIP-17 EVENT DEBUG ===');
log('NIP-17 DM event created and signed with ephemeral key, publishing...', 'INFO');
// Publish via SimplePool
@@ -4002,27 +4064,14 @@ function updateStatsFromTopPubkeysMonitoringEvent(monitoringData) {
}
}
// Update statistics display from active_subscriptions monitoring event
function updateStatsFromActiveSubscriptionsMonitoringEvent(monitoringData) {
try {
if (monitoringData.data_type !== 'active_subscriptions') {
return;
}
// Update active subscriptions cell with real-time data
// The data is nested under monitoringData.data.total_subscriptions
if (monitoringData.data && monitoringData.data.total_subscriptions !== undefined) {
updateStatsCell('active-subscriptions', monitoringData.data.total_subscriptions.toString());
}
} catch (error) {
log(`Error updating active subscriptions from monitoring event: ${error.message}`, 'ERROR');
}
}
// Update statistics display from subscription_details monitoring event
function updateStatsFromSubscriptionDetailsMonitoringEvent(monitoringData) {
try {
// DEBUG: Log every subscription_details event that arrives at the webpage
console.log('subscription_details', JSON.stringify(monitoringData, null, 2));
console.log('subscription_details decoded:', monitoringData);
if (monitoringData.data_type !== 'subscription_details') {
return;
}
@@ -4578,14 +4627,15 @@ function switchPage(pageName) {
});
// Hide all sections
const sections = [
'databaseStatisticsSection',
'subscriptionDetailsSection',
'div_config',
'authRulesSection',
'nip17DMSection',
'sqlQuerySection'
];
const sections = [
'databaseStatisticsSection',
'subscriptionDetailsSection',
'div_config',
'authRulesSection',
'relayEventsSection',
'nip17DMSection',
'sqlQuerySection'
];
sections.forEach(sectionId => {
const section = document.getElementById(sectionId);
@@ -4600,6 +4650,7 @@ function switchPage(pageName) {
'subscriptions': 'subscriptionDetailsSection',
'configuration': 'div_config',
'authorization': 'authRulesSection',
'relay-events': 'relayEventsSection',
'dm': 'nip17DMSection',
'database': 'sqlQuerySection'
};
@@ -4995,13 +5046,15 @@ function displaySqlQueryResults(response) {
// Handle SQL query response (called by event listener)
function handleSqlQueryResponse(response) {
// Check if this is a response to one of our queries
if (response.request_id && pendingSqlQueries.has(response.request_id)) {
const queryInfo = pendingSqlQueries.get(response.request_id);
pendingSqlQueries.delete(response.request_id);
console.log('=== HANDLING SQL QUERY RESPONSE ===');
console.log('Response:', response);
// Display results
displaySqlQueryResults(response);
// Always display SQL query results when received
displaySqlQueryResults(response);
// Clean up any pending queries
if (response.request_id && pendingSqlQueries.has(response.request_id)) {
pendingSqlQueries.delete(response.request_id);
}
}
@@ -5220,6 +5273,355 @@ function initializeToggleButtonsFromConfig(configData) {
log('Monitoring system initialized - subscription-based activation ready', 'INFO');
}
// ================================
// RELAY EVENTS FUNCTIONS
// ================================
// Handle received relay events
function handleRelayEventReceived(event) {
console.log('Handling relay event:', event.kind, event);
switch (event.kind) {
case 0:
populateKind0Form(event);
break;
case 10050:
populateKind10050Form(event);
break;
case 10002:
populateKind10002Form(event);
break;
default:
console.log('Unknown relay event kind:', event.kind);
}
}
// Populate Kind 0 form (User Metadata)
function populateKind0Form(event) {
try {
const metadata = JSON.parse(event.content);
console.log('Populating Kind 0 form with:', metadata);
// Update form fields
const nameField = document.getElementById('kind0-name');
const aboutField = document.getElementById('kind0-about');
const pictureField = document.getElementById('kind0-picture');
const bannerField = document.getElementById('kind0-banner');
const nip05Field = document.getElementById('kind0-nip05');
const websiteField = document.getElementById('kind0-website');
if (nameField) nameField.value = metadata.name || '';
if (aboutField) aboutField.value = metadata.about || '';
if (pictureField) pictureField.value = metadata.picture || '';
if (bannerField) bannerField.value = metadata.banner || '';
if (nip05Field) nip05Field.value = metadata.nip05 || '';
if (websiteField) websiteField.value = metadata.website || '';
showStatus('kind0-status', 'Metadata loaded from relay', 'success');
} catch (error) {
console.error('Error populating Kind 0 form:', error);
showStatus('kind0-status', 'Error loading metadata', 'error');
}
}
// Populate Kind 10050 form (DM Relay List)
function populateKind10050Form(event) {
try {
console.log('Populating Kind 10050 form with tags:', event.tags);
// Extract relay URLs from "relay" tags
const relayUrls = event.tags
.filter(tag => tag[0] === 'relay' && tag[1])
.map(tag => tag[1]);
const relaysField = document.getElementById('kind10050-relays');
if (relaysField) {
relaysField.value = relayUrls.join('\n');
}
showStatus('kind10050-status', 'DM relay list loaded from relay', 'success');
} catch (error) {
console.error('Error populating Kind 10050 form:', error);
showStatus('kind10050-status', 'Error loading DM relay list', 'error');
}
}
// Populate Kind 10002 form (Relay List)
function populateKind10002Form(event) {
try {
console.log('Populating Kind 10002 form with tags:', event.tags);
// Clear existing entries
const container = document.getElementById('kind10002-relay-entries');
if (container) {
container.innerHTML = '';
}
// Extract relay entries from "r" tags
event.tags.forEach(tag => {
if (tag[0] === 'r' && tag[1]) {
const url = tag[1];
const marker = tag[2] || 'read'; // Default to read if no marker
const read = marker.includes('read');
const write = marker.includes('write');
addRelayEntry(url, read, write);
}
});
showStatus('kind10002-status', 'Relay list loaded from relay', 'success');
} catch (error) {
console.error('Error populating Kind 10002 form:', error);
showStatus('kind10002-status', 'Error loading relay list', 'error');
}
}
// Submit Kind 0 event
async function submitKind0Event() {
try {
showStatus('kind0-status', 'Submitting metadata...', 'info');
// Collect form data
const metadata = {
name: document.getElementById('kind0-name').value.trim(),
about: document.getElementById('kind0-about').value.trim(),
picture: document.getElementById('kind0-picture').value.trim(),
banner: document.getElementById('kind0-banner').value.trim(),
nip05: document.getElementById('kind0-nip05').value.trim(),
website: document.getElementById('kind0-website').value.trim()
};
// Remove empty fields
Object.keys(metadata).forEach(key => {
if (!metadata[key]) delete metadata[key];
});
// Validate required fields
if (!metadata.name) {
showStatus('kind0-status', 'Name is required', 'error');
return;
}
await sendCreateRelayEventCommand(0, metadata);
showStatus('kind0-status', 'Metadata updated successfully', 'success');
} catch (error) {
console.error('Error submitting Kind 0 event:', error);
showStatus('kind0-status', 'Error updating metadata: ' + error.message, 'error');
}
}
// Submit Kind 10050 event
async function submitKind10050Event() {
try {
showStatus('kind10050-status', 'Submitting DM relay list...', 'info');
// Parse textarea content
const relaysText = document.getElementById('kind10050-relays').value.trim();
const relays = relaysText.split('\n')
.map(url => url.trim())
.filter(url => url.length > 0)
.filter(url => isValidRelayUrl(url));
if (relays.length === 0) {
showStatus('kind10050-status', 'At least one valid relay URL is required', 'error');
return;
}
await sendCreateRelayEventCommand(10050, { relays });
showStatus('kind10050-status', 'DM relay list updated successfully', 'success');
} catch (error) {
console.error('Error submitting Kind 10050 event:', error);
showStatus('kind10050-status', 'Error updating DM relay list: ' + error.message, 'error');
}
}
// Submit Kind 10002 event
async function submitKind10002Event() {
try {
showStatus('kind10002-status', 'Submitting relay list...', 'info');
// Collect relay entries
const relays = [];
const entries = document.querySelectorAll('.relay-entry');
entries.forEach(entry => {
const url = entry.querySelector('.relay-url').value.trim();
const read = entry.querySelector('.relay-read').checked;
const write = entry.querySelector('.relay-write').checked;
if (url && isValidRelayUrl(url)) {
relays.push({
url: url,
read: read,
write: write
});
}
});
if (relays.length === 0) {
showStatus('kind10002-status', 'At least one valid relay entry is required', 'error');
return;
}
await sendCreateRelayEventCommand(10002, { relays });
showStatus('kind10002-status', 'Relay list updated successfully', 'success');
} catch (error) {
console.error('Error submitting Kind 10002 event:', error);
showStatus('kind10002-status', 'Error updating relay list: ' + error.message, 'error');
}
}
// Send create_relay_event command
async function sendCreateRelayEventCommand(kind, eventData) {
if (!isLoggedIn || !userPubkey) {
throw new Error('Must be logged in to create relay events');
}
if (!relayPool) {
throw new Error('SimplePool connection not available');
}
try {
console.log(`Sending create_relay_event command for kind ${kind}...`);
// Create command array
const command_array = ["create_relay_event", kind, eventData];
// Encrypt the command array
const encrypted_content = await encryptForRelay(JSON.stringify(command_array));
if (!encrypted_content) {
throw new Error('Failed to encrypt command array');
}
// Create kind 23456 admin event
const adminEvent = {
kind: 23456,
pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [["p", getRelayPubkey()]],
content: encrypted_content
};
// Sign the event
const signedEvent = await window.nostr.signEvent(adminEvent);
if (!signedEvent || !signedEvent.sig) {
throw new Error('Event signing failed');
}
// Publish via SimplePool
const url = relayConnectionUrl.value.trim();
const publishPromises = relayPool.publish([url], signedEvent);
// Wait for publish results
const results = await Promise.allSettled(publishPromises);
let successCount = 0;
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
successCount++;
console.log(`✅ Relay event published successfully to relay ${index}`);
} else {
console.error(`❌ Relay event failed on relay ${index}:`, result.reason);
}
});
if (successCount === 0) {
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
throw new Error(`All relays rejected relay event. Details: ${errorDetails}`);
}
console.log(`Relay event command sent successfully for kind ${kind}`);
} catch (error) {
console.error(`Failed to send create_relay_event command for kind ${kind}:`, error);
throw error;
}
}
// Validation helpers
function isValidUrl(url) {
try {
new URL(url);
return true;
} catch {
return false;
}
}
function isValidRelayUrl(url) {
if (!isValidUrl(url)) return false;
return url.startsWith('ws://') || url.startsWith('wss://');
}
// UI helpers
function showStatus(elementId, message, type = 'info') {
const element = document.getElementById(elementId);
if (!element) return;
// Remove emojis from message
const cleanMessage = message.replace(/[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, '');
element.textContent = cleanMessage;
element.className = 'status-message';
element.style.display = 'block'; // Ensure it's visible
// Add type-specific styling
switch (type) {
case 'success':
element.style.color = 'var(--accent-color)';
break;
case 'error':
element.style.color = '#ff0000';
break;
case 'info':
default:
element.style.color = 'var(--primary-color)';
break;
}
// Auto-hide after 5 seconds
setTimeout(() => {
element.style.display = 'none';
}, 5000);
}
function addRelayEntry(url = '', read = true, write = true) {
const container = document.getElementById('kind10002-relay-entries');
if (!container) return;
const entryDiv = document.createElement('div');
entryDiv.className = 'relay-entry';
entryDiv.innerHTML = `
<div class="form-group" style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
<input type="url" class="relay-url" placeholder="wss://relay.example.com" value="${url}" style="flex: 1; min-width: 300px; pointer-events: auto; cursor: text;">
<label style="display: flex; align-items: center; gap: 5px; white-space: nowrap;">
<input type="checkbox" class="relay-read" ${read ? 'checked' : ''}>
Read
</label>
<label style="display: flex; align-items: center; gap: 5px; white-space: nowrap;">
<input type="checkbox" class="relay-write" ${write ? 'checked' : ''}>
Write
</label>
<button type="button" onclick="removeRelayEntry(this)" style="padding: 4px 8px; font-size: 12px; white-space: nowrap;">Remove</button>
</div>
`;
container.appendChild(entryDiv);
}
function removeRelayEntry(button) {
const entry = button.closest('.relay-entry');
if (entry) {
entry.remove();
}
}
// Initialize toggle button after DOM is ready
document.addEventListener('DOMContentLoaded', function() {
console.log('=== DOM CONTENT LOADED - INITIALIZING TOGGLE BUTTON ===');
@@ -5229,4 +5631,43 @@ document.addEventListener('DOMContentLoaded', function() {
console.log('=== SETTIMEOUT CALLBACK - CALLING initializeMonitoringToggleButton ===');
initializeMonitoringToggleButton();
}, 500); // Small delay to ensure DOM is fully ready
});
// Initialize relay events functionality
initializeRelayEvents();
});
// Initialize relay events functionality
function initializeRelayEvents() {
console.log('Initializing relay events functionality...');
// Set up event handlers for relay events page
const submitKind0Btn = document.getElementById('submit-kind0-btn');
const submitKind10050Btn = document.getElementById('submit-kind10050-btn');
const submitKind10002Btn = document.getElementById('submit-kind10002-btn');
const addRelayEntryBtn = document.getElementById('add-relay-entry-btn');
if (submitKind0Btn) {
submitKind0Btn.addEventListener('click', submitKind0Event);
}
if (submitKind10050Btn) {
submitKind10050Btn.addEventListener('click', submitKind10050Event);
}
if (submitKind10002Btn) {
submitKind10002Btn.addEventListener('click', submitKind10002Event);
}
if (addRelayEntryBtn) {
addRelayEntryBtn.addEventListener('click', () => addRelayEntry());
}
// Add one empty relay entry by default for Kind 10002
const kind10002Container = document.getElementById('kind10002-relay-entries');
if (kind10002Container && kind10002Container.children.length === 0) {
addRelayEntry(); // Add one empty entry to start
console.log('Added initial empty relay entry for Kind 10002');
}
console.log('Relay events functionality initialized');
}

24
debug.log Normal file
View File

@@ -0,0 +1,24 @@
=== NOSTR WebSocket Debug Log Started ===
[14:16:28.243] SEND localhost:8888: ["EVENT", {
"pubkey": "193279d1459ba1399aadb954422bf8595aa77367dccf482c682f5f208e435844",
"created_at": 1761499411,
"kind": 1059,
"tags": [["p", "4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"]],
"content": "AmWNi4P5J126kk69XH2o5mYvGj+69+Fjfr/nZx892I2z8edkwtp2IH7XAnPUqdGPu7x1xiZF1sNfr21VKThOhE54K/uQHLFydZN3acgUfX13sCeWhrvnQD0EvjvZC6QzW9DfFayYoYl+rEPYcra1/N68a+N1R7XnNcf1K/ZFh5Grcnln0H5YdXKRBhQI9aai4iFp1VGy2V0IR+6gDJGbJ7TbAbD3wgGWv1i77C03skH3RgzH+f2b7VBtm+vjKX6q7v6v8j3w1lRFE5Qh0Tqgedh3+UsnwqQta7OCzF9OyAVPK7EqKQBss5LzYSRUpcCE1vw5b7I7yeBFwU9WfnGLUW+uZxMJ2C3P4NBBrVO8UFIkBrPL2cqkoD5c8DgMLJjXGmc4EWfB4ZWb3KjbfLbgi6DVQ++cDjBbnCOPhX+/4qOnWq+gI28e/xk3cBvQtgUOkvWX3oGl3/Q33u4UGtxkFEXGfzdKHVDkR86kqf7RMZjIwTjLGpx4uov0cNmzj07hYEdoG/lJ4yA1v/GyF7viJdnnz3tE0hCZaViqSCev0rfUHWRDDMXJzJ9SS+OwpVswSG4NKvYsDhDM89BjhFs08HshTFdIh2AY45jR/16CsZM9JudH5BwqcX23wToYdZ+lrerOA0EkYb0DJUzGVe4lMpdJZoB8qXLHxMAKwKu0UEWEkeBnnZbvTGwCRbfGorxwPrnyqUCy9tzJx0GOLhRIzBmt6lki607VLDYjK97VIz0dff3fyWPAfy/yBlO2nHhVubUgpPaAjcaYNkO/iZwuP8oJkClWWmKwAQoNoxt+Ly2llrkz+Ne8oXMQdJSq416x6MLHo2JbKH8uwjx0yKG0oldLyWaz3A8OHYkJuxOi7HPVTlOOJrsjG4kMn2g97rVUXLs5v9F/StOjzxtiQWmCBtCsvK2LEEK/DzfavcJstEMxQztJjhiYRO3MJanL7lN2zu1ZHO149FJrgqGV6RQ8DDXf55yuabqHilBuUSDKpI0gl0+Efuor1my+L9J7MjJQ83aSwGizX7uXedMsGQRcvU3++Uvbw7sd2l67fb7IoYU04TPGZkIm120qwf7GAUpnDL7Lhulu/9LFMFs3UnGl9cLzY6EAJtDANHjMAoXbGbYclnoSiNW4yr3X9PBHO5o2YhIxfpTyEgLebJLOkzoziuCTpX8/MdhOhFtlIyo5B8Mbt5GDOHh4x1ZMKOl02J00Vvgui0hLw4Vri8Lz/ErPIRSlrEOB+8K5zPzJy/bD8XrOKlOwSbF5j9dsqs+8uCTC/v9YNQ0cC9wP7gVAxErQ3suJVeV7pzY+eGR051AcW7ppTs1gShhxDDaSaKdMlrkBdFDZcCJ+tomSgW56bOi45erpmk8Lcv4RrBzjBtq1hz+XSaTBAtEnGtHNH2uOn7KP/NNaD38dYkpb3N1VR3zuV67RcuPZeB+5WR9jhnLoSMGox2s=",
"id": "c6c18d902744fc0aaa4ca9172b3bcd0dde3fd7d943b41b2a39a16927ede67804",
"sig": "d67e0e914aa361c528510efd216548b6734a5fa68c46426571fbc87626bf19a9ec46e16883e7fad700f4fee5cfffd9bba03c3c08e57938fbca77a28b30a32bb7"
}]
[14:16:28.256] RECV localhost:8888: ["OK", "c6c18d902744fc0aaa4ca9172b3bcd0dde3fd7d943b41b2a39a16927ede67804", true, ""]
=== NOSTR WebSocket Debug Log Started ===
[15:01:18.592] SEND localhost:8888: ["EVENT", {
"pubkey": "ec9578ade9e74358ed35d8091d41bfa277e86d649614a8865e3725e38ebe5bc9",
"created_at": 1761502101,
"kind": 1059,
"tags": [["p", "4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"]],
"content": "AlgLnVwti8Dk2nu0e4bMrXeZiR/u+RPnA85kpts2svaFGfMByS4iap7xqdiSrXpSQPjQsix6jP9Qiy1a6rrvC6MutqTi3JfsMexLR61/ZKTK41sWTXNDTT3keH543vx3fVQH1mq+LgG4mjNzkPe0RqkYFvC8R0nxyAcCecHDxZUlmXQmAGiB5JB2GvstA4eoZLP1OI3fcLA3qaITLNRJOwRUoTYKqUkENHwz74CW0TnYDrKRZVe9zKNWQBLmtsgVoGd5CXNAVgXwmm2h0eCNIcRGnFqDHzpegpEGO+A7tvB0KJwlj4j/GmRgmnWO4pkrM2fmsTdlb5KNqe7NPuTVgYfdvld70zWpenp7jF/0psaEQEl8R7FbG2rNCv8fXtH+womvJQj4S0eUBxfvsUU1wWYmhusEzvyTfpV/nw0Er+pmAUZ2eGk7LEB2GMsJrkT+G5oohm0n+5c72iWJqW9A1eAzjR6Z21FkH4kAEJOl70fw9Xeig+s9rYk3GcKlMvj42zf7DepMXHPy62TbqUeclcm5W/semyasGP521GBuw152IN+dS67OVVmEvEJ89xhwiTeIty78enR4Gq7d1eNK+rqStdtJ7FN6kD/8gv4sFojUXyi0sIxzaSPrwI3ohOqbpEK1dTs6fmTUiyT/Buq++IhD9UwsZgz/kYpZfm1NVnWx+yTEv4I1H80FDxmMzbYnTHuIdRJFeh/NJRy9h+gXoZlZnteHkbwm1w2AejTkVnGs2Pz7aUZgC+1Za8fhtXq3Z9N3R8f8gtVFnnjRzApg5U89QrXUBS0R6F9dTqINk4qti94JWO4dYcPuudCutME5hfCYBoo+LHuRmdPKry7vSK1WgLYQsHuG+r313Ak8DhZYNbL+0d5UJ9kDFlFKaP3xLahSbEc/7u+AyuN68IyM1NwEehllxqVUsX8dsD4bZ2yW5rVjAQm9tT8Ypm73kJEb+DYVqT0WjFx88ee+HX89a9NgszWf1HE1KNQ9gWjn4eH6xbwrOkS4/v2O2tQoAd00vyPKAWly3Zlrz2cRnrSnxTZ5Lt7HtwAt6Err8MhD/w5rMLXHTBCMrroG1VfMo1OgL1YPafKDZmwVcHWacqtZiB0heRx742WipmTonqMjCOTNufdwxQcRPLLio0mtqiIrzgJqqIQenBXSa1jaG6Lvb5PCUKThbg4sSFfgssoUNKM7ytmBAe+PPmOVe11/gGaFWoQeUordbvmiCtzIPUYiKsuhfeK4I3jKvEofQU37hOam8ZxUczXvX6dgOOto002EWyCVfAzFgyey6wI+FGEbhXqlw7nB+azpqLQMJnHg7pfb1stXk3d8rjgVrRsVRJe/5KrXyZ5cd7ftJuJLxpTYmfFu6CKoUE0L5eRxXiwa16Pi0BehxOLaZteiTzttyfj+ClMKs2J/2/T1BVya1oGUW2Wg6ri/qS8oXv8bqiXBZ1/BwfI=",
"id": "ea5bb419a8efea8ee86bb8696406a70a0387a7d0ac6e60760026d1aea28b427f",
"sig": "0ffde3fd0d83c80693aa656668f2553807f8d474738ff3d9676090a5b8748a8e8e0c75a1d64963e4604046e18a806c4371a9cf2af2fd72f9db50f15bc78a4e25"
}]
[15:01:18.604] RECV localhost:8888: ["OK", "ea5bb419a8efea8ee86bb8696406a70a0387a7d0ac6e60760026d1aea28b427f", true, ""]

View File

@@ -0,0 +1,209 @@
# Subscription Matching Debug Plan
## Problem
The relay is not matching kind 1059 (NIP-17 gift wrap) events to subscriptions, even though a subscription exists with `kinds:[1059]` filter. The log shows:
```
Event broadcast complete: 0 subscriptions matched
```
But we have this subscription:
```
sub:3 146.70.187.119 0x78edc9b43210 8m 27s kinds:[1059], since:10/23/2025, 4:27:59 PM, limit:50
```
## Investigation Strategy
### 1. Add Debug Output to `event_matches_filter()` (lines 386-564)
Add debug logging at each filter check to trace the matching logic:
- **Entry point**: Log the event kind and filter being tested
- **Kinds filter check** (lines 392-415): Log whether kinds filter exists, the event kind value, and each filter kind being compared
- **Authors filter check** (lines 417-442): Log if authors filter exists and matching results
- **IDs filter check** (lines 444-469): Log if IDs filter exists and matching results
- **Since filter check** (lines 471-482): Log the event timestamp vs filter since value
- **Until filter check** (lines 484-495): Log the event timestamp vs filter until value
- **Tag filters check** (lines 497-561): Log tag filter matching details
- **Exit point**: Log whether the overall filter matched
### 2. Add Debug Output to `event_matches_subscription()` (lines 567-581)
Add logging to show:
- How many filters are in the subscription
- Which filter (if any) matched
- Overall subscription match result
### 3. Add Debug Output to `broadcast_event_to_subscriptions()` (lines 584-726)
Add logging to show:
- The event being broadcast (kind, id, created_at)
- Total number of active subscriptions being checked
- How many subscriptions matched after the first pass
### 4. Key Areas to Focus On
Based on the code analysis, the most likely issues are:
1. **Kind matching logic** (lines 392-415): The event kind might not be extracted correctly, or the comparison might be failing
2. **Since timestamp** (lines 471-482): The subscription has a `since` filter - if the event timestamp is before this, it won't match
3. **Event structure**: The event JSON might not have the expected structure
### 5. Specific Debug Additions
#### In `event_matches_filter()` at line 386:
```c
// Add at start of function
cJSON* event_kind_obj = cJSON_GetObjectItem(event, "kind");
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
cJSON* event_created_at_obj = cJSON_GetObjectItem(event, "created_at");
DEBUG_TRACE("FILTER_MATCH: Testing event kind=%d id=%.8s created_at=%ld",
event_kind_obj ? (int)cJSON_GetNumberValue(event_kind_obj) : -1,
event_id_obj && cJSON_IsString(event_id_obj) ? cJSON_GetStringValue(event_id_obj) : "null",
event_created_at_obj ? (long)cJSON_GetNumberValue(event_created_at_obj) : 0);
```
#### In kinds filter check (after line 392):
```c
if (filter->kinds && cJSON_IsArray(filter->kinds)) {
DEBUG_TRACE("FILTER_MATCH: Checking kinds filter with %d kinds", cJSON_GetArraySize(filter->kinds));
cJSON* event_kind = cJSON_GetObjectItem(event, "kind");
if (!event_kind || !cJSON_IsNumber(event_kind)) {
DEBUG_WARN("FILTER_MATCH: Event has no valid kind field");
return 0;
}
int event_kind_val = (int)cJSON_GetNumberValue(event_kind);
DEBUG_TRACE("FILTER_MATCH: Event kind=%d", event_kind_val);
int kind_match = 0;
cJSON* kind_item = NULL;
cJSON_ArrayForEach(kind_item, filter->kinds) {
if (cJSON_IsNumber(kind_item)) {
int filter_kind = (int)cJSON_GetNumberValue(kind_item);
DEBUG_TRACE("FILTER_MATCH: Comparing event kind %d with filter kind %d", event_kind_val, filter_kind);
if (filter_kind == event_kind_val) {
kind_match = 1;
DEBUG_TRACE("FILTER_MATCH: Kind matched!");
break;
}
}
}
if (!kind_match) {
DEBUG_TRACE("FILTER_MATCH: No kind match, filter rejected");
return 0;
}
DEBUG_TRACE("FILTER_MATCH: Kinds filter passed");
}
```
#### In since filter check (after line 472):
```c
if (filter->since > 0) {
cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
if (!event_created_at || !cJSON_IsNumber(event_created_at)) {
DEBUG_WARN("FILTER_MATCH: Event has no valid created_at field");
return 0;
}
long event_timestamp = (long)cJSON_GetNumberValue(event_created_at);
DEBUG_TRACE("FILTER_MATCH: Checking since filter: event_ts=%ld filter_since=%ld",
event_timestamp, filter->since);
if (event_timestamp < filter->since) {
DEBUG_TRACE("FILTER_MATCH: Event too old (before since), filter rejected");
return 0;
}
DEBUG_TRACE("FILTER_MATCH: Since filter passed");
}
```
#### At end of `event_matches_filter()` (before line 563):
```c
DEBUG_TRACE("FILTER_MATCH: All filters passed, event matches!");
return 1; // All filters passed
```
#### In `event_matches_subscription()` at line 567:
```c
int event_matches_subscription(cJSON* event, subscription_t* subscription) {
if (!event || !subscription || !subscription->filters) {
return 0;
}
DEBUG_TRACE("SUB_MATCH: Testing subscription '%s'", subscription->id);
int filter_num = 0;
subscription_filter_t* filter = subscription->filters;
while (filter) {
filter_num++;
DEBUG_TRACE("SUB_MATCH: Testing filter #%d", filter_num);
if (event_matches_filter(event, filter)) {
DEBUG_TRACE("SUB_MATCH: Filter #%d matched! Subscription '%s' matches",
filter_num, subscription->id);
return 1; // Match found (OR logic)
}
filter = filter->next;
}
DEBUG_TRACE("SUB_MATCH: No filters matched for subscription '%s'", subscription->id);
return 0; // No filters matched
}
```
#### In `broadcast_event_to_subscriptions()` at line 584:
```c
int broadcast_event_to_subscriptions(cJSON* event) {
if (!event) {
return 0;
}
// Log event details
cJSON* event_kind = cJSON_GetObjectItem(event, "kind");
cJSON* event_id = cJSON_GetObjectItem(event, "id");
cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
DEBUG_TRACE("BROADCAST: Event kind=%d id=%.8s created_at=%ld",
event_kind ? (int)cJSON_GetNumberValue(event_kind) : -1,
event_id && cJSON_IsString(event_id) ? cJSON_GetStringValue(event_id) : "null",
event_created_at ? (long)cJSON_GetNumberValue(event_created_at) : 0);
// ... existing expiration check code ...
// After line 611 (before pthread_mutex_lock):
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
int total_subs = 0;
subscription_t* count_sub = g_subscription_manager.active_subscriptions;
while (count_sub) {
total_subs++;
count_sub = count_sub->next;
}
DEBUG_TRACE("BROADCAST: Checking %d active subscriptions", total_subs);
subscription_t* sub = g_subscription_manager.active_subscriptions;
// ... rest of matching logic ...
```
## Expected Outcome
With these debug additions, we should see output like:
```
BROADCAST: Event kind=1059 id=abc12345 created_at=1729712279
BROADCAST: Checking 1 active subscriptions
SUB_MATCH: Testing subscription 'sub:3'
SUB_MATCH: Testing filter #1
FILTER_MATCH: Testing event kind=1059 id=abc12345 created_at=1729712279
FILTER_MATCH: Checking kinds filter with 1 kinds
FILTER_MATCH: Event kind=1059
FILTER_MATCH: Comparing event kind 1059 with filter kind 1059
FILTER_MATCH: Kind matched!
FILTER_MATCH: Kinds filter passed
FILTER_MATCH: Checking since filter: event_ts=1729712279 filter_since=1729708079
FILTER_MATCH: Since filter passed
FILTER_MATCH: All filters passed, event matches!
SUB_MATCH: Filter #1 matched! Subscription 'sub:3' matches
Event broadcast complete: 1 subscriptions matched
```
This will help us identify exactly where the matching is failing.

View File

@@ -1 +1 @@
1018941
2352885

423
src/api.c
View File

@@ -13,6 +13,7 @@
#include <sys/stat.h>
#include <unistd.h>
#include <strings.h>
#include <stdbool.h>
#include "api.h"
#include "embedded_web_content.h"
#include "config.h"
@@ -222,79 +223,6 @@ cJSON* query_top_pubkeys(void) {
return top_pubkeys;
}
// Query active subscriptions summary from database
cJSON* query_active_subscriptions(void) {
extern sqlite3* g_db;
if (!g_db) {
DEBUG_ERROR("Database not available for active subscriptions query");
return NULL;
}
// Get configuration limits
int max_subs = g_subscription_manager.max_total_subscriptions;
int max_per_client = g_subscription_manager.max_subscriptions_per_client;
// Query total active subscriptions from database
sqlite3_stmt* stmt;
const char* sql =
"SELECT COUNT(*) as total_subs, "
"COUNT(DISTINCT client_ip) as client_count "
"FROM subscriptions "
"WHERE event_type = 'created' AND ended_at IS NULL";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
DEBUG_ERROR("Failed to prepare active subscriptions query");
return NULL;
}
int total_subs = 0;
int client_count = 0;
if (sqlite3_step(stmt) == SQLITE_ROW) {
total_subs = sqlite3_column_int(stmt, 0);
client_count = sqlite3_column_int(stmt, 1);
}
sqlite3_finalize(stmt);
// Query max subscriptions per client
int most_subs_per_client = 0;
const char* max_sql =
"SELECT MAX(sub_count) FROM ("
" SELECT COUNT(*) as sub_count "
" FROM subscriptions "
" WHERE event_type = 'created' AND ended_at IS NULL "
" GROUP BY client_ip"
")";
if (sqlite3_prepare_v2(g_db, max_sql, -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
most_subs_per_client = sqlite3_column_int(stmt, 0);
}
sqlite3_finalize(stmt);
}
// Calculate statistics
double utilization_percentage = max_subs > 0 ? (total_subs * 100.0 / max_subs) : 0.0;
double avg_subs_per_client = client_count > 0 ? (total_subs * 1.0 / client_count) : 0.0;
// Build JSON response matching the design spec
cJSON* subscriptions = cJSON_CreateObject();
cJSON_AddStringToObject(subscriptions, "data_type", "active_subscriptions");
cJSON_AddNumberToObject(subscriptions, "timestamp", (double)time(NULL));
cJSON* data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "total_subscriptions", total_subs);
cJSON_AddNumberToObject(data, "max_subscriptions", max_subs);
cJSON_AddNumberToObject(data, "utilization_percentage", utilization_percentage);
cJSON_AddNumberToObject(data, "subscriptions_per_client_avg", avg_subs_per_client);
cJSON_AddNumberToObject(data, "most_subscriptions_per_client", most_subs_per_client);
cJSON_AddNumberToObject(data, "max_subscriptions_per_client", max_per_client);
cJSON_AddNumberToObject(data, "active_clients", client_count);
cJSON_AddItemToObject(subscriptions, "data", data);
return subscriptions;
}
// Query detailed subscription information from database log (ADMIN ONLY)
// Uses subscriptions table instead of in-memory iteration to avoid mutex contention
@@ -305,16 +233,18 @@ cJSON* query_subscription_details(void) {
return NULL;
}
// Query active subscriptions directly from subscriptions table
// Get subscriptions that were created but not yet closed/expired/disconnected
// Query active subscriptions from the active_subscriptions_log view
// This view properly handles deduplication of closed/expired subscriptions
sqlite3_stmt* stmt;
const char* sql =
"SELECT subscription_id, client_ip, wsi_pointer, filter_json, events_sent, "
"created_at, (strftime('%s', 'now') - created_at) as duration_seconds "
"FROM subscriptions "
"WHERE event_type = 'created' AND ended_at IS NULL "
"SELECT * "
"FROM active_subscriptions_log "
"ORDER BY created_at DESC LIMIT 100";
// DEBUG: Log the query results for debugging subscription_details
DEBUG_LOG("=== SUBSCRIPTION_DETAILS QUERY DEBUG ===");
DEBUG_LOG("Query: %s", sql);
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
DEBUG_ERROR("Failed to prepare subscription details query");
return NULL;
@@ -329,22 +259,27 @@ cJSON* query_subscription_details(void) {
cJSON* subscriptions_array = cJSON_CreateArray();
// Iterate through query results
int row_count = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
row_count++;
cJSON* sub_obj = cJSON_CreateObject();
// Extract subscription data from database
const char* sub_id = (const char*)sqlite3_column_text(stmt, 0);
const char* client_ip = (const char*)sqlite3_column_text(stmt, 1);
const char* wsi_pointer = (const char*)sqlite3_column_text(stmt, 2);
const char* filter_json = (const char*)sqlite3_column_text(stmt, 3);
long long events_sent = sqlite3_column_int64(stmt, 4);
long long created_at = sqlite3_column_int64(stmt, 5);
long long duration_seconds = sqlite3_column_int64(stmt, 6);
const char* filter_json = (const char*)sqlite3_column_text(stmt, 2);
long long events_sent = sqlite3_column_int64(stmt, 3);
long long created_at = sqlite3_column_int64(stmt, 4);
long long duration_seconds = sqlite3_column_int64(stmt, 5);
// DEBUG: Log each subscription found
DEBUG_LOG("Row %d: sub_id=%s, client_ip=%s, events_sent=%lld, created_at=%lld",
row_count, sub_id ? sub_id : "NULL", client_ip ? client_ip : "NULL",
events_sent, created_at);
// Add basic subscription info
cJSON_AddStringToObject(sub_obj, "id", sub_id ? sub_id : "");
cJSON_AddStringToObject(sub_obj, "client_ip", client_ip ? client_ip : "");
cJSON_AddStringToObject(sub_obj, "wsi_pointer", wsi_pointer ? wsi_pointer : "");
cJSON_AddNumberToObject(sub_obj, "created_at", (double)created_at);
cJSON_AddNumberToObject(sub_obj, "duration_seconds", (double)duration_seconds);
cJSON_AddNumberToObject(sub_obj, "events_sent", events_sent);
@@ -374,6 +309,10 @@ cJSON* query_subscription_details(void) {
cJSON_AddItemToObject(subscriptions_data, "data", data);
// DEBUG: Log final summary
DEBUG_LOG("Total subscriptions found: %d", cJSON_GetArraySize(subscriptions_array));
DEBUG_LOG("=== END SUBSCRIPTION_DETAILS QUERY DEBUG ===");
return subscriptions_data;
}
@@ -397,11 +336,6 @@ int generate_event_driven_monitoring(void) {
return -1;
}
// Generate active_subscriptions monitoring event
if (generate_monitoring_event_for_type("active_subscriptions", query_active_subscriptions) != 0) {
DEBUG_ERROR("Failed to generate active_subscriptions monitoring event");
return -1;
}
// Generate CPU metrics monitoring event (also triggered by event storage)
if (generate_monitoring_event_for_type("cpu_metrics", query_cpu_metrics) != 0) {
@@ -409,17 +343,11 @@ int generate_event_driven_monitoring(void) {
return -1;
}
DEBUG_INFO("Generated and broadcast event-driven monitoring events");
return 0;
}
// Generate subscription-driven monitoring events (triggered by subscription changes)
int generate_subscription_driven_monitoring(void) {
// Generate active_subscriptions monitoring event (subscription changes affect this)
if (generate_monitoring_event_for_type("active_subscriptions", query_active_subscriptions) != 0) {
DEBUG_ERROR("Failed to generate active_subscriptions monitoring event");
return -1;
}
// Generate subscription_details monitoring event (admin-only)
if (generate_monitoring_event_for_type("subscription_details", query_subscription_details) != 0) {
@@ -433,7 +361,6 @@ int generate_subscription_driven_monitoring(void) {
return -1;
}
DEBUG_INFO("Generated and broadcast subscription-driven monitoring events");
return 0;
}
@@ -799,7 +726,7 @@ int send_admin_response(const char* sender_pubkey, const char* response_content,
}
// Encrypt response content using NIP-44
char encrypted_content[16384]; // Buffer for encrypted content (increased size)
char encrypted_content[131072]; // Buffer for encrypted content (128KB to handle large SQL responses)
int encrypt_result = nostr_nip44_encrypt(
relay_privkey, // sender private key (bytes)
sender_pubkey_bytes, // recipient public key (bytes)
@@ -2304,6 +2231,306 @@ int process_config_change_request(const char* admin_pubkey, const char* message)
return 1; // Confirmation sent
}
// Forward declarations for relay event creation functions
cJSON* create_relay_metadata_event(cJSON* metadata);
cJSON* create_relay_dm_list_event(cJSON* dm_relays);
cJSON* create_relay_list_event(cJSON* relays);
// Handle create_relay_event admin commands
int handle_create_relay_event_command(cJSON* event, int kind, cJSON* event_data, char* error_message, size_t error_size, struct lws* wsi) {
if (!event || !event_data || !error_message) {
return -1;
}
// Get request event ID for response correlation
cJSON* request_id_obj = cJSON_GetObjectItem(event, "id");
if (!request_id_obj || !cJSON_IsString(request_id_obj)) {
snprintf(error_message, error_size, "Missing request event ID");
return -1;
}
const char* request_id = cJSON_GetStringValue(request_id_obj);
// Get sender pubkey for response
cJSON* sender_pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
if (!sender_pubkey_obj || !cJSON_IsString(sender_pubkey_obj)) {
snprintf(error_message, error_size, "Missing sender pubkey");
return -1;
}
const char* sender_pubkey = cJSON_GetStringValue(sender_pubkey_obj);
// Create the relay event based on kind
cJSON* relay_event = NULL;
switch (kind) {
case 0: // User metadata
relay_event = create_relay_metadata_event(event_data);
break;
case 10050: // DM relay list
relay_event = create_relay_dm_list_event(event_data);
break;
case 10002: // Relay list
relay_event = create_relay_list_event(event_data);
break;
default: {
char response_content[256];
snprintf(response_content, sizeof(response_content),
"❌ Unsupported event kind: %d\n\nSupported kinds: 0 (metadata), 10050 (DM relays), 10002 (relays)",
kind);
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
}
}
if (!relay_event) {
char response_content[128];
snprintf(response_content, sizeof(response_content),
"❌ Failed to create relay event (kind %d)\n\nCheck relay logs for details.", kind);
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
}
// Store the event in database
int store_result = store_event(relay_event);
if (store_result != 0) {
cJSON_Delete(relay_event);
char response_content[128];
snprintf(response_content, sizeof(response_content),
"❌ Failed to store relay event (kind %d) in database", kind);
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
}
// Broadcast the event to connected clients
broadcast_event_to_subscriptions(relay_event);
// Clean up
cJSON_Delete(relay_event);
// Send success response (plain text like other admin commands)
char response_content[256];
const char* kind_name = (kind == 0) ? "metadata" : (kind == 10050) ? "DM relay list" : "relay list";
snprintf(response_content, sizeof(response_content),
"✅ Relay event created successfully\n\nKind: %d (%s)\n\nEvent has been stored and broadcast to subscribers.",
kind, kind_name);
return send_admin_response(sender_pubkey, response_content, request_id, error_message, error_size, wsi);
}
// Create a relay metadata event (kind 0)
cJSON* create_relay_metadata_event(cJSON* metadata) {
if (!metadata || !cJSON_IsObject(metadata)) {
DEBUG_ERROR("Invalid metadata object for kind 0 event");
return NULL;
}
// Get relay keys
const char* relay_pubkey = get_config_value("relay_pubkey");
char* relay_privkey_hex = get_relay_private_key();
if (!relay_pubkey || !relay_privkey_hex) {
DEBUG_ERROR("Could not get relay keys for metadata event");
if (relay_privkey_hex) free(relay_privkey_hex);
return NULL;
}
// 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);
DEBUG_ERROR("Failed to convert relay private key for metadata event");
return NULL;
}
free(relay_privkey_hex);
// Create metadata content
char* content = cJSON_Print(metadata);
if (!content) {
DEBUG_ERROR("Failed to serialize metadata for kind 0 event");
return NULL;
}
// Create and sign the event
cJSON* signed_event = nostr_create_and_sign_event(
0, // kind (metadata)
content, // content
NULL, // tags (none for kind 0)
relay_privkey, // private key
(time_t)time(NULL) // timestamp
);
free(content);
if (!signed_event) {
DEBUG_ERROR("Failed to create and sign metadata event");
return NULL;
}
DEBUG_LOG("Created relay metadata event (kind 0)");
return signed_event;
}
// Create a relay DM list event (kind 10050)
cJSON* create_relay_dm_list_event(cJSON* dm_relays) {
if (!dm_relays || !cJSON_IsObject(dm_relays)) {
DEBUG_ERROR("Invalid DM relays object for kind 10050 event");
return NULL;
}
// Get relay keys
const char* relay_pubkey = get_config_value("relay_pubkey");
char* relay_privkey_hex = get_relay_private_key();
if (!relay_pubkey || !relay_privkey_hex) {
DEBUG_ERROR("Could not get relay keys for DM list event");
if (relay_privkey_hex) free(relay_privkey_hex);
return NULL;
}
// 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);
DEBUG_ERROR("Failed to convert relay private key for DM list event");
return NULL;
}
free(relay_privkey_hex);
// Create empty content for kind 10050
const char* content = "";
// Create tags from relay list
cJSON* tags = cJSON_CreateArray();
if (!tags) {
DEBUG_ERROR("Failed to create tags array for DM list event");
return NULL;
}
// Extract relays array
cJSON* relays_array = cJSON_GetObjectItem(dm_relays, "relays");
if (relays_array && cJSON_IsArray(relays_array)) {
cJSON* relay_item = NULL;
cJSON_ArrayForEach(relay_item, relays_array) {
if (cJSON_IsString(relay_item)) {
const char* relay_url = cJSON_GetStringValue(relay_item);
if (relay_url && strlen(relay_url) > 0) {
cJSON* tag = cJSON_CreateArray();
cJSON_AddItemToArray(tag, cJSON_CreateString("relay"));
cJSON_AddItemToArray(tag, cJSON_CreateString(relay_url));
cJSON_AddItemToArray(tags, tag);
}
}
}
}
// Create and sign the event
cJSON* signed_event = nostr_create_and_sign_event(
10050, // kind (DM relay list)
content, // content (empty)
tags, // tags
relay_privkey, // private key
(time_t)time(NULL) // timestamp
);
cJSON_Delete(tags);
if (!signed_event) {
DEBUG_ERROR("Failed to create and sign DM list event");
return NULL;
}
DEBUG_LOG("Created relay DM list event (kind 10050)");
return signed_event;
}
// Create a relay list event (kind 10002)
cJSON* create_relay_list_event(cJSON* relays) {
if (!relays || !cJSON_IsObject(relays)) {
DEBUG_ERROR("Invalid relays object for kind 10002 event");
return NULL;
}
// Get relay keys
const char* relay_pubkey = get_config_value("relay_pubkey");
char* relay_privkey_hex = get_relay_private_key();
if (!relay_pubkey || !relay_privkey_hex) {
DEBUG_ERROR("Could not get relay keys for relay list event");
if (relay_privkey_hex) free(relay_privkey_hex);
return NULL;
}
// 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);
DEBUG_ERROR("Failed to convert relay private key for relay list event");
return NULL;
}
free(relay_privkey_hex);
// Create empty content for kind 10002
const char* content = "";
// Create tags from relay list
cJSON* tags = cJSON_CreateArray();
if (!tags) {
DEBUG_ERROR("Failed to create tags array for relay list event");
return NULL;
}
// Extract relays array
cJSON* relays_array = cJSON_GetObjectItem(relays, "relays");
if (relays_array && cJSON_IsArray(relays_array)) {
cJSON* relay_item = NULL;
cJSON_ArrayForEach(relay_item, relays_array) {
if (cJSON_IsObject(relay_item)) {
cJSON* url = cJSON_GetObjectItem(relay_item, "url");
cJSON* read = cJSON_GetObjectItem(relay_item, "read");
cJSON* write = cJSON_GetObjectItem(relay_item, "write");
if (url && cJSON_IsString(url)) {
const char* relay_url = cJSON_GetStringValue(url);
int read_flag = read && cJSON_IsBool(read) ? cJSON_IsTrue(read) : true;
int write_flag = write && cJSON_IsBool(write) ? cJSON_IsTrue(write) : true;
// Create marker string
const char* marker = NULL;
if (read_flag && write_flag) {
marker = ""; // No marker means both read and write
} else if (read_flag) {
marker = "read";
} else if (write_flag) {
marker = "write";
} else {
// Skip invalid entries
continue;
}
cJSON* tag = cJSON_CreateArray();
cJSON_AddItemToArray(tag, cJSON_CreateString("r"));
cJSON_AddItemToArray(tag, cJSON_CreateString(relay_url));
if (marker[0] != '\0') {
cJSON_AddItemToArray(tag, cJSON_CreateString(marker));
}
cJSON_AddItemToArray(tags, tag);
}
}
}
}
// Create and sign the event
cJSON* signed_event = nostr_create_and_sign_event(
10002, // kind (relay list)
content, // content (empty)
tags, // tags
relay_privkey, // private key
(time_t)time(NULL) // timestamp
);
cJSON_Delete(tags);
if (!signed_event) {
DEBUG_ERROR("Failed to create and sign relay list event");
return NULL;
}
DEBUG_LOG("Created relay list event (kind 10002)");
return signed_event;
}
// Handle monitoring system admin commands
int handle_monitoring_command(cJSON* event, const char* command, char* error_message, size_t error_size, struct lws* wsi) {
if (!event || !command || !error_message) {

View File

@@ -85,6 +85,7 @@ int migrate_config_from_events_to_table(void);
int populate_config_table_from_event(const cJSON* event);
int handle_config_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi);
int handle_config_set_unified(cJSON* event, const char* config_key, const char* config_value, char* error_message, size_t error_size, struct lws* wsi);
int handle_create_relay_event_unified(cJSON* event, const char* kind_str, const char* event_data_json, char* error_message, size_t error_size, struct lws* wsi);
// Forward declarations for tag parsing utilities
const char* get_first_tag_name(cJSON* event);
@@ -92,6 +93,7 @@ const char* get_tag_value(cJSON* event, const char* tag_name, int value_index);
int parse_auth_query_parameters(cJSON* event, char** query_type, char** pattern_value);
int handle_config_update_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
int handle_sql_query_unified(cJSON* event, const char* query, char* error_message, size_t error_size, struct lws* wsi);
// Current configuration cache
@@ -814,7 +816,7 @@ int first_time_startup_sequence(const cli_options_t* cli_options, char* admin_pu
return 0;
}
int startup_existing_relay(const char* relay_pubkey, const cli_options_t* cli_options) {
int startup_existing_relay(const char* relay_pubkey, const cli_options_t* cli_options __attribute__((unused))) {
if (!relay_pubkey) {
DEBUG_ERROR("Invalid relay pubkey for existing relay startup");
return -1;
@@ -2549,51 +2551,17 @@ int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_si
return -1;
}
// Check if decrypted content is a direct command array (DM control system)
cJSON* potential_command_array = cJSON_Parse(decrypted_text);
if (potential_command_array && cJSON_IsArray(potential_command_array)) {
// Route to DM admin system
int dm_result = process_dm_admin_command(potential_command_array, event, error_message, error_size, wsi);
cJSON_Delete(potential_command_array);
memset(decrypted_text, 0, sizeof(decrypted_text)); // Clear sensitive data
return dm_result;
}
// If not a direct command array, try parsing as inner event JSON (NIP-17)
cJSON* inner_event = potential_command_array; // Reuse the parsed JSON
if (!inner_event || !cJSON_IsObject(inner_event)) {
DEBUG_ERROR("error: decrypted content is not valid inner event JSON");
cJSON_Delete(inner_event);
snprintf(error_message, error_size, "error: decrypted content is not valid inner event JSON");
return -1;
}
// Extract content from inner event
cJSON* inner_content_obj = cJSON_GetObjectItem(inner_event, "content");
if (!inner_content_obj || !cJSON_IsString(inner_content_obj)) {
DEBUG_ERROR("error: 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);
// Parse inner content as JSON array (the command array)
decrypted_content = cJSON_Parse(inner_content);
// Parse decrypted content as command array directly (NOT as NIP-17 inner event)
// Kind 23456 events contain direct command arrays: ["command_name", arg1, arg2, ...]
decrypted_content = cJSON_Parse(decrypted_text);
if (!decrypted_content || !cJSON_IsArray(decrypted_content)) {
DEBUG_ERROR("error: inner content is not valid JSON array");
cJSON_Delete(inner_event);
snprintf(error_message, error_size, "error: inner content is not valid JSON array");
DEBUG_ERROR("error: decrypted content is not valid command array");
cJSON_Delete(decrypted_content);
snprintf(error_message, error_size, "error: decrypted content is not valid command array");
return -1;
}
// Clean up inner event
cJSON_Delete(inner_event);
// Replace event content with decrypted command array for processing
cJSON_DeleteItemFromObject(event, "content");
cJSON_AddStringToObject(event, "content", "decrypted");
@@ -2610,10 +2578,26 @@ int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_si
cJSON_AddItemToArray(command_tag, cJSON_Duplicate(first_item, 1));
// Add remaining items as tag values
// Convert non-string items (objects, arrays, numbers) to JSON strings
for (int i = 1; i < cJSON_GetArraySize(decrypted_content); i++) {
cJSON* item = cJSON_GetArrayItem(decrypted_content, i);
if (item) {
cJSON_AddItemToArray(command_tag, cJSON_Duplicate(item, 1));
if (cJSON_IsString(item)) {
// Keep strings as-is
cJSON_AddItemToArray(command_tag, cJSON_Duplicate(item, 1));
} else if (cJSON_IsNumber(item)) {
// Convert numbers to strings
char num_str[32];
snprintf(num_str, sizeof(num_str), "%.0f", cJSON_GetNumberValue(item));
cJSON_AddItemToArray(command_tag, cJSON_CreateString(num_str));
} else if (cJSON_IsObject(item) || cJSON_IsArray(item)) {
// Convert objects/arrays to JSON strings
char* json_str = cJSON_PrintUnformatted(item);
if (json_str) {
cJSON_AddItemToArray(command_tag, cJSON_CreateString(json_str));
free(json_str);
}
}
}
}
@@ -2690,6 +2674,25 @@ int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_si
else if (strcmp(action_type, "stats_query") == 0) {
return handle_stats_query_unified(event, error_message, error_size, wsi);
}
else if (strcmp(action_type, "create_relay_event") == 0) {
const char* kind_str = get_tag_value(event, action_type, 1);
const char* event_data_json = get_tag_value(event, action_type, 2);
if (!kind_str || !event_data_json) {
DEBUG_ERROR("invalid: missing kind or event data");
snprintf(error_message, error_size, "invalid: missing kind or event data");
return -1;
}
return handle_create_relay_event_unified(event, kind_str, event_data_json, error_message, error_size, wsi);
}
else if (strcmp(action_type, "sql_query") == 0) {
const char* query = get_tag_value(event, action_type, 1);
if (!query) {
DEBUG_ERROR("invalid: missing sql_query parameter");
snprintf(error_message, error_size, "invalid: missing sql_query parameter");
return -1;
}
return handle_sql_query_unified(event, query, error_message, error_size, wsi);
}
else if (strcmp(action_type, "whitelist") == 0 || strcmp(action_type, "blacklist") == 0) {
// Handle auth rule modifications (existing logic from process_admin_auth_event)
return handle_auth_rule_modification_unified(event, error_message, error_size, wsi);
@@ -3489,6 +3492,41 @@ int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_s
return -1;
}
// Unified create relay event handler
int handle_create_relay_event_unified(cJSON* event, const char* kind_str, const char* event_data_json, char* error_message, size_t error_size, struct lws* wsi) {
// Suppress unused parameter warning
(void)wsi;
if (!event || !kind_str || !event_data_json) {
snprintf(error_message, error_size, "invalid: missing parameters for create_relay_event");
return -1;
}
// Parse kind string to integer
char* endptr;
int kind = (int)strtol(kind_str, &endptr, 10);
if (endptr == kind_str || *endptr != '\0') {
snprintf(error_message, error_size, "invalid: kind must be a valid integer");
return -1;
}
// Parse event data JSON
cJSON* event_data = cJSON_Parse(event_data_json);
if (!event_data) {
snprintf(error_message, error_size, "invalid: event_data must be valid JSON");
return -1;
}
// Call the existing implementation from api.c
extern int handle_create_relay_event_command(cJSON* event, int kind, cJSON* event_data, char* error_message, size_t error_size, struct lws* wsi);
int result = handle_create_relay_event_command(event, kind, event_data, error_message, error_size, wsi);
// Clean up
cJSON_Delete(event_data);
return result;
}
// Unified config update handler - handles multiple config objects in single atomic command
int handle_config_update_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi) {
// Suppress unused parameter warning

View File

@@ -80,6 +80,7 @@ extern int handle_sql_query_unified(cJSON* event, const char* query, char* error
// Process direct command arrays (DM control system)
// This handles commands sent as direct JSON arrays, not wrapped in inner events
// Note: create_relay_event is NOT supported via DMs - use Kind 23456 events only
int process_dm_admin_command(cJSON* command_array, cJSON* event, char* error_message, size_t error_size, struct lws* wsi) {
if (!command_array || !cJSON_IsArray(command_array) || !event) {
DEBUG_ERROR("DM Admin: Invalid command array or event");
@@ -231,19 +232,27 @@ cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message,
return NULL;
}
DEBUG_INFO("DM_ADMIN: Received potential NIP-17 gift wrap event for processing");
// Step 1: Validate it's addressed to us
if (!is_nip17_gift_wrap_for_relay(gift_wrap_event)) {
DEBUG_INFO("DM_ADMIN: Event is not a valid gift wrap for this relay - rejecting");
strncpy(error_message, "NIP-17: Event is not a valid gift wrap for this relay", error_size - 1);
return NULL;
}
DEBUG_INFO("DM_ADMIN: Valid NIP-17 gift wrap confirmed for this relay");
// Step 2: Get relay private key for decryption
char* relay_privkey_hex = get_relay_private_key();
if (!relay_privkey_hex) {
DEBUG_INFO("DM_ADMIN: Could not get relay private key for decryption");
strncpy(error_message, "NIP-17: Could not get relay private key for decryption", error_size - 1);
return NULL;
}
DEBUG_INFO("DM_ADMIN: Retrieved relay private key for decryption");
// 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) {
@@ -254,10 +263,13 @@ cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message,
}
free(relay_privkey_hex);
DEBUG_INFO("DM_ADMIN: Converted relay private key to bytes successfully");
// Step 3: Decrypt and parse inner event using library function
DEBUG_INFO("DM_ADMIN: Attempting to decrypt NIP-17 gift wrap using nostr_nip17_receive_dm");
cJSON* inner_dm = nostr_nip17_receive_dm(gift_wrap_event, relay_privkey);
if (!inner_dm) {
DEBUG_ERROR("NIP-17: nostr_nip17_receive_dm returned NULL");
DEBUG_INFO("DM_ADMIN: nostr_nip17_receive_dm returned NULL - decryption failed");
// Debug: Print the gift wrap event
char* gift_wrap_debug = cJSON_Print(gift_wrap_event);
if (gift_wrap_debug) {
@@ -273,12 +285,17 @@ cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message,
}
privkey_hex[64] = '\0';
DEBUG_INFO("DM_ADMIN: NIP-17 decryption failed - returning error");
strncpy(error_message, "NIP-17: Failed to decrypt and parse inner DM event", error_size - 1);
return NULL;
}
DEBUG_INFO("DM_ADMIN: Successfully decrypted NIP-17 gift wrap, processing inner DM");
// Step 4: Process admin command
DEBUG_INFO("DM_ADMIN: Processing decrypted admin command");
int result = process_nip17_admin_command(inner_dm, error_message, error_size, wsi);
DEBUG_INFO("DM_ADMIN: Admin command processing completed with result: %d", result);
// Step 5: For plain text commands (stats/config), the response is already handled
// Only create a generic response for other command types that don't handle their own responses
@@ -457,18 +474,23 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
return -1;
}
DEBUG_INFO("DM_ADMIN: Processing NIP-17 admin command from decrypted DM");
// Extract content from DM
cJSON* content_obj = cJSON_GetObjectItem(dm_event, "content");
if (!content_obj || !cJSON_IsString(content_obj)) {
DEBUG_INFO("DM_ADMIN: DM missing content field");
strncpy(error_message, "NIP-17: DM missing content", error_size - 1);
return -1;
}
const char* dm_content = cJSON_GetStringValue(content_obj);
DEBUG_INFO("DM_ADMIN: Extracted DM content: %.100s%s", dm_content, strlen(dm_content) > 100 ? "..." : "");
// Check if sender is admin before processing any commands
cJSON* sender_pubkey_obj = cJSON_GetObjectItem(dm_event, "pubkey");
if (!sender_pubkey_obj || !cJSON_IsString(sender_pubkey_obj)) {
DEBUG_INFO("DM_ADMIN: DM missing sender pubkey - treating as user DM");
return 0; // Not an error, just treat as user DM
}
const char* sender_pubkey = cJSON_GetStringValue(sender_pubkey_obj);
@@ -477,11 +499,16 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
const char* admin_pubkey = get_config_value("admin_pubkey");
int is_admin = admin_pubkey && strlen(admin_pubkey) > 0 && strcmp(sender_pubkey, admin_pubkey) == 0;
DEBUG_INFO("DM_ADMIN: Sender pubkey: %.16s... (admin: %s)", sender_pubkey, is_admin ? "YES" : "NO");
// Parse DM content as JSON array of commands
DEBUG_INFO("DM_ADMIN: Attempting to parse DM content as JSON command array");
cJSON* command_array = cJSON_Parse(dm_content);
if (!command_array || !cJSON_IsArray(command_array)) {
DEBUG_INFO("DM_ADMIN: Content is not a JSON array, checking for plain text commands");
// If content is not a JSON array, check for plain text commands
if (is_admin) {
DEBUG_INFO("DM_ADMIN: Processing plain text admin command");
// Convert content to lowercase for case-insensitive matching
char content_lower[256];
size_t content_len = strlen(dm_content);
@@ -498,47 +525,55 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
// Check for stats commands
if (strstr(content_lower, "stats") != NULL || strstr(content_lower, "statistics") != NULL) {
DEBUG_INFO("DM_ADMIN: Processing stats command");
char* stats_text = generate_stats_text();
if (!stats_text) {
DEBUG_INFO("DM_ADMIN: Failed to generate stats text");
return -1;
}
char error_msg[256];
int result = send_nip17_response(sender_pubkey, stats_text, error_msg, sizeof(error_msg));
free(stats_text);
if (result != 0) {
DEBUG_ERROR(error_msg);
return -1;
}
DEBUG_INFO("DM_ADMIN: Stats command processed successfully");
return 0;
}
// Check for config commands
else if (strstr(content_lower, "config") != NULL || strstr(content_lower, "configuration") != NULL) {
DEBUG_INFO("DM_ADMIN: Processing config command");
char* config_text = generate_config_text();
if (!config_text) {
DEBUG_INFO("DM_ADMIN: Failed to generate config text");
return -1;
}
char error_msg[256];
int result = send_nip17_response(sender_pubkey, config_text, error_msg, sizeof(error_msg));
free(config_text);
if (result != 0) {
DEBUG_ERROR(error_msg);
return -1;
}
DEBUG_INFO("DM_ADMIN: Config command processed successfully");
return 0;
}
else {
DEBUG_INFO("DM_ADMIN: Checking for confirmation or config change requests");
// Check if it's a confirmation response (yes/no)
int confirmation_result = handle_config_confirmation(sender_pubkey, dm_content);
if (confirmation_result != 0) {
if (confirmation_result > 0) {
// Configuration confirmation processed successfully
DEBUG_INFO("DM_ADMIN: Configuration confirmation processed successfully");
} else if (confirmation_result == -2) {
DEBUG_INFO("DM_ADMIN: No pending changes to confirm");
// No pending changes
char no_pending_msg[256];
snprintf(no_pending_msg, sizeof(no_pending_msg),
@@ -558,6 +593,7 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
int config_result = process_config_change_request(sender_pubkey, dm_content);
if (config_result != 0) {
if (config_result > 0) {
DEBUG_INFO("DM_ADMIN: Configuration change request processed successfully");
return 1; // Return positive value to indicate response was handled
} else {
DEBUG_ERROR("NIP-17: Configuration change request failed");
@@ -565,22 +601,28 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
}
}
DEBUG_INFO("DM_ADMIN: Unrecognized plain text admin command");
return 0; // Admin sent unrecognized plain text, treat as user DM
}
} else {
DEBUG_INFO("DM_ADMIN: Non-admin user sent plain text - treating as user DM");
// Not admin, treat as user DM
return 0;
}
}
DEBUG_INFO("DM_ADMIN: Successfully parsed JSON command array");
// 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) {
DEBUG_INFO("DM_ADMIN: Processing JSON stats command");
// Get sender pubkey for response
cJSON* sender_pubkey_obj = cJSON_GetObjectItem(dm_event, "pubkey");
if (!sender_pubkey_obj || !cJSON_IsString(sender_pubkey_obj)) {
cJSON_Delete(command_array);
DEBUG_INFO("DM_ADMIN: DM missing sender pubkey for stats command");
strncpy(error_message, "NIP-17: DM missing sender pubkey", error_size - 1);
return -1;
}
@@ -590,6 +632,7 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
char* stats_json = generate_stats_json();
if (!stats_json) {
cJSON_Delete(command_array);
DEBUG_INFO("DM_ADMIN: Failed to generate stats JSON");
strncpy(error_message, "NIP-17: Failed to generate stats", error_size - 1);
return -1;
}
@@ -598,17 +641,19 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
int result = send_nip17_response(sender_pubkey, stats_json, error_msg, sizeof(error_msg));
free(stats_json);
cJSON_Delete(command_array);
if (result != 0) {
DEBUG_ERROR(error_msg);
strncpy(error_message, error_msg, error_size - 1);
return -1;
}
DEBUG_INFO("DM_ADMIN: JSON stats command processed successfully");
return 0;
}
}
DEBUG_INFO("DM_ADMIN: Delegating to unified admin processing for command array");
// For other commands, delegate to existing admin processing
// Create a synthetic kind 23456 event with the DM content
cJSON* synthetic_event = cJSON_CreateObject();
@@ -628,10 +673,12 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
}
// Process as regular admin event
DEBUG_INFO("DM_ADMIN: Processing synthetic admin event");
int result = process_admin_event_in_config(synthetic_event, error_message, error_size, wsi);
cJSON_Delete(synthetic_event);
cJSON_Delete(command_array);
DEBUG_INFO("DM_ADMIN: Unified admin processing completed with result: %d", result);
return result;
}

File diff suppressed because one or more lines are too long

View File

@@ -95,7 +95,6 @@ void update_subscription_manager_config(void);
void log_subscription_created(const subscription_t* sub);
void log_subscription_closed(const char* sub_id, const char* client_ip, const char* reason);
void log_subscription_disconnected(const char* client_ip);
void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip);
void update_subscription_events_sent(const char* sub_id, int events_sent);
// Forward declarations for NIP-01 event handling
@@ -148,6 +147,7 @@ int mark_event_as_deleted(const char* event_id, const char* deletion_event_id, c
// Forward declaration for database functions
int store_event(cJSON* event);
cJSON* retrieve_event(const char* event_id);
// Forward declaration for monitoring system
void monitoring_on_event_stored(void);
@@ -209,23 +209,21 @@ void signal_handler(int sig) {
// Send NOTICE message to client (NIP-01)
void send_notice_message(struct lws* wsi, const char* message) {
if (!wsi || !message) return;
cJSON* notice_msg = cJSON_CreateArray();
cJSON_AddItemToArray(notice_msg, cJSON_CreateString("NOTICE"));
cJSON_AddItemToArray(notice_msg, cJSON_CreateString(message));
char* msg_str = cJSON_Print(notice_msg);
if (msg_str) {
size_t msg_len = strlen(msg_str);
unsigned char* buf = malloc(LWS_PRE + msg_len);
if (buf) {
memcpy(buf + LWS_PRE, msg_str, msg_len);
lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
free(buf);
// Use proper message queue system instead of direct lws_write
if (queue_message(wsi, NULL, msg_str, msg_len, LWS_WRITE_TEXT) != 0) {
DEBUG_ERROR("Failed to queue NOTICE message");
}
free(msg_str);
}
cJSON_Delete(notice_msg);
}
@@ -590,93 +588,6 @@ const char* extract_d_tag_value(cJSON* tags) {
return NULL;
}
// Check and handle replaceable events according to NIP-01
int check_and_handle_replaceable_event(int kind, const char* pubkey, long created_at) {
if (!g_db || !pubkey) return 0;
const char* sql =
"SELECT created_at FROM events WHERE kind = ? AND pubkey = ? ORDER BY created_at DESC LIMIT 1";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
return 0; // Allow storage on DB error
}
sqlite3_bind_int(stmt, 1, kind);
sqlite3_bind_text(stmt, 2, pubkey, -1, SQLITE_STATIC);
int result = 0;
if (sqlite3_step(stmt) == SQLITE_ROW) {
long existing_created_at = sqlite3_column_int64(stmt, 0);
if (created_at <= existing_created_at) {
result = -1; // Older or same timestamp, reject
} else {
// Delete older versions
const char* delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at < ?";
sqlite3_stmt* delete_stmt;
if (sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL) == SQLITE_OK) {
sqlite3_bind_int(delete_stmt, 1, kind);
sqlite3_bind_text(delete_stmt, 2, pubkey, -1, SQLITE_STATIC);
sqlite3_bind_int64(delete_stmt, 3, created_at);
sqlite3_step(delete_stmt);
sqlite3_finalize(delete_stmt);
}
}
}
sqlite3_finalize(stmt);
return result;
}
// Check and handle addressable events according to NIP-01
int check_and_handle_addressable_event(int kind, const char* pubkey, const char* d_tag_value, long created_at) {
if (!g_db || !pubkey) return 0;
// If no d tag, treat as regular replaceable
if (!d_tag_value) {
return check_and_handle_replaceable_event(kind, pubkey, created_at);
}
const char* sql =
"SELECT created_at FROM events WHERE kind = ? AND pubkey = ? AND json_extract(tags, '$[*][1]') = ? "
"AND json_extract(tags, '$[*][0]') = 'd' ORDER BY created_at DESC LIMIT 1";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
return 0; // Allow storage on DB error
}
sqlite3_bind_int(stmt, 1, kind);
sqlite3_bind_text(stmt, 2, pubkey, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, d_tag_value, -1, SQLITE_STATIC);
int result = 0;
if (sqlite3_step(stmt) == SQLITE_ROW) {
long existing_created_at = sqlite3_column_int64(stmt, 0);
if (created_at <= existing_created_at) {
result = -1; // Older or same timestamp, reject
} else {
// Delete older versions with same kind, pubkey, and d tag
const char* delete_sql =
"DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at < ? "
"AND json_extract(tags, '$[*][1]') = ? AND json_extract(tags, '$[*][0]') = 'd'";
sqlite3_stmt* delete_stmt;
if (sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL) == SQLITE_OK) {
sqlite3_bind_int(delete_stmt, 1, kind);
sqlite3_bind_text(delete_stmt, 2, pubkey, -1, SQLITE_STATIC);
sqlite3_bind_int64(delete_stmt, 3, created_at);
sqlite3_bind_text(delete_stmt, 4, d_tag_value, -1, SQLITE_STATIC);
sqlite3_step(delete_stmt);
sqlite3_finalize(delete_stmt);
}
}
}
sqlite3_finalize(stmt);
return result;
}
// Store event in database
int store_event(cJSON* event) {
@@ -700,7 +611,14 @@ int store_event(cJSON* event) {
// Classify event type
event_type_t type = classify_event_kind((int)cJSON_GetNumberValue(kind));
// EPHEMERAL EVENTS (kinds 20000-29999) should NOT be stored
if (type == EVENT_TYPE_EPHEMERAL) {
DEBUG_LOG("Ephemeral event (kind %d) - broadcasting only, not storing",
(int)cJSON_GetNumberValue(kind));
return 0; // Success - event was handled but not stored
}
// Serialize tags to JSON (use empty array if no tags)
char* tags_json = NULL;
if (tags && cJSON_IsArray(tags)) {
@@ -739,11 +657,36 @@ int store_event(cJSON* event) {
// Execute statement
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
const char* err_msg = sqlite3_errmsg(g_db);
int extended_errcode = sqlite3_extended_errcode(g_db);
DEBUG_ERROR("INSERT failed: rc=%d, extended_errcode=%d, msg=%s", rc, extended_errcode, err_msg);
}
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
if (rc == SQLITE_CONSTRAINT) {
DEBUG_WARN("Event already exists in database");
// Add TRACE level debug to show both events
if (g_debug_level >= DEBUG_LEVEL_TRACE) {
// Get the existing event from database
cJSON* existing_event = retrieve_event(cJSON_GetStringValue(id));
if (existing_event) {
char* existing_json = cJSON_Print(existing_event);
DEBUG_TRACE("EXISTING EVENT: %s", existing_json ? existing_json : "NULL");
free(existing_json);
cJSON_Delete(existing_event);
} else {
DEBUG_TRACE("EXISTING EVENT: Could not retrieve existing event");
}
// Show the event we're trying to insert
char* new_json = cJSON_Print(event);
DEBUG_TRACE("NEW EVENT: %s", new_json ? new_json : "NULL");
free(new_json);
}
free(tags_json);
return 0; // Not an error, just duplicate
}
@@ -935,12 +878,11 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
char* msg_str = cJSON_Print(event_msg);
if (msg_str) {
size_t msg_len = strlen(msg_str);
unsigned char* buf = malloc(LWS_PRE + msg_len);
if (buf) {
memcpy(buf + LWS_PRE, msg_str, msg_len);
lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
// Use proper message queue system instead of direct lws_write
if (queue_message(wsi, NULL, msg_str, msg_len, LWS_WRITE_TEXT) != 0) {
DEBUG_ERROR("Failed to queue config EVENT message");
} else {
config_events_sent++;
free(buf);
}
free(msg_str);
}
@@ -978,11 +920,9 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
char* closed_str = cJSON_Print(closed_msg);
if (closed_str) {
size_t closed_len = strlen(closed_str);
unsigned char* buf = malloc(LWS_PRE + closed_len);
if (buf) {
memcpy(buf + LWS_PRE, closed_str, closed_len);
lws_write(wsi, buf + LWS_PRE, closed_len, LWS_WRITE_TEXT);
free(buf);
// Use proper message queue system instead of direct lws_write
if (queue_message(wsi, pss, closed_str, closed_len, LWS_WRITE_TEXT) != 0) {
DEBUG_ERROR("Failed to queue CLOSED message");
}
free(closed_str);
}
@@ -1308,19 +1248,17 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
cJSON_AddItemToArray(event_msg, cJSON_CreateString("EVENT"));
cJSON_AddItemToArray(event_msg, cJSON_CreateString(sub_id));
cJSON_AddItemToArray(event_msg, event);
char* msg_str = cJSON_Print(event_msg);
if (msg_str) {
size_t msg_len = strlen(msg_str);
unsigned char* buf = malloc(LWS_PRE + msg_len);
if (buf) {
memcpy(buf + LWS_PRE, msg_str, msg_len);
lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
free(buf);
// Use proper message queue system instead of direct lws_write
if (queue_message(wsi, pss, msg_str, msg_len, LWS_WRITE_TEXT) != 0) {
DEBUG_ERROR("Failed to queue EVENT message for sub=%s", sub_id);
}
free(msg_str);
}
cJSON_Delete(event_msg);
events_sent++;
}
@@ -1722,70 +1660,7 @@ int main(int argc, char* argv[]) {
return 1;
}
// COMMENTED OUT: Old incremental config building code replaced by unified startup sequence
// The new first_time_startup_sequence() function handles all config creation atomically
/*
// Handle configuration setup after database is initialized
// Always populate defaults directly in config table (abandoning legacy event signing)
// Populate default config values in table
if (populate_default_config_values() != 0) {
DEBUG_ERROR("Failed to populate default config values");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
// DEBUG_GUARD_START
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
int row_count = sqlite3_column_int(stmt, 0);
DEBUG_LOG("Config table row count after populate_default_config_values(): %d", row_count);
}
sqlite3_finalize(stmt);
}
}
// DEBUG_GUARD_END
// Apply CLI overrides now that database is available
if (cli_options.port_override > 0) {
char port_str[16];
snprintf(port_str, sizeof(port_str), "%d", cli_options.port_override);
if (update_config_in_table("relay_port", port_str) != 0) {
DEBUG_ERROR("Failed to update relay port override in config table");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
printf(" Port: %d (overriding default)\n", cli_options.port_override);
}
// Add pubkeys to config table (single authoritative call)
if (add_pubkeys_to_config_table() != 0) {
DEBUG_ERROR("Failed to add pubkeys to config table");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
// DEBUG_GUARD_START
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
int row_count = sqlite3_column_int(stmt, 0);
DEBUG_LOG("Config table row count after add_pubkeys_to_config_table() (first-time): %d", row_count);
}
sqlite3_finalize(stmt);
}
}
// DEBUG_GUARD_END
*/
} else {
// Find existing database file
char** existing_files = find_existing_db_files();
@@ -1873,103 +1748,7 @@ int main(int argc, char* argv[]) {
sqlite3_finalize(stmt);
}
}
// DEBUG_GUARD_END
// COMMENTED OUT: Old incremental config building code replaced by unified startup sequence
// The new startup_existing_relay() function handles all config loading atomically
/*
// Ensure default configuration values are populated (for any missing keys)
// This must be done AFTER database initialization
// COMMENTED OUT: Don't modify existing database config on restart
// if (populate_default_config_values() != 0) {
// DEBUG_WARN("Failed to populate default config values for existing relay - continuing");
// }
// Load configuration from database
cJSON* config_event = load_config_event_from_database(relay_pubkey);
if (config_event) {
if (apply_configuration_from_event(config_event) != 0) {
DEBUG_WARN("Failed to apply configuration from database");
}
cJSON_Delete(config_event);
} else {
// This is expected for relays using table-based configuration
// No longer a warning - just informational
}
// DEBUG_GUARD_START
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
int row_count = sqlite3_column_int(stmt, 0);
DEBUG_LOG("Config table row count before checking pubkeys: %d", row_count);
}
sqlite3_finalize(stmt);
}
}
// DEBUG_GUARD_END
// Ensure pubkeys are in config table for existing relay
// This handles migration from old event-based config to table-based config
const char* admin_pubkey_from_table = get_config_value_from_table("admin_pubkey");
const char* relay_pubkey_from_table = get_config_value_from_table("relay_pubkey");
int need_to_add_pubkeys = 0;
// Check if admin_pubkey is missing or invalid
if (!admin_pubkey_from_table || strlen(admin_pubkey_from_table) != 64) {
DEBUG_WARN("Admin pubkey missing or invalid in config table - will regenerate from cache");
need_to_add_pubkeys = 1;
}
if (admin_pubkey_from_table) free((char*)admin_pubkey_from_table);
// Check if relay_pubkey is missing or invalid
if (!relay_pubkey_from_table || strlen(relay_pubkey_from_table) != 64) {
DEBUG_WARN("Relay pubkey missing or invalid in config table - will regenerate from cache");
need_to_add_pubkeys = 1;
}
if (relay_pubkey_from_table) free((char*)relay_pubkey_from_table);
// If either pubkey is missing, call add_pubkeys_to_config_table to populate both
if (need_to_add_pubkeys) {
if (add_pubkeys_to_config_table() != 0) {
DEBUG_ERROR("Failed to add pubkeys to config table for existing relay");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
// DEBUG_GUARD_START
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
int row_count = sqlite3_column_int(stmt, 0);
DEBUG_LOG("Config table row count after add_pubkeys_to_config_table(): %d", row_count);
}
sqlite3_finalize(stmt);
}
}
// DEBUG_GUARD_END
}
// Apply CLI overrides for existing relay (port override should work even for existing relays)
if (cli_options.port_override > 0) {
char port_str[16];
snprintf(port_str, sizeof(port_str), "%d", cli_options.port_override);
if (update_config_in_table("relay_port", port_str) != 0) {
DEBUG_ERROR("Failed to update relay port override in config table for existing relay");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
printf(" Port: %d (overriding configured port)\n", cli_options.port_override);
}
*/
// Free memory
free(relay_pubkey);
for (int i = 0; existing_files[i]; i++) {

View File

@@ -10,10 +10,10 @@
#define MAIN_H
// Version information (auto-updated by build system)
#define VERSION "v0.7.38"
#define VERSION "v0.7.42"
#define VERSION_MAJOR 0
#define VERSION_MINOR 7
#define VERSION_PATCH 38
#define VERSION_PATCH 42
// Relay metadata (authoritative source for NIP-11 information)
#define RELAY_NAME "C-Relay"

View File

@@ -12,6 +12,7 @@
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include "websockets.h"
// Forward declaration for notice message function
@@ -22,23 +23,7 @@ int nostr_nip42_generate_challenge(char *challenge_buffer, size_t buffer_size);
int nostr_nip42_verify_auth_event(cJSON *event, const char *challenge_id,
const char *relay_url, int time_tolerance_seconds);
// Forward declaration for per_session_data struct (defined in main.c)
struct per_session_data {
int authenticated;
void* subscriptions; // Head of this session's subscription list
pthread_mutex_t session_lock; // Per-session thread safety
char client_ip[41]; // Client IP for logging
int subscription_count; // Number of subscriptions for this session
// NIP-42 Authentication State
char authenticated_pubkey[65]; // Authenticated public key (64 hex + null)
char active_challenge[65]; // Current challenge for this session (64 hex + null)
time_t challenge_created; // When challenge was created
time_t challenge_expires; // Challenge expiration time
int nip42_auth_required_events; // Whether NIP-42 auth is required for EVENT submission
int nip42_auth_required_subscriptions; // Whether NIP-42 auth is required for REQ operations
int auth_challenge_sent; // Whether challenge has been sent (0/1)
};
// Forward declaration for per_session_data struct (defined in websockets.h)
// Send NIP-42 authentication challenge to client
@@ -70,11 +55,9 @@ void send_nip42_auth_challenge(struct lws* wsi, struct per_session_data* pss) {
char* msg_str = cJSON_Print(auth_msg);
if (msg_str) {
size_t msg_len = strlen(msg_str);
unsigned char* buf = malloc(LWS_PRE + msg_len);
if (buf) {
memcpy(buf + LWS_PRE, msg_str, msg_len);
lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
free(buf);
// Use proper message queue system instead of direct lws_write
if (queue_message(wsi, pss, msg_str, msg_len, LWS_WRITE_TEXT) != 0) {
DEBUG_ERROR("Failed to queue AUTH challenge message");
}
free(msg_str);
}

View File

@@ -93,16 +93,6 @@ 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\
@@ -209,15 +199,6 @@ CREATE TABLE subscription_metrics (\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_subscriptions_id ON subscriptions(subscription_id);\n\
@@ -228,9 +209,6 @@ CREATE INDEX idx_subscriptions_wsi ON subscriptions(wsi_pointer);\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\
@@ -259,17 +237,19 @@ ORDER BY date DESC;\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 subscriptions\n\
WHERE event_type = 'created'\n\
AND subscription_id NOT IN (\n\
SELECT subscription_id FROM subscriptions\n\
WHERE event_type IN ('closed', 'expired', 'disconnected')\n\
s.subscription_id,\n\
s.client_ip,\n\
s.filter_json,\n\
s.events_sent,\n\
s.created_at,\n\
(strftime('%s', 'now') - s.created_at) as duration_seconds\n\
FROM subscriptions s\n\
WHERE s.event_type = 'created'\n\
AND NOT EXISTS (\n\
SELECT 1 FROM subscriptions s2\n\
WHERE s2.subscription_id = s.subscription_id\n\
AND s2.wsi_pointer = s.wsi_pointer\n\
AND s2.event_type IN ('closed', 'expired', 'disconnected')\n\
);\n\
\n\
-- Database Statistics Views for Admin API\n\

View File

@@ -388,30 +388,48 @@ int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
return 0;
}
// Debug: Log event details being tested
cJSON* event_kind_obj = cJSON_GetObjectItem(event, "kind");
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
cJSON* event_created_at_obj = cJSON_GetObjectItem(event, "created_at");
DEBUG_TRACE("FILTER_MATCH: Testing event kind=%d id=%.8s created_at=%ld",
event_kind_obj ? (int)cJSON_GetNumberValue(event_kind_obj) : -1,
event_id_obj && cJSON_IsString(event_id_obj) ? cJSON_GetStringValue(event_id_obj) : "null",
event_created_at_obj ? (long)cJSON_GetNumberValue(event_created_at_obj) : 0);
// Check kinds filter
if (filter->kinds && cJSON_IsArray(filter->kinds)) {
DEBUG_TRACE("FILTER_MATCH: Checking kinds filter with %d kinds", cJSON_GetArraySize(filter->kinds));
cJSON* event_kind = cJSON_GetObjectItem(event, "kind");
if (!event_kind || !cJSON_IsNumber(event_kind)) {
DEBUG_WARN("FILTER_MATCH: Event has no valid kind field");
return 0;
}
int event_kind_val = (int)cJSON_GetNumberValue(event_kind);
int kind_match = 0;
DEBUG_TRACE("FILTER_MATCH: Event kind=%d", event_kind_val);
int kind_match = 0;
cJSON* kind_item = NULL;
cJSON_ArrayForEach(kind_item, filter->kinds) {
if (cJSON_IsNumber(kind_item)) {
int filter_kind = (int)cJSON_GetNumberValue(kind_item);
DEBUG_TRACE("FILTER_MATCH: Comparing event kind %d with filter kind %d", event_kind_val, filter_kind);
if (filter_kind == event_kind_val) {
kind_match = 1;
DEBUG_TRACE("FILTER_MATCH: Kind matched!");
break;
}
}
}
if (!kind_match) {
DEBUG_TRACE("FILTER_MATCH: No kind match, filter rejected");
return 0;
}
DEBUG_TRACE("FILTER_MATCH: Kinds filter passed");
}
// Check authors filter
@@ -472,13 +490,19 @@ int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
if (filter->since > 0) {
cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
if (!event_created_at || !cJSON_IsNumber(event_created_at)) {
DEBUG_WARN("FILTER_MATCH: Event has no valid created_at field");
return 0;
}
long event_timestamp = (long)cJSON_GetNumberValue(event_created_at);
DEBUG_TRACE("FILTER_MATCH: Checking since filter: event_ts=%ld filter_since=%ld",
event_timestamp, filter->since);
if (event_timestamp < filter->since) {
DEBUG_TRACE("FILTER_MATCH: Event too old (before since), filter rejected");
return 0;
}
DEBUG_TRACE("FILTER_MATCH: Since filter passed");
}
// Check until filter
@@ -560,6 +584,7 @@ int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
}
}
DEBUG_TRACE("FILTER_MATCH: All filters passed, event matches!");
return 1; // All filters passed
}
@@ -569,14 +594,23 @@ int event_matches_subscription(cJSON* event, subscription_t* subscription) {
return 0;
}
DEBUG_TRACE("SUB_MATCH: Testing subscription '%s'", subscription->id);
int filter_num = 0;
subscription_filter_t* filter = subscription->filters;
while (filter) {
filter_num++;
DEBUG_TRACE("SUB_MATCH: Testing filter #%d", filter_num);
if (event_matches_filter(event, filter)) {
DEBUG_TRACE("SUB_MATCH: Filter #%d matched! Subscription '%s' matches",
filter_num, subscription->id);
return 1; // Match found (OR logic)
}
filter = filter->next;
}
DEBUG_TRACE("SUB_MATCH: No filters matched for subscription '%s'", subscription->id);
return 0; // No filters matched
}
@@ -598,7 +632,17 @@ int broadcast_event_to_subscriptions(cJSON* event) {
}
int broadcasts = 0;
// Log event details
cJSON* event_kind = cJSON_GetObjectItem(event, "kind");
cJSON* event_id = cJSON_GetObjectItem(event, "id");
cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
DEBUG_TRACE("BROADCAST: Event kind=%d id=%.8s created_at=%ld",
event_kind ? (int)cJSON_GetNumberValue(event_kind) : -1,
event_id && cJSON_IsString(event_id) ? cJSON_GetStringValue(event_id) : "null",
event_created_at ? (long)cJSON_GetNumberValue(event_created_at) : 0);
// Create a temporary list of matching subscriptions to avoid holding lock during I/O
typedef struct temp_sub {
struct lws* wsi;
@@ -606,13 +650,21 @@ int broadcast_event_to_subscriptions(cJSON* event) {
char client_ip[CLIENT_IP_MAX_LENGTH];
struct temp_sub* next;
} temp_sub_t;
temp_sub_t* matching_subs = NULL;
int matching_count = 0;
// First pass: collect matching subscriptions while holding lock
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
int total_subs = 0;
subscription_t* count_sub = g_subscription_manager.active_subscriptions;
while (count_sub) {
total_subs++;
count_sub = count_sub->next;
}
DEBUG_TRACE("BROADCAST: Checking %d active subscriptions", total_subs);
subscription_t* sub = g_subscription_manager.active_subscriptions;
while (sub) {
if (sub->active && sub->wsi && event_matches_subscription(event, sub)) {
@@ -692,10 +744,11 @@ int broadcast_event_to_subscriptions(cJSON* event) {
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
// Log event broadcast to database (optional - can be disabled for performance)
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
if (event_id_obj && cJSON_IsString(event_id_obj)) {
log_event_broadcast(cJSON_GetStringValue(event_id_obj), current_temp->id, current_temp->client_ip);
}
// NOTE: event_broadcasts table removed due to FOREIGN KEY constraint issues
// cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
// if (event_id_obj && cJSON_IsString(event_id_obj)) {
// log_event_broadcast(cJSON_GetStringValue(event_id_obj), current_temp->id, current_temp->client_ip);
// }
} else {
DEBUG_ERROR("Failed to queue EVENT message for sub=%s", current_temp->id);
}
@@ -906,24 +959,25 @@ void log_subscription_disconnected(const char* client_ip) {
}
// Log event broadcast to database (optional, can be resource intensive)
void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip) {
if (!g_db || !event_id || !sub_id || !client_ip) return;
const char* sql =
"INSERT INTO event_broadcasts (event_id, subscription_id, client_ip) "
"VALUES (?, ?, ?)";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, client_ip, -1, SQLITE_STATIC);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
}
// REMOVED: event_broadcasts table removed due to FOREIGN KEY constraint issues
// void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip) {
// if (!g_db || !event_id || !sub_id || !client_ip) return;
//
// const char* sql =
// "INSERT INTO event_broadcasts (event_id, subscription_id, client_ip) "
// "VALUES (?, ?, ?)";
//
// sqlite3_stmt* stmt;
// int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
// if (rc == SQLITE_OK) {
// sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC);
// sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_STATIC);
// sqlite3_bind_text(stmt, 3, client_ip, -1, SQLITE_STATIC);
//
// sqlite3_step(stmt);
// sqlite3_finalize(stmt);
// }
// }
// Update events sent counter for a subscription
void update_subscription_events_sent(const char* sub_id, int events_sent) {

View File

@@ -115,7 +115,6 @@ int get_active_connections_for_ip(const char* client_ip);
void log_subscription_created(const subscription_t* sub);
void log_subscription_closed(const char* sub_id, const char* client_ip, const char* reason);
void log_subscription_disconnected(const char* client_ip);
void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip);
void update_subscription_events_sent(const char* sub_id, int events_sent);
// Subscription query functions

View File

@@ -562,11 +562,9 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
char *error_str = cJSON_Print(error_response);
if (error_str) {
size_t error_len = strlen(error_str);
unsigned char *buf = malloc(LWS_PRE + error_len);
if (buf) {
memcpy(buf + LWS_PRE, error_str, error_len);
lws_write(wsi, buf + LWS_PRE, error_len, LWS_WRITE_TEXT);
free(buf);
// Use proper message queue system instead of direct lws_write
if (queue_message(wsi, pss, error_str, error_len, LWS_WRITE_TEXT) != 0) {
DEBUG_ERROR("Failed to queue error response message");
}
free(error_str);
}

12
tests/debug.log Normal file
View File

@@ -0,0 +1,12 @@
=== NOSTR WebSocket Debug Log Started ===
[14:13:42.079] SEND localhost:8888: ["EVENT", {
"pubkey": "e74e808f64b82fe4671b92cdf83f6dd5f5f44dbcb67fbd0e044f34a6193e0994",
"created_at": 1761499244,
"kind": 1059,
"tags": [["p", "4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"]],
"content": "ApTb8y2oD3/TtVCV73Szhgfh5ODlluGd5zjsH44g5BBwaGB1NshOJ/5kF/XN0TfYJKQBe07UTpnOYMZ4l2ppU6SrR8Tor+ZEiAF/kpCpa/x6LDDIvf4mueQicDKjOf8Y6oEbsxYjtFrpuSC0LLMgLaVhcZjAgVD0YQTo+8nHOzHZD5RBr305vdnrxIe4ubEficAHCpnKq9L3A46AIyb+aHjjTbSYmB061cf6hzLSnmdh5xeACExjhxwsX9ivSvqGYcDNsH1JCM8EYQyRX9xAPDBYM1yuS8PpadqMluOcqOd/FFYyjYNpFrardblPsjUzZTz/TDSLyrYFDUKNa7pWIhW1asc1ZaY0ry0AoWnbl/QyMxqBjDFXd3mJfWccYsOI/Yrx3sxbZdL+ayRlQeQuDk/M9rQkH8GN/5+GE1aN5I6eVl0F37Axc/lLuIt/AIpoTwZYAEi9j/BYGLP6sYkjUp0foz91QximOTgu8evynu+nfAv330HVkipTIGOjEZea7QNSK0Fylxs8fanHlmiqWGyfyBeoWpxGslHZVu6K9k7GC8ABEIdNRa8vlqlphPfWPCS70Lnq3LgeKOj1C3sNF9ST8g7pth/0FEZgXruzhpx/EyjsasNbdLZg3iX1QwRS0P4L341Flrztovt8npyP9ytTiukkYIQzXCX8XuWjiaUuzXiLkVazjh0Nl03ikKKu2+7nuaBB92geBjbGT76zZ6HeXBgcmC7dWn7pHhzqu+QTonZK0oCl427Fs0eXiYsILjxFFQkmk7OHXgdZF9jquNXloz5lgwY9S3xj4JyRwLN/9xfh16awxLZNEFvX10X97bXsmNMRUDrJJPkKMTSxZpvuTbd+Lx2iB++4NyGZibNa6nOWOJG9d2LwEzIcIHS0uQpEIPl7Ccz6+rmkVh9kLbB2rda2fYp9GCOcn6XbfaXZZXJM+HAQwPJgrtDiuQex0tEIcQcB9CYCN4ze9HCt1kb23TUgEDAipz/RqYP4dOCYmRZ7vaYk/irJ+iRDfnvPK0Id1TrSeo5kaVc7py2zWZRVdndpTM8RvW0SLwdldXDIv+ym/mS0L7bchoaYjoNeuTNKQ6AOoc0E7f4ySr65FUKYd2FTvIsP2Avsa3S+D0za30ensxr733l80AQlVmUPrhsgOzzjEuOW1hGlGus38X+CDDEuMSJnq3hvz/CxVtAk71Zkbyr5lc1BPi758Y4rlZFQnhaKYKv5nSFJc7GtDykv+1cwxNGC6AxGKprnYMDVxuAIFYBztFitdO5BsjWvvKzAbleszewtGfjE2NgltIJk+gQlTpWvLNxd3gvb+qHarfEv7BPnPfsKktDpEfuNMKXdJPANyACq5gXj854o/X8iO2iLm7JSdMhEQgIIyHNyLCCQdLDnqDWIfcdyIzAfRilSCwImt3CVJBGD7HoXRbwGRR3vgEBcoVPmsYzaU9vr62I=",
"id": "75c178ee47aac3ab9e984ddb85bdf9d8c68ade0d97e9cd86bb39e3110218a589",
"sig": "aba8382cc8d6ba6bba467109d2ddc19718732fe803d71e73fd2db62c1cbbb1b4527447240906e01755139067a71c75d8c03271826ca5d0226c818cb7fb495fe2"
}]
[14:13:42.083] RECV localhost:8888: ["OK", "75c178ee47aac3ab9e984ddb85bdf9d8c68ade0d97e9cd86bb39e3110218a589", true, ""]

BIN
tests/sendDM Executable file

Binary file not shown.

296
tests/sendDM.c Normal file
View File

@@ -0,0 +1,296 @@
/*
* NIP-17 Private Direct Messages - Command Line Application
*
* This example demonstrates how to send NIP-17 private direct messages
* using the Nostr Core Library.
*
* Usage:
* ./send_nip17_dm -r <recipient> -s <sender> [-R <relay>]... <message>
*
* Options:
* -r <recipient>: The recipient's public key (npub or hex)
* -s <sender>: The sender's private key (nsec or hex)
* -R <relay>: Relay URL to send to (can be specified multiple times)
* <message>: The message to send (must be the last argument)
*
* If no relays are specified, uses default relay.
* If no sender key is provided, uses a default test key.
*
* Examples:
* ./send_nip17_dm -r npub1example... -s nsec1test... -R wss://relay1.com "Hello from NIP-17!"
* ./send_nip17_dm -r 4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa -s aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -R ws://localhost:8888 "config"
*/
#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
#include "../nostr_core_lib/nostr_core/nostr_core.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <getopt.h>
// Default test private key (for demonstration - DO NOT USE IN PRODUCTION)
#define DEFAULT_SENDER_NSEC "nsec12kgt0dv2k2safv6s32w8f89z9uw27e68hjaa0d66c5xvk70ezpwqncd045"
// Default relay for sending DMs
#define DEFAULT_RELAY "wss://relay.laantungir.net"
// Progress callback for publishing
void publish_progress_callback(const char* relay_url, const char* status,
const char* message, int success_count,
int total_relays, int completed_relays, void* user_data) {
(void)user_data;
if (relay_url) {
printf("📡 [%s]: %s", relay_url, status);
if (message) {
printf(" - %s", message);
}
printf(" (%d/%d completed, %d successful)\n", completed_relays, total_relays, success_count);
} else {
printf("📡 PUBLISH COMPLETE: %d/%d successful\n", success_count, total_relays);
}
}
/**
* Convert npub or hex pubkey to hex format
*/
int convert_pubkey_to_hex(const char* input_pubkey, char* output_hex) {
// Check if it's already hex (64 characters)
if (strlen(input_pubkey) == 64) {
// Assume it's already hex
strcpy(output_hex, input_pubkey);
return 0;
}
// Check if it's an npub (starts with "npub1")
if (strncmp(input_pubkey, "npub1", 5) == 0) {
// Convert npub to hex
unsigned char pubkey_bytes[32];
if (nostr_decode_npub(input_pubkey, pubkey_bytes) != 0) {
fprintf(stderr, "Error: Invalid npub format\n");
return -1;
}
nostr_bytes_to_hex(pubkey_bytes, 32, output_hex);
return 0;
}
fprintf(stderr, "Error: Public key must be 64-character hex or valid npub\n");
return -1;
}
/**
* Convert nsec to private key bytes if needed
*/
int convert_nsec_to_private_key(const char* input_nsec, unsigned char* private_key) {
// Check if it's already hex (64 characters)
if (strlen(input_nsec) == 64) {
// Convert hex to bytes
if (nostr_hex_to_bytes(input_nsec, private_key, 32) != 0) {
fprintf(stderr, "Error: Invalid hex private key\n");
return -1;
}
return 0;
}
// Check if it's an nsec (starts with "nsec1")
if (strncmp(input_nsec, "nsec1", 5) == 0) {
// Convert nsec directly to private key bytes
if (nostr_decode_nsec(input_nsec, private_key) != 0) {
fprintf(stderr, "Error: Invalid nsec format\n");
return -1;
}
return 0;
}
fprintf(stderr, "Error: Private key must be 64-character hex or valid nsec\n");
return -1;
}
/**
* Main function
*/
int main(int argc, char* argv[]) {
char* recipient_key = NULL;
char* sender_key = NULL;
char** relays = NULL;
int relay_count = 0;
char* message = NULL;
// Parse command line options
int opt;
while ((opt = getopt(argc, argv, "r:s:R:")) != -1) {
switch (opt) {
case 'r':
recipient_key = optarg;
break;
case 's':
sender_key = optarg;
break;
case 'R':
relays = realloc(relays, (relay_count + 1) * sizeof(char*));
relays[relay_count] = optarg;
relay_count++;
break;
default:
fprintf(stderr, "Usage: %s -r <recipient> -s <sender> [-R <relay>]... <message>\n", argv[0]);
fprintf(stderr, "Options:\n");
fprintf(stderr, " -r <recipient>: The recipient's public key (npub or hex)\n");
fprintf(stderr, " -s <sender>: The sender's private key (nsec or hex)\n");
fprintf(stderr, " -R <relay>: Relay URL to send to (can be specified multiple times)\n");
fprintf(stderr, " <message>: The message to send (must be the last argument)\n");
return 1;
}
}
// Check for required arguments
if (!recipient_key) {
fprintf(stderr, "Error: Recipient key (-r) is required\n");
return 1;
}
// Get message from remaining arguments
if (optind >= argc) {
fprintf(stderr, "Error: Message is required\n");
return 1;
}
message = argv[optind];
// Use default values if not provided
if (!sender_key) {
sender_key = DEFAULT_SENDER_NSEC;
}
if (relay_count == 0) {
relays = malloc(sizeof(char*));
relays[0] = DEFAULT_RELAY;
relay_count = 1;
}
printf("🧪 NIP-17 Private Direct Message Sender\n");
printf("======================================\n\n");
// Initialize crypto
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to initialize crypto\n");
free(relays);
return 1;
}
// Convert recipient pubkey
char recipient_pubkey_hex[65];
if (convert_pubkey_to_hex(recipient_key, recipient_pubkey_hex) != 0) {
free(relays);
return 1;
}
// Convert sender private key
unsigned char sender_privkey[32];
if (convert_nsec_to_private_key(sender_key, sender_privkey) != 0) {
free(relays);
return 1;
}
// Derive sender public key for display
unsigned char sender_pubkey_bytes[32];
char sender_pubkey_hex[65];
if (nostr_ec_public_key_from_private_key(sender_privkey, sender_pubkey_bytes) != 0) {
fprintf(stderr, "Failed to derive sender public key\n");
return 1;
}
nostr_bytes_to_hex(sender_pubkey_bytes, 32, sender_pubkey_hex);
printf("📤 Sender: %s\n", sender_pubkey_hex);
printf("📥 Recipient: %s\n", recipient_pubkey_hex);
printf("💬 Message: %s\n", message);
printf("🌐 Relays: ");
for (int i = 0; i < relay_count; i++) {
printf("%s", relays[i]);
if (i < relay_count - 1) printf(", ");
}
printf("\n\n");
// Create DM event
printf("💬 Creating DM event...\n");
const char* recipient_pubkeys[] = {recipient_pubkey_hex};
cJSON* dm_event = nostr_nip17_create_chat_event(
message,
recipient_pubkeys,
1,
"NIP-17 CLI", // subject
NULL, // no reply
relays[0], // relay hint (use first relay)
sender_pubkey_hex
);
if (!dm_event) {
fprintf(stderr, "Failed to create DM event\n");
return 1;
}
printf("✅ Created DM event (kind 14)\n");
// Send DM (create gift wraps)
printf("🎁 Creating gift wraps...\n");
cJSON* gift_wraps[10]; // Max 10 gift wraps
int gift_wrap_count = nostr_nip17_send_dm(
dm_event,
recipient_pubkeys,
1,
sender_privkey,
gift_wraps,
10
);
cJSON_Delete(dm_event); // Original DM event no longer needed
if (gift_wrap_count <= 0) {
fprintf(stderr, "Failed to create gift wraps\n");
return 1;
}
printf("✅ Created %d gift wrap(s)\n", gift_wrap_count);
// Publish the gift wrap to relays
printf("\n📤 Publishing gift wrap to %d relay(s)...\n", relay_count);
int success_count = 0;
publish_result_t* publish_results = synchronous_publish_event_with_progress(
(const char**)relays,
relay_count,
gift_wraps[0], // Send the first gift wrap
&success_count,
10, // 10 second timeout
publish_progress_callback,
NULL, // no user data
0, // NIP-42 disabled
NULL // no private key for auth
);
if (!publish_results || success_count == 0) {
fprintf(stderr, "\n❌ Failed to publish gift wrap to any relay (success_count: %d/%d)\n", success_count, relay_count);
// Clean up gift wraps
for (int i = 0; i < gift_wrap_count; i++) {
cJSON_Delete(gift_wraps[i]);
}
if (publish_results) free(publish_results);
free(relays);
return 1;
}
printf("\n✅ Successfully published NIP-17 DM to %d/%d relay(s)!\n", success_count, relay_count);
// Clean up
free(publish_results);
for (int i = 0; i < gift_wrap_count; i++) {
cJSON_Delete(gift_wraps[i]);
}
free(relays);
nostr_cleanup();
printf("\n🎉 DM sent successfully! The recipient can now decrypt it using their private key.\n");
return 0;
}