Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a916cc221 | ||
|
|
dcf421ff93 | ||
|
|
d655258311 | ||
|
|
f6d13d4318 |
Binary file not shown.
Binary file not shown.
@@ -187,12 +187,47 @@ button:disabled {
|
||||
|
||||
.config-table th {
|
||||
font-weight: bold;
|
||||
height: 40px; /* Double the default height */
|
||||
line-height: 40px; /* Center text vertically */
|
||||
}
|
||||
|
||||
.config-table tr:hover {
|
||||
background-color: var(--muted-color);
|
||||
}
|
||||
|
||||
/* Inline config value inputs - remove borders and padding to fit seamlessly in table cells */
|
||||
.config-value-input {
|
||||
border: none;
|
||||
padding: 2px 4px;
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
min-height: auto;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.config-value-input:focus {
|
||||
border: 1px solid var(--accent-color);
|
||||
background: var(--secondary-color);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Config actions cell - clickable for saving */
|
||||
.config-actions-cell {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.config-actions-cell:hover {
|
||||
border: 1px solid var(--accent-color);
|
||||
background-color: var(--muted-color);
|
||||
}
|
||||
|
||||
.json-display {
|
||||
background-color: var(--secondary-color);
|
||||
border: var(--border-width) solid var(--border-color);
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<div class="inline-buttons">
|
||||
<button type="button" id="connect-relay-btn">CONNECT TO RELAY</button>
|
||||
<button type="button" id="disconnect-relay-btn" disabled>DISCONNECT</button>
|
||||
<button type="button" id="test-websocket-btn" disabled>TEST WEBSOCKET</button>
|
||||
<button type="button" id="restart-relay-btn" disabled>RESTART RELAY</button>
|
||||
</div>
|
||||
|
||||
<div class="status disconnected" id="relay-connection-status">NOT CONNECTED</div>
|
||||
@@ -93,41 +93,25 @@
|
||||
<div id="div_config" class="section flex-section" style="display: none;">
|
||||
<h2>RELAY CONFIGURATION</h2>
|
||||
<div id="config-display" class="hidden">
|
||||
<div id="config-view-mode">
|
||||
<div class="config-table-container">
|
||||
<table class="config-table" id="config-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Parameter</th>
|
||||
<th>Value</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="config-table-body">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="inline-buttons">
|
||||
|
||||
<button type="button" id="edit-config-btn">EDIT CONFIGURATION</button>
|
||||
<button type="button" id="copy-config-btn">COPY CONFIGURATION</button>
|
||||
<button type="button" id="fetch-config-btn">REFRESH</button>
|
||||
</div>
|
||||
<div class="config-table-container">
|
||||
<table class="config-table" id="config-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Parameter</th>
|
||||
<th>Value</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="config-table-body">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="config-edit-mode" class="hidden">
|
||||
<h3>Edit Configuration</h3>
|
||||
<div id="config-form" class="section">
|
||||
<!-- Dynamic form will be generated here -->
|
||||
</div>
|
||||
|
||||
<div class="inline-buttons">
|
||||
<button type="button" id="save-config-btn">SAVE & PUBLISH</button>
|
||||
<button type="button" id="cancel-edit-btn">CANCEL</button>
|
||||
</div>
|
||||
<div class="inline-buttons">
|
||||
<button type="button" id="fetch-config-btn">REFRESH</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -316,8 +300,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NIP-17 DIRECT MESSAGES Section -->
|
||||
<div class="section" id="nip17DMSection" style="display: none;">
|
||||
<div class="section-header">
|
||||
<h2>NIP-17 DIRECT MESSAGES</h2>
|
||||
</div>
|
||||
|
||||
<!-- Outbox -->
|
||||
<div class="input-group">
|
||||
<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>
|
||||
|
||||
<!-- Send Button -->
|
||||
<div class="input-group">
|
||||
<button type="button" id="send-dm-btn">SEND MESSAGE</button>
|
||||
</div>
|
||||
|
||||
<!-- Inbox -->
|
||||
<div class="input-group">
|
||||
<label>Received Messages from Relay:</label>
|
||||
<div id="dm-inbox" class="log-panel" style="height: 200px;">
|
||||
<div class="log-entry">No messages received yet.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load the official nostr-tools bundle first -->
|
||||
<!-- <script src="https://laantungir.net/nostr-login-lite/nostr.bundle.js"></script> -->
|
||||
|
||||
756
api/index.js
756
api/index.js
@@ -41,16 +41,14 @@
|
||||
const relayConnectionStatus = document.getElementById('relay-connection-status');
|
||||
const connectRelayBtn = document.getElementById('connect-relay-btn');
|
||||
const disconnectRelayBtn = document.getElementById('disconnect-relay-btn');
|
||||
const testWebSocketBtn = document.getElementById('test-websocket-btn');
|
||||
const restartRelayBtn = document.getElementById('restart-relay-btn');
|
||||
const configDisplay = document.getElementById('config-display');
|
||||
const configViewMode = document.getElementById('config-view-mode');
|
||||
const configEditMode = document.getElementById('config-edit-mode');
|
||||
const configTableBody = document.getElementById('config-table-body');
|
||||
const configForm = document.getElementById('config-form');
|
||||
const copyConfigBtn = document.getElementById('copy-config-btn');
|
||||
const editConfigBtn = document.getElementById('edit-config-btn');
|
||||
const saveConfigBtn = document.getElementById('save-config-btn');
|
||||
const cancelEditBtn = document.getElementById('cancel-edit-btn');
|
||||
|
||||
// NIP-17 DM elements
|
||||
const dmOutbox = document.getElementById('dm-outbox');
|
||||
const dmInbox = document.getElementById('dm-inbox');
|
||||
const sendDmBtn = document.getElementById('send-dm-btn');
|
||||
|
||||
// Utility functions
|
||||
function log(message, type = 'INFO') {
|
||||
@@ -63,6 +61,33 @@
|
||||
// UI logging removed - using console only
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function log(message, type = 'INFO') {
|
||||
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
|
||||
const logMessage = `${timestamp} [${type}]: ${message}`;
|
||||
|
||||
// Always log to browser console so we don't lose logs on refresh
|
||||
console.log(logMessage);
|
||||
|
||||
// UI logging removed - using console only
|
||||
}
|
||||
|
||||
// NIP-59 helper: randomize created_at to thwart time-analysis (past 2 days)
|
||||
function randomNow() {
|
||||
const TWO_DAYS = 2 * 24 * 60 * 60; // 172800 seconds
|
||||
const now = Math.round(Date.now() / 1000);
|
||||
return Math.round(now - Math.random() * TWO_DAYS);
|
||||
}
|
||||
|
||||
// Safe JSON parse with error handling
|
||||
function safeJsonParse(jsonString) {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (error) {
|
||||
console.error('JSON parse error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// ================================
|
||||
// NIP-11 RELAY CONNECTION FUNCTIONS
|
||||
// ================================
|
||||
@@ -344,28 +369,28 @@
|
||||
relayConnectionStatus.className = 'status connected';
|
||||
connectRelayBtn.disabled = true;
|
||||
disconnectRelayBtn.disabled = true;
|
||||
testWebSocketBtn.disabled = true;
|
||||
restartRelayBtn.disabled = true;
|
||||
break;
|
||||
case 'connected':
|
||||
relayConnectionStatus.textContent = 'CONNECTED';
|
||||
relayConnectionStatus.className = 'status connected';
|
||||
connectRelayBtn.disabled = true;
|
||||
disconnectRelayBtn.disabled = false;
|
||||
testWebSocketBtn.disabled = false;
|
||||
restartRelayBtn.disabled = false;
|
||||
break;
|
||||
case 'disconnected':
|
||||
relayConnectionStatus.textContent = 'NOT CONNECTED';
|
||||
relayConnectionStatus.className = 'status disconnected';
|
||||
connectRelayBtn.disabled = false;
|
||||
disconnectRelayBtn.disabled = true;
|
||||
testWebSocketBtn.disabled = true;
|
||||
restartRelayBtn.disabled = true;
|
||||
break;
|
||||
case 'error':
|
||||
relayConnectionStatus.textContent = 'CONNECTION FAILED';
|
||||
relayConnectionStatus.className = 'status error';
|
||||
connectRelayBtn.disabled = false;
|
||||
disconnectRelayBtn.disabled = true;
|
||||
testWebSocketBtn.disabled = true;
|
||||
restartRelayBtn.disabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -582,10 +607,12 @@
|
||||
function updateAdminSectionsVisibility() {
|
||||
const divConfig = document.getElementById('div_config');
|
||||
const authRulesSection = document.getElementById('authRulesSection');
|
||||
const nip17DMSection = document.getElementById('nip17DMSection');
|
||||
const shouldShow = isLoggedIn && isRelayConnected;
|
||||
|
||||
if (divConfig) divConfig.style.display = shouldShow ? 'block' : 'none';
|
||||
if (authRulesSection) authRulesSection.style.display = shouldShow ? 'block' : 'none';
|
||||
if (nip17DMSection) nip17DMSection.style.display = shouldShow ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// Show main interface after login
|
||||
@@ -736,30 +763,105 @@
|
||||
subscriptionId = generateSubId();
|
||||
|
||||
console.log(`Generated subscription ID: ${subscriptionId}`);
|
||||
|
||||
// Subscribe to kind 23457 events (admin response events)
|
||||
console.log(`User pubkey ${userPubkey}`)
|
||||
// Subscribe to kind 23457 events (admin response events), kind 4 (NIP-04 DMs), and kind 1059 (NIP-17 GiftWrap)
|
||||
const subscription = relayPool.subscribeMany([url], [{
|
||||
since: Math.floor(Date.now() / 1000),
|
||||
kinds: [23457],
|
||||
authors: [getRelayPubkey()], // Only listen to responses from the relay
|
||||
"#p": [userPubkey], // Only responses directed to this user
|
||||
limit: 50
|
||||
}, {
|
||||
since: Math.floor(Date.now() / 1000),
|
||||
kinds: [4], // NIP-04 Direct Messages
|
||||
authors: [getRelayPubkey()], // Only listen to DMs from the relay
|
||||
"#p": [userPubkey], // Only DMs directed to this user
|
||||
limit: 50
|
||||
}, {
|
||||
kinds: [1059], // NIP-17 GiftWrap events
|
||||
"#p": [userPubkey], // Only GiftWrap events addressed to this user
|
||||
limit: 50
|
||||
}], {
|
||||
onevent(event) {
|
||||
console.log('=== ADMIN RESPONSE EVENT RECEIVED VIA SIMPLEPOOL ===');
|
||||
async onevent(event) {
|
||||
console.log('=== EVENT RECEIVED VIA SIMPLEPOOL ===');
|
||||
console.log('Event data:', event);
|
||||
console.log('Event kind:', event.kind);
|
||||
console.log('Event tags:', event.tags);
|
||||
console.log('Event pubkey:', event.pubkey);
|
||||
console.log('=== END ADMIN RESPONSE ===');
|
||||
console.log('=== END EVENT ===');
|
||||
|
||||
// Log all received messages for testing
|
||||
if (typeof logTestEvent === 'function') {
|
||||
logTestEvent('RECV', `Admin response event: ${JSON.stringify(event)}`, 'EVENT');
|
||||
// Handle NIP-04 DMs
|
||||
if (event.kind === 4) {
|
||||
console.log('=== NIP-04 DM RECEIVED ===');
|
||||
try {
|
||||
// Decrypt the DM content
|
||||
const decryptedContent = await window.nostr.nip04.decrypt(event.pubkey, event.content);
|
||||
log(`Received NIP-04 DM from relay: ${decryptedContent.substring(0, 50)}...`, 'INFO');
|
||||
|
||||
// Add to inbox
|
||||
const timestamp = new Date(event.created_at * 1000).toLocaleString();
|
||||
addMessageToInbox('received', decryptedContent, timestamp);
|
||||
|
||||
// Log for testing
|
||||
if (typeof logTestEvent === 'function') {
|
||||
logTestEvent('RECV', `NIP-04 DM: ${decryptedContent}`, 'DM');
|
||||
}
|
||||
} catch (decryptError) {
|
||||
log(`Failed to decrypt NIP-04 DM: ${decryptError.message}`, 'ERROR');
|
||||
if (typeof logTestEvent === 'function') {
|
||||
logTestEvent('ERROR', `Failed to decrypt DM: ${decryptError.message}`, 'DM');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Process admin response event
|
||||
processAdminResponse(event);
|
||||
// Handle NIP-17 GiftWrap DMs
|
||||
if (event.kind === 1059) {
|
||||
console.log('=== NIP-17 GIFTWRAP RECEIVED ===');
|
||||
try {
|
||||
// Step 1: Unwrap gift wrap to get seal
|
||||
const sealJson = await window.nostr.nip44.decrypt(event.pubkey, event.content);
|
||||
const seal = safeJsonParse(sealJson);
|
||||
if (!seal || seal.kind !== 13) {
|
||||
throw new Error('Unwrapped content is not a valid seal (kind 13)');
|
||||
}
|
||||
|
||||
// Step 2: Unseal to get rumor
|
||||
const rumorJson = await window.nostr.nip44.decrypt(seal.pubkey, seal.content);
|
||||
const rumor = safeJsonParse(rumorJson);
|
||||
if (!rumor || rumor.kind !== 14) {
|
||||
throw new Error('Unsealed content is not a valid rumor (kind 14)');
|
||||
}
|
||||
|
||||
log(`Received NIP-17 DM from relay: ${rumor.content.substring(0, 50)}...`, 'INFO');
|
||||
|
||||
// Add to inbox
|
||||
const timestamp = new Date(event.created_at * 1000).toLocaleString();
|
||||
addMessageToInbox('received', rumor.content, timestamp);
|
||||
|
||||
// Log for testing
|
||||
if (typeof logTestEvent === 'function') {
|
||||
logTestEvent('RECV', `NIP-17 DM: ${rumor.content}`, 'DM');
|
||||
}
|
||||
} catch (unwrapError) {
|
||||
log(`Failed to unwrap NIP-17 DM: ${unwrapError.message}`, 'ERROR');
|
||||
if (typeof logTestEvent === 'function') {
|
||||
logTestEvent('ERROR', `Failed to unwrap DM: ${unwrapError.message}`, 'DM');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle admin response events (kind 23457)
|
||||
if (event.kind === 23457) {
|
||||
// Log all received messages for testing
|
||||
if (typeof logTestEvent === 'function') {
|
||||
logTestEvent('RECV', `Admin response event: ${JSON.stringify(event)}`, 'EVENT');
|
||||
}
|
||||
|
||||
// Process admin response event
|
||||
processAdminResponse(event);
|
||||
}
|
||||
},
|
||||
oneose() {
|
||||
console.log('EOSE received - End of stored events');
|
||||
@@ -1270,32 +1372,61 @@
|
||||
// Clear existing table
|
||||
configTableBody.innerHTML = '';
|
||||
|
||||
// Display basic event info
|
||||
const basicInfo = [
|
||||
['Event ID', event.id],
|
||||
['Public Key', event.pubkey],
|
||||
['Created At', new Date(event.created_at * 1000).toISOString()],
|
||||
['Kind', event.kind],
|
||||
['Content', event.content]
|
||||
];
|
||||
|
||||
console.log(`Adding ${basicInfo.length} basic info rows`);
|
||||
basicInfo.forEach(([key, value]) => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `<td>${key}</td><td>${value}</td><td>-</td>`;
|
||||
configTableBody.appendChild(row);
|
||||
});
|
||||
|
||||
// Display tags
|
||||
console.log(`Processing ${event.tags.length} tags`);
|
||||
event.tags.forEach(tag => {
|
||||
// Display tags (editable configuration parameters only)
|
||||
console.log(`Processing ${event.tags.length} configuration parameters`);
|
||||
event.tags.forEach((tag, index) => {
|
||||
if (tag.length >= 2) {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `<td>${tag[0]}</td><td>${tag[1]}</td><td>-</td>`;
|
||||
const key = tag[0];
|
||||
const value = tag[1];
|
||||
|
||||
// Create editable input for value
|
||||
const valueInput = document.createElement('input');
|
||||
valueInput.type = 'text';
|
||||
valueInput.value = value;
|
||||
valueInput.className = 'config-value-input';
|
||||
valueInput.dataset.key = key;
|
||||
valueInput.dataset.originalValue = value;
|
||||
valueInput.dataset.rowIndex = index;
|
||||
|
||||
// Create clickable Actions cell
|
||||
const actionsCell = document.createElement('td');
|
||||
actionsCell.className = 'config-actions-cell';
|
||||
actionsCell.textContent = 'SAVE';
|
||||
actionsCell.dataset.key = key;
|
||||
actionsCell.dataset.originalValue = value;
|
||||
actionsCell.dataset.rowIndex = index;
|
||||
|
||||
// Initially hide the SAVE text
|
||||
actionsCell.style.color = 'transparent';
|
||||
|
||||
// Show SAVE text and make clickable when value changes
|
||||
valueInput.addEventListener('input', function() {
|
||||
if (this.value !== this.dataset.originalValue) {
|
||||
actionsCell.style.color = 'var(--primary-color)';
|
||||
actionsCell.style.cursor = 'pointer';
|
||||
actionsCell.onclick = () => saveIndividualConfig(key, valueInput.value, valueInput.dataset.originalValue, actionsCell);
|
||||
} else {
|
||||
actionsCell.style.color = 'transparent';
|
||||
actionsCell.style.cursor = 'default';
|
||||
actionsCell.onclick = null;
|
||||
}
|
||||
});
|
||||
|
||||
row.innerHTML = `<td>${key}</td><td></td>`;
|
||||
row.cells[1].appendChild(valueInput);
|
||||
row.appendChild(actionsCell);
|
||||
configTableBody.appendChild(row);
|
||||
}
|
||||
});
|
||||
|
||||
// Show message if no configuration parameters found
|
||||
if (event.tags.length === 0) {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `<td colspan="3" style="text-align: center; font-style: italic;">No configuration parameters found</td>`;
|
||||
configTableBody.appendChild(row);
|
||||
}
|
||||
|
||||
console.log('Configuration display completed successfully');
|
||||
updateConfigStatus(true);
|
||||
|
||||
@@ -1305,195 +1436,98 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration editing functions
|
||||
function generateConfigForm(event) {
|
||||
if (!event || !event.tags) {
|
||||
console.log('No configuration event to edit');
|
||||
return;
|
||||
}
|
||||
|
||||
configForm.innerHTML = '';
|
||||
|
||||
// Define field types and validation for different config parameters (aligned with README.md)
|
||||
const fieldTypes = {
|
||||
'auth_enabled': 'boolean',
|
||||
'nip42_auth_required': 'boolean',
|
||||
'nip40_expiration_enabled': 'boolean',
|
||||
'max_connections': 'number',
|
||||
'pow_min_difficulty': 'number',
|
||||
'nip42_challenge_timeout': 'number',
|
||||
'max_subscriptions_per_client': 'number',
|
||||
'max_event_tags': 'number',
|
||||
'max_content_length': 'number'
|
||||
};
|
||||
|
||||
const descriptions = {
|
||||
'relay_pubkey': 'Relay Public Key (Read-only)',
|
||||
'auth_enabled': 'Enable Authentication',
|
||||
'nip42_auth_required': 'Enable NIP-42 Cryptographic Authentication',
|
||||
'nip42_auth_required_kinds': 'Event Kinds Requiring NIP-42 Auth',
|
||||
'nip42_challenge_timeout': 'NIP-42 Challenge Expiration Seconds',
|
||||
'max_connections': 'Maximum Connections',
|
||||
'relay_description': 'Relay Description',
|
||||
'relay_contact': 'Relay Contact',
|
||||
'pow_min_difficulty': 'Minimum Proof-of-Work Difficulty',
|
||||
'nip40_expiration_enabled': 'Enable Event Expiration',
|
||||
'max_subscriptions_per_client': 'Max Subscriptions per Client',
|
||||
'max_event_tags': 'Maximum Tags per Event',
|
||||
'max_content_length': 'Maximum Event Content Length'
|
||||
};
|
||||
|
||||
// Process configuration tags (no d tag filtering for ephemeral events)
|
||||
const configData = {};
|
||||
event.tags.forEach(tag => {
|
||||
if (tag.length >= 2) {
|
||||
configData[tag[0]] = tag[1];
|
||||
}
|
||||
});
|
||||
|
||||
// Create form fields for each configuration parameter
|
||||
Object.entries(configData).forEach(([key, value]) => {
|
||||
const fieldType = fieldTypes[key] || 'text';
|
||||
const description = descriptions[key] || key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
|
||||
const fieldGroup = document.createElement('div');
|
||||
fieldGroup.className = 'input-group';
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.textContent = description;
|
||||
label.setAttribute('for', `config-${key}`);
|
||||
|
||||
let input;
|
||||
|
||||
if (fieldType === 'boolean') {
|
||||
input = document.createElement('select');
|
||||
input.innerHTML = `
|
||||
<option value="true" ${value === 'true' ? 'selected' : ''}>true</option>
|
||||
<option value="false" ${value === 'false' ? 'selected' : ''}>false</option>
|
||||
`;
|
||||
} else if (fieldType === 'number') {
|
||||
input = document.createElement('input');
|
||||
input.type = 'number';
|
||||
input.value = value;
|
||||
input.min = '0';
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.value = value;
|
||||
}
|
||||
|
||||
input.id = `config-${key}`;
|
||||
input.name = key;
|
||||
|
||||
// Make relay_pubkey read-only
|
||||
if (key === 'relay_pubkey' || key === 'd') {
|
||||
input.disabled = true;
|
||||
}
|
||||
|
||||
fieldGroup.appendChild(label);
|
||||
fieldGroup.appendChild(input);
|
||||
configForm.appendChild(fieldGroup);
|
||||
});
|
||||
|
||||
console.log('Configuration form generated');
|
||||
}
|
||||
|
||||
function enterEditMode() {
|
||||
if (!currentConfig) {
|
||||
console.log('No configuration loaded to edit');
|
||||
return;
|
||||
}
|
||||
|
||||
generateConfigForm(currentConfig);
|
||||
configViewMode.classList.add('hidden');
|
||||
configEditMode.classList.remove('hidden');
|
||||
console.log('Entered edit mode');
|
||||
}
|
||||
|
||||
function exitEditMode() {
|
||||
configViewMode.classList.remove('hidden');
|
||||
configEditMode.classList.add('hidden');
|
||||
configForm.innerHTML = '';
|
||||
console.log('Exited edit mode');
|
||||
}
|
||||
|
||||
|
||||
async function saveConfiguration() {
|
||||
// Save individual configuration parameter
|
||||
async function saveIndividualConfig(key, newValue, originalValue, actionsCell) {
|
||||
if (!isLoggedIn || !userPubkey) {
|
||||
console.log('Must be logged in to save configuration');
|
||||
log('Must be logged in to save configuration', 'ERROR');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentConfig) {
|
||||
console.log('No current configuration to update');
|
||||
log('No current configuration to update', 'ERROR');
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't save if value hasn't changed
|
||||
if (newValue === originalValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Building configuration update command...');
|
||||
log(`Saving individual config: ${key} = ${newValue}`, 'INFO');
|
||||
|
||||
// Collect form data
|
||||
const formInputs = configForm.querySelectorAll('input, select');
|
||||
const configObjects = [];
|
||||
// Determine data type based on key name
|
||||
let dataType = 'string';
|
||||
if (['max_connections', 'pow_min_difficulty', 'nip42_challenge_timeout', 'max_subscriptions_per_client', 'max_event_tags', 'max_content_length'].includes(key)) {
|
||||
dataType = 'integer';
|
||||
} else if (['auth_enabled', 'nip42_auth_required', 'nip40_expiration_enabled'].includes(key)) {
|
||||
dataType = 'boolean';
|
||||
}
|
||||
|
||||
// Process each form input as a config object
|
||||
formInputs.forEach(input => {
|
||||
if (!input.disabled && input.name && input.name !== 'd' && input.name !== 'relay_pubkey') {
|
||||
// Determine data type based on input type
|
||||
let dataType = 'string';
|
||||
if (input.type === 'number') {
|
||||
dataType = 'integer';
|
||||
} else if (input.tagName === 'SELECT' && (input.value === 'true' || input.value === 'false')) {
|
||||
dataType = 'boolean';
|
||||
}
|
||||
// Determine category based on key name
|
||||
let category = 'general';
|
||||
if (key.startsWith('relay_')) {
|
||||
category = 'relay';
|
||||
} else if (key.startsWith('nip40_')) {
|
||||
category = 'expiration';
|
||||
} else if (key.startsWith('nip42_') || key.startsWith('auth_')) {
|
||||
category = 'authentication';
|
||||
} else if (key.startsWith('pow_')) {
|
||||
category = 'proof_of_work';
|
||||
} else if (key.startsWith('max_')) {
|
||||
category = 'limits';
|
||||
}
|
||||
|
||||
// Determine category based on key name
|
||||
let category = 'general';
|
||||
const key = input.name;
|
||||
if (key.startsWith('relay_')) {
|
||||
category = 'relay';
|
||||
} else if (key.startsWith('nip40_')) {
|
||||
category = 'expiration';
|
||||
} else if (key.startsWith('nip42_') || key.startsWith('auth_')) {
|
||||
category = 'authentication';
|
||||
} else if (key.startsWith('pow_')) {
|
||||
category = 'proof_of_work';
|
||||
} else if (key.startsWith('max_')) {
|
||||
category = 'limits';
|
||||
}
|
||||
const configObj = {
|
||||
key: key,
|
||||
value: newValue,
|
||||
data_type: dataType,
|
||||
category: category
|
||||
};
|
||||
|
||||
configObjects.push({
|
||||
key: key,
|
||||
value: input.value,
|
||||
data_type: dataType,
|
||||
category: category
|
||||
});
|
||||
// Update cell during save
|
||||
actionsCell.textContent = 'SAVING...';
|
||||
actionsCell.style.color = 'var(--accent-color)';
|
||||
actionsCell.style.cursor = 'not-allowed';
|
||||
actionsCell.onclick = null;
|
||||
|
||||
// Send single config update
|
||||
await sendConfigUpdateCommand([configObj]);
|
||||
|
||||
// Update the original value on success
|
||||
const input = actionsCell.parentElement.cells[1].querySelector('input');
|
||||
if (input) {
|
||||
input.dataset.originalValue = newValue;
|
||||
// Hide SAVE text since value now matches original
|
||||
actionsCell.style.color = 'transparent';
|
||||
actionsCell.style.cursor = 'default';
|
||||
actionsCell.onclick = null;
|
||||
}
|
||||
|
||||
actionsCell.textContent = 'SAVED';
|
||||
actionsCell.style.color = 'var(--accent-color)';
|
||||
setTimeout(() => {
|
||||
actionsCell.textContent = 'SAVE';
|
||||
// Keep transparent if value matches original
|
||||
if (input && input.value === input.dataset.originalValue) {
|
||||
actionsCell.style.color = 'transparent';
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
if (configObjects.length === 0) {
|
||||
console.log('No configuration changes to save');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Sending config_update commands for ${configObjects.length} configuration objects...`);
|
||||
|
||||
// Send config_update commands one at a time to avoid large event size
|
||||
for (const configObj of configObjects) {
|
||||
await sendConfigUpdateCommand([configObj]);
|
||||
}
|
||||
|
||||
console.log('Configuration update command sent successfully');
|
||||
|
||||
// Exit edit mode
|
||||
exitEditMode();
|
||||
log(`Successfully saved config: ${key} = ${newValue}`, 'INFO');
|
||||
|
||||
} catch (error) {
|
||||
console.log('Configuration save failed: ' + error.message);
|
||||
console.error('Save configuration error:', error);
|
||||
log(`Failed to save individual config ${key}: ${error.message}`, 'ERROR');
|
||||
actionsCell.textContent = 'SAVE';
|
||||
actionsCell.style.color = 'var(--primary-color)';
|
||||
actionsCell.style.cursor = 'pointer';
|
||||
actionsCell.onclick = () => saveIndividualConfig(key, actionsCell.parentElement.cells[1].querySelector('input').value, originalValue, actionsCell);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Send config update command using kind 23456 with Administrator API (inner events)
|
||||
async function sendConfigUpdateCommand(configObjects) {
|
||||
try {
|
||||
@@ -1619,35 +1653,7 @@
|
||||
});
|
||||
|
||||
|
||||
copyConfigBtn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (currentConfig) {
|
||||
navigator.clipboard.writeText(JSON.stringify(currentConfig, null, 2))
|
||||
.then(() => console.log('Configuration copied to clipboard'))
|
||||
.catch(err => console.log('Failed to copy: ' + err.message));
|
||||
}
|
||||
});
|
||||
|
||||
editConfigBtn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
enterEditMode();
|
||||
});
|
||||
|
||||
saveConfigBtn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
saveConfiguration().catch(error => {
|
||||
console.log('Save configuration failed: ' + error.message);
|
||||
});
|
||||
});
|
||||
|
||||
cancelEditBtn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
exitEditMode();
|
||||
});
|
||||
|
||||
// Relay connection event handlers
|
||||
connectRelayBtn.addEventListener('click', function (e) {
|
||||
@@ -1664,22 +1670,12 @@
|
||||
disconnectFromRelay();
|
||||
});
|
||||
|
||||
testWebSocketBtn.addEventListener('click', function (e) {
|
||||
restartRelayBtn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
if (!url) {
|
||||
log('Please enter a relay URL first', 'ERROR');
|
||||
return;
|
||||
}
|
||||
|
||||
testWebSocketConnection(url)
|
||||
.then(() => {
|
||||
log('WebSocket test successful', 'INFO');
|
||||
})
|
||||
.catch(error => {
|
||||
log(`WebSocket test failed: ${error.message}`, 'ERROR');
|
||||
});
|
||||
sendRestartCommand().catch(error => {
|
||||
log(`Restart command failed: ${error.message}`, 'ERROR');
|
||||
});
|
||||
});
|
||||
|
||||
// ================================
|
||||
@@ -2116,10 +2112,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Update existing logout and showMainInterface functions to handle auth rules
|
||||
// Update existing logout and showMainInterface functions to handle auth rules and NIP-17 DMs
|
||||
const originalLogout = logout;
|
||||
logout = async function () {
|
||||
hideAuthRulesSection();
|
||||
// Clear DM inbox and outbox on logout
|
||||
if (dmInbox) {
|
||||
dmInbox.innerHTML = '<div class="log-entry">No messages received yet.</div>';
|
||||
}
|
||||
if (dmOutbox) {
|
||||
dmOutbox.value = '';
|
||||
}
|
||||
await originalLogout();
|
||||
};
|
||||
|
||||
@@ -2936,6 +2939,160 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Send NIP-17 Direct Message to relay using NIP-59 layering
|
||||
async function sendNIP17DM() {
|
||||
if (!isLoggedIn || !userPubkey) {
|
||||
log('Must be logged in to send DM', 'ERROR');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isRelayConnected || !relayPubkey) {
|
||||
log('Must be connected to relay to send DM', 'ERROR');
|
||||
return;
|
||||
}
|
||||
|
||||
const message = dmOutbox.value.trim();
|
||||
if (!message) {
|
||||
log('Please enter a message to send', 'ERROR');
|
||||
return;
|
||||
}
|
||||
|
||||
// Capability checks
|
||||
if (!window.nostr || !window.nostr.nip44 || !window.nostr.signEvent) {
|
||||
log('NIP-17 DMs require a NIP-07 extension with NIP-44 support', 'ERROR');
|
||||
alert('NIP-17 DMs require a NIP-07 extension with NIP-44 support. Please install and configure a compatible extension.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.NostrTools || !window.NostrTools.generateSecretKey || !window.NostrTools.getPublicKey || !window.NostrTools.finalizeEvent) {
|
||||
log('NostrTools library not available for ephemeral key operations', 'ERROR');
|
||||
alert('NostrTools library not available. Please ensure nostr.bundle.js is loaded.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log(`Sending NIP-17 DM to relay: ${message.substring(0, 50)}...`, 'INFO');
|
||||
|
||||
// Step 1: Build unsigned rumor (kind 14)
|
||||
const rumor = {
|
||||
kind: 14,
|
||||
pubkey: userPubkey,
|
||||
created_at: Math.floor(Date.now() / 1000), // Canonical time for rumor
|
||||
tags: [["p", relayPubkey]],
|
||||
content: message
|
||||
};
|
||||
// NOTE: Rumor remains unsigned per NIP-59
|
||||
|
||||
log('Rumor built (unsigned), creating seal...', 'INFO');
|
||||
|
||||
// Step 2: Create seal (kind 13)
|
||||
const seal = {
|
||||
kind: 13,
|
||||
pubkey: userPubkey,
|
||||
created_at: randomNow(), // Randomized to past for metadata protection
|
||||
tags: [], // Empty tags per NIP-59
|
||||
content: await window.nostr.nip44.encrypt(relayPubkey, JSON.stringify(rumor))
|
||||
};
|
||||
|
||||
// Sign seal with long-term key
|
||||
const signedSeal = await window.nostr.signEvent(seal);
|
||||
if (!signedSeal || !signedSeal.sig) {
|
||||
throw new Error('Failed to sign seal event');
|
||||
}
|
||||
|
||||
log('Seal created and signed, creating gift wrap...', 'INFO');
|
||||
|
||||
// Step 3: Create gift wrap (kind 1059) with ephemeral key
|
||||
const ephemeralPriv = window.NostrTools.generateSecretKey();
|
||||
const ephemeralPub = window.NostrTools.getPublicKey(ephemeralPriv);
|
||||
|
||||
const giftWrap = {
|
||||
kind: 1059,
|
||||
pubkey: ephemeralPub,
|
||||
created_at: randomNow(), // Randomized to past for metadata protection
|
||||
tags: [["p", relayPubkey]],
|
||||
content: await window.NostrTools.nip44.encrypt(
|
||||
JSON.stringify(signedSeal),
|
||||
window.NostrTools.nip44.getConversationKey(ephemeralPriv, relayPubkey)
|
||||
)
|
||||
};
|
||||
|
||||
// Sign gift wrap with ephemeral key using finalizeEvent
|
||||
const signedGiftWrap = window.NostrTools.finalizeEvent(giftWrap, ephemeralPriv);
|
||||
if (!signedGiftWrap || !signedGiftWrap.sig) {
|
||||
throw new Error('Failed to sign gift wrap event');
|
||||
}
|
||||
|
||||
log('NIP-17 DM event created and signed with ephemeral key, publishing...', 'INFO');
|
||||
|
||||
// Publish via SimplePool
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const publishPromises = relayPool.publish([url], signedGiftWrap);
|
||||
|
||||
// Use Promise.allSettled to capture per-relay outcomes
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
|
||||
// Log detailed publish results
|
||||
let successCount = 0;
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
successCount++;
|
||||
log(`✅ NIP-17 DM published successfully to relay ${index}`, 'INFO');
|
||||
} else {
|
||||
log(`❌ NIP-17 DM failed on relay ${index}: ${result.reason?.message || result.reason}`, 'ERROR');
|
||||
}
|
||||
});
|
||||
|
||||
if (successCount === 0) {
|
||||
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
|
||||
throw new Error(`All relays rejected NIP-17 DM event. Details: ${errorDetails}`);
|
||||
}
|
||||
|
||||
// Clear the outbox and show success
|
||||
dmOutbox.value = '';
|
||||
log('NIP-17 DM sent successfully', 'INFO');
|
||||
|
||||
// Add to inbox for display
|
||||
addMessageToInbox('sent', message, new Date().toLocaleString());
|
||||
|
||||
} catch (error) {
|
||||
log(`Failed to send NIP-17 DM: ${error.message}`, 'ERROR');
|
||||
}
|
||||
}
|
||||
|
||||
// Add message to inbox display
|
||||
function addMessageToInbox(direction, message, timestamp) {
|
||||
if (!dmInbox) return;
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'log-entry';
|
||||
|
||||
const directionColor = direction === 'sent' ? '#007bff' : '#28a745';
|
||||
|
||||
// Convert newlines to <br> tags for proper HTML display
|
||||
const formattedMessage = message.replace(/\n/g, '<br>');
|
||||
|
||||
messageDiv.innerHTML = `
|
||||
<span class="log-timestamp">${timestamp}</span>
|
||||
<span style="color: ${directionColor}; font-weight: bold;">[${direction.toUpperCase()}]</span>
|
||||
<span style="white-space: pre-wrap;">${formattedMessage}</span>
|
||||
`;
|
||||
|
||||
// Remove the "No messages received yet" placeholder if it exists
|
||||
const placeholder = dmInbox.querySelector('.log-entry');
|
||||
if (placeholder && placeholder.textContent === 'No messages received yet.') {
|
||||
dmInbox.innerHTML = '';
|
||||
}
|
||||
|
||||
// Add new message at the top
|
||||
dmInbox.insertBefore(messageDiv, dmInbox.firstChild);
|
||||
|
||||
// Limit to last 50 messages
|
||||
while (dmInbox.children.length > 50) {
|
||||
dmInbox.removeChild(dmInbox.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get relay pubkey
|
||||
function getRelayPubkey() {
|
||||
// Use the dynamically fetched relay pubkey if available
|
||||
@@ -2981,6 +3138,83 @@
|
||||
// DATABASE STATISTICS FUNCTIONS
|
||||
// ================================
|
||||
|
||||
// Send restart command to restart the relay using Administrator API
|
||||
async function sendRestartCommand() {
|
||||
if (!isLoggedIn || !userPubkey) {
|
||||
log('Must be logged in to restart relay', 'ERROR');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!relayPool) {
|
||||
log('SimplePool connection not available', 'ERROR');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log('Sending restart command to relay...', 'INFO');
|
||||
|
||||
// Create command array for restart
|
||||
const command_array = ["system_command", "restart"];
|
||||
|
||||
// Encrypt the command array directly using NIP-44
|
||||
const encrypted_content = await encryptForRelay(JSON.stringify(command_array));
|
||||
if (!encrypted_content) {
|
||||
throw new Error('Failed to encrypt command array');
|
||||
}
|
||||
|
||||
// Create single kind 23456 admin event
|
||||
const restartEvent = {
|
||||
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(restartEvent);
|
||||
if (!signedEvent || !signedEvent.sig) {
|
||||
throw new Error('Event signing failed');
|
||||
}
|
||||
|
||||
// Publish via SimplePool
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const publishPromises = relayPool.publish([url], signedEvent);
|
||||
|
||||
// Use Promise.allSettled to capture per-relay outcomes
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
|
||||
// Check if any relay accepted the event
|
||||
let successCount = 0;
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
successCount++;
|
||||
log(`Restart command published successfully to relay ${index}`, 'INFO');
|
||||
} else {
|
||||
log(`Restart command failed on relay ${index}: ${result.reason?.message || result.reason}`, 'ERROR');
|
||||
}
|
||||
});
|
||||
|
||||
if (successCount === 0) {
|
||||
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
|
||||
throw new Error(`All relays rejected restart command. Details: ${errorDetails}`);
|
||||
}
|
||||
|
||||
log('Restart command sent successfully - relay should restart shortly...', 'INFO');
|
||||
|
||||
// Update connection status to indicate restart is in progress
|
||||
updateRelayConnectionStatus('connecting');
|
||||
relayConnectionStatus.textContent = 'RESTARTING...';
|
||||
|
||||
// The relay will disconnect and need to be reconnected after restart
|
||||
// This will be handled by the WebSocket disconnection event
|
||||
|
||||
} catch (error) {
|
||||
log(`Failed to send restart command: ${error.message}`, 'ERROR');
|
||||
updateRelayConnectionStatus('error');
|
||||
}
|
||||
}
|
||||
|
||||
// Send stats_query command to get database statistics using Administrator API (inner events)
|
||||
async function sendStatsQuery() {
|
||||
if (!isLoggedIn || !userPubkey) {
|
||||
@@ -3137,9 +3371,12 @@
|
||||
const events7d = document.getElementById('events-7d');
|
||||
const events30d = document.getElementById('events-30d');
|
||||
|
||||
if (events24h) events24h.textContent = data.events_24h || '-';
|
||||
if (events7d) events7d.textContent = data.events_7d || '-';
|
||||
if (events30d) events30d.textContent = data.events_30d || '-';
|
||||
// Access the nested time_stats object from backend response
|
||||
const timeStats = data.time_stats || {};
|
||||
|
||||
if (events24h) events24h.textContent = timeStats.last_24h || '0';
|
||||
if (events7d) events7d.textContent = timeStats.last_7d || '0';
|
||||
if (events30d) events30d.textContent = timeStats.last_30d || '0';
|
||||
}
|
||||
|
||||
// Populate top pubkeys table
|
||||
@@ -3251,6 +3488,11 @@
|
||||
if (refreshStatsBtn) {
|
||||
refreshStatsBtn.addEventListener('click', sendStatsQuery);
|
||||
}
|
||||
|
||||
// NIP-17 DM event handlers
|
||||
if (sendDmBtn) {
|
||||
sendDmBtn.addEventListener('click', sendNIP17DM);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize the app
|
||||
|
||||
@@ -159,6 +159,10 @@ fi
|
||||
rm -rf dev-config/ 2>/dev/null
|
||||
rm -f db/c_nostr_relay.db* 2>/dev/null
|
||||
|
||||
# Embed web files into C headers before building
|
||||
echo "Embedding web files..."
|
||||
./embed_web_files.sh
|
||||
|
||||
# Build the project first
|
||||
echo "Building project..."
|
||||
make clean all
|
||||
|
||||
440
src/api.c
440
src/api.c
@@ -9,18 +9,8 @@
|
||||
#include <libwebsockets.h>
|
||||
#include "api.h"
|
||||
#include "embedded_web_content.h"
|
||||
#include "../nostr_core_lib/nostr_core/nip017.h"
|
||||
#include "../nostr_core_lib/nostr_core/nip044.h"
|
||||
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
#include "config.h"
|
||||
|
||||
// Forward declarations for event creation and signing
|
||||
cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags,
|
||||
const unsigned char* privkey_bytes, time_t created_at);
|
||||
|
||||
// Forward declaration for stats generation
|
||||
char* generate_stats_json(void);
|
||||
|
||||
|
||||
// Forward declarations for logging functions
|
||||
void log_info(const char* message);
|
||||
@@ -178,433 +168,3 @@ int handle_embedded_file_writeable(struct lws* wsi) {
|
||||
log_success("Embedded file served successfully");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// NIP-17 GIFT WRAP ADMIN MESSAGING FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
// Check if an event is a NIP-17 gift wrap addressed to this relay
|
||||
int is_nip17_gift_wrap_for_relay(cJSON* event) {
|
||||
if (!event || !cJSON_IsObject(event)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check kind
|
||||
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||
if (!kind_obj || !cJSON_IsNumber(kind_obj) || (int)cJSON_GetNumberValue(kind_obj) != 1059) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check tags for "p" tag with relay pubkey
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* relay_pubkey = get_relay_pubkey_cached();
|
||||
if (!relay_pubkey) {
|
||||
log_error("NIP-17: Could not get relay pubkey for validation");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Look for "p" tag with relay pubkey
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags) {
|
||||
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
|
||||
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
|
||||
|
||||
if (tag_name && cJSON_IsString(tag_name) &&
|
||||
strcmp(cJSON_GetStringValue(tag_name), "p") == 0 &&
|
||||
tag_value && cJSON_IsString(tag_value) &&
|
||||
strcmp(cJSON_GetStringValue(tag_value), relay_pubkey) == 0) {
|
||||
return 1; // Found matching p tag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // No matching p tag found
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Process NIP-17 admin command from decrypted DM content
|
||||
int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t error_size, struct lws* wsi) {
|
||||
if (!dm_event || !error_message) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Extract content from DM
|
||||
cJSON* content_obj = cJSON_GetObjectItem(dm_event, "content");
|
||||
if (!content_obj || !cJSON_IsString(content_obj)) {
|
||||
strncpy(error_message, "NIP-17: DM missing content", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* dm_content = cJSON_GetStringValue(content_obj);
|
||||
log_info("NIP-17: Processing admin command from DM content");
|
||||
|
||||
// Parse DM content as JSON array of commands
|
||||
cJSON* command_array = cJSON_Parse(dm_content);
|
||||
if (!command_array || !cJSON_IsArray(command_array)) {
|
||||
strncpy(error_message, "NIP-17: DM content is not valid JSON array", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check if this is a "stats" command
|
||||
if (cJSON_GetArraySize(command_array) > 0) {
|
||||
cJSON* first_item = cJSON_GetArrayItem(command_array, 0);
|
||||
if (cJSON_IsString(first_item) && strcmp(cJSON_GetStringValue(first_item), "stats") == 0) {
|
||||
log_info("NIP-17: Processing 'stats' command directly");
|
||||
|
||||
// Generate stats JSON
|
||||
char* stats_json = generate_stats_json();
|
||||
if (!stats_json) {
|
||||
cJSON_Delete(command_array);
|
||||
strncpy(error_message, "NIP-17: Failed to generate stats", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Get sender pubkey for response
|
||||
cJSON* sender_pubkey_obj = cJSON_GetObjectItem(dm_event, "pubkey");
|
||||
if (!sender_pubkey_obj || !cJSON_IsString(sender_pubkey_obj)) {
|
||||
free(stats_json);
|
||||
cJSON_Delete(command_array);
|
||||
strncpy(error_message, "NIP-17: DM missing sender pubkey", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
const char* sender_pubkey = cJSON_GetStringValue(sender_pubkey_obj);
|
||||
|
||||
// Get relay keys for signing
|
||||
const char* relay_pubkey = get_relay_pubkey_cached();
|
||||
char* relay_privkey_hex = get_relay_private_key();
|
||||
if (!relay_pubkey || !relay_privkey_hex) {
|
||||
free(stats_json);
|
||||
cJSON_Delete(command_array);
|
||||
if (relay_privkey_hex) free(relay_privkey_hex);
|
||||
strncpy(error_message, "NIP-17: Could not get relay keys", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Convert relay private key to bytes
|
||||
unsigned char relay_privkey[32];
|
||||
if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) {
|
||||
free(stats_json);
|
||||
free(relay_privkey_hex);
|
||||
cJSON_Delete(command_array);
|
||||
strncpy(error_message, "NIP-17: Failed to convert relay private key", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
free(relay_privkey_hex);
|
||||
|
||||
// Create DM response event using library function
|
||||
cJSON* dm_response = nostr_nip17_create_chat_event(
|
||||
stats_json, // message content
|
||||
(const char**)&sender_pubkey, // recipient pubkeys
|
||||
1, // num recipients
|
||||
NULL, // subject (optional)
|
||||
NULL, // reply_to_event_id (optional)
|
||||
NULL, // reply_relay_url (optional)
|
||||
relay_pubkey // sender pubkey
|
||||
);
|
||||
|
||||
free(stats_json);
|
||||
|
||||
if (!dm_response) {
|
||||
cJSON_Delete(command_array);
|
||||
strncpy(error_message, "NIP-17: Failed to create DM response event", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Create and sign gift wrap using library function
|
||||
cJSON* gift_wraps[1];
|
||||
int send_result = nostr_nip17_send_dm(
|
||||
dm_response, // dm_event
|
||||
(const char**)&sender_pubkey, // recipient_pubkeys
|
||||
1, // num_recipients
|
||||
relay_privkey, // sender_private_key
|
||||
gift_wraps, // gift_wraps_out
|
||||
1 // max_gift_wraps
|
||||
);
|
||||
|
||||
cJSON_Delete(dm_response);
|
||||
|
||||
if (send_result != 1 || !gift_wraps[0]) {
|
||||
cJSON_Delete(command_array);
|
||||
strncpy(error_message, "NIP-17: Failed to create and sign response gift wrap", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Store the gift wrap in database
|
||||
int store_result = store_event(gift_wraps[0]);
|
||||
cJSON_Delete(gift_wraps[0]);
|
||||
|
||||
if (store_result != 0) {
|
||||
cJSON_Delete(command_array);
|
||||
strncpy(error_message, "NIP-17: Failed to store response gift wrap", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_Delete(command_array);
|
||||
log_success("NIP-17: Stats command processed successfully");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// For other commands, delegate to existing admin processing
|
||||
// Create a synthetic kind 23456 event with the DM content
|
||||
cJSON* synthetic_event = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(synthetic_event, "kind", 23456);
|
||||
cJSON_AddStringToObject(synthetic_event, "content", dm_content);
|
||||
|
||||
// Copy pubkey from DM
|
||||
cJSON* pubkey_obj = cJSON_GetObjectItem(dm_event, "pubkey");
|
||||
if (pubkey_obj && cJSON_IsString(pubkey_obj)) {
|
||||
cJSON_AddStringToObject(synthetic_event, "pubkey", cJSON_GetStringValue(pubkey_obj));
|
||||
}
|
||||
|
||||
// Copy tags from DM
|
||||
cJSON* tags = cJSON_GetObjectItem(dm_event, "tags");
|
||||
if (tags) {
|
||||
cJSON_AddItemToObject(synthetic_event, "tags", cJSON_Duplicate(tags, 1));
|
||||
}
|
||||
|
||||
// Process as regular admin event
|
||||
int result = process_admin_event_in_config(synthetic_event, error_message, error_size, wsi);
|
||||
|
||||
cJSON_Delete(synthetic_event);
|
||||
cJSON_Delete(command_array);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Generate stats JSON from database queries
|
||||
char* generate_stats_json(void) {
|
||||
extern sqlite3* g_db;
|
||||
if (!g_db) {
|
||||
log_error("Database not available for stats generation");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
log_info("Generating stats JSON from database");
|
||||
|
||||
// Build response with database statistics
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(response, "query_type", "stats_query");
|
||||
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
|
||||
|
||||
// Get database file size
|
||||
extern char g_database_path[512];
|
||||
struct stat db_stat;
|
||||
long long db_size = 0;
|
||||
if (stat(g_database_path, &db_stat) == 0) {
|
||||
db_size = db_stat.st_size;
|
||||
}
|
||||
cJSON_AddNumberToObject(response, "database_size_bytes", db_size);
|
||||
|
||||
// Query total events count
|
||||
sqlite3_stmt* stmt;
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM events", -1, &stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
cJSON_AddNumberToObject(response, "total_events", sqlite3_column_int64(stmt, 0));
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
// Query event kinds distribution
|
||||
cJSON* event_kinds = cJSON_CreateArray();
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT kind, count, percentage FROM event_kinds_view ORDER BY count DESC", -1, &stmt, NULL) == SQLITE_OK) {
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
cJSON* kind_obj = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(kind_obj, "kind", sqlite3_column_int(stmt, 0));
|
||||
cJSON_AddNumberToObject(kind_obj, "count", sqlite3_column_int64(stmt, 1));
|
||||
cJSON_AddNumberToObject(kind_obj, "percentage", sqlite3_column_double(stmt, 2));
|
||||
cJSON_AddItemToArray(event_kinds, kind_obj);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
cJSON_AddItemToObject(response, "event_kinds", event_kinds);
|
||||
|
||||
// Query time-based statistics
|
||||
cJSON* time_stats = cJSON_CreateObject();
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT period, total_events FROM time_stats_view", -1, &stmt, NULL) == SQLITE_OK) {
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
const char* period = (const char*)sqlite3_column_text(stmt, 0);
|
||||
sqlite3_int64 count = sqlite3_column_int64(stmt, 1);
|
||||
|
||||
if (strcmp(period, "total") == 0) {
|
||||
cJSON_AddNumberToObject(time_stats, "total", count);
|
||||
} else if (strcmp(period, "24h") == 0) {
|
||||
cJSON_AddNumberToObject(time_stats, "last_24h", count);
|
||||
} else if (strcmp(period, "7d") == 0) {
|
||||
cJSON_AddNumberToObject(time_stats, "last_7d", count);
|
||||
} else if (strcmp(period, "30d") == 0) {
|
||||
cJSON_AddNumberToObject(time_stats, "last_30d", count);
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
cJSON_AddItemToObject(response, "time_stats", time_stats);
|
||||
|
||||
// Query top pubkeys
|
||||
cJSON* top_pubkeys = cJSON_CreateArray();
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT pubkey, event_count, percentage FROM top_pubkeys_view ORDER BY event_count DESC LIMIT 10", -1, &stmt, NULL) == SQLITE_OK) {
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
cJSON* pubkey_obj = cJSON_CreateObject();
|
||||
const char* pubkey = (const char*)sqlite3_column_text(stmt, 0);
|
||||
cJSON_AddStringToObject(pubkey_obj, "pubkey", pubkey ? pubkey : "");
|
||||
cJSON_AddNumberToObject(pubkey_obj, "event_count", sqlite3_column_int64(stmt, 1));
|
||||
cJSON_AddNumberToObject(pubkey_obj, "percentage", sqlite3_column_double(stmt, 2));
|
||||
cJSON_AddItemToArray(top_pubkeys, pubkey_obj);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
cJSON_AddItemToObject(response, "top_pubkeys", top_pubkeys);
|
||||
|
||||
// Get database creation timestamp (oldest event)
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT MIN(created_at) FROM events", -1, &stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
sqlite3_int64 oldest_timestamp = sqlite3_column_int64(stmt, 0);
|
||||
if (oldest_timestamp > 0) {
|
||||
cJSON_AddNumberToObject(response, "database_created_at", (double)oldest_timestamp);
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
// Get latest event timestamp
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT MAX(created_at) FROM events", -1, &stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
sqlite3_int64 latest_timestamp = sqlite3_column_int64(stmt, 0);
|
||||
if (latest_timestamp > 0) {
|
||||
cJSON_AddNumberToObject(response, "latest_event_at", (double)latest_timestamp);
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
// Convert to JSON string
|
||||
char* json_string = cJSON_Print(response);
|
||||
cJSON_Delete(response);
|
||||
|
||||
if (json_string) {
|
||||
log_success("Stats JSON generated successfully");
|
||||
} else {
|
||||
log_error("Failed to generate stats JSON");
|
||||
}
|
||||
|
||||
return json_string;
|
||||
}
|
||||
|
||||
// Main NIP-17 processing function
|
||||
int process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message, size_t error_size, struct lws* wsi) {
|
||||
if (!gift_wrap_event || !error_message) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Step 1: Validate it's addressed to us
|
||||
if (!is_nip17_gift_wrap_for_relay(gift_wrap_event)) {
|
||||
strncpy(error_message, "NIP-17: Event is not a valid gift wrap for this relay", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Step 2: Get relay private key for decryption
|
||||
char* relay_privkey_hex = get_relay_private_key();
|
||||
if (!relay_privkey_hex) {
|
||||
strncpy(error_message, "NIP-17: Could not get relay private key for decryption", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Convert hex private key to bytes
|
||||
unsigned char relay_privkey[32];
|
||||
if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) {
|
||||
log_error("NIP-17: Failed to convert relay private key from hex");
|
||||
free(relay_privkey_hex);
|
||||
strncpy(error_message, "NIP-17: Failed to convert relay private key", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
free(relay_privkey_hex);
|
||||
|
||||
// Step 3: Decrypt and parse inner event using library function
|
||||
log_info("NIP-17: Attempting to decrypt gift wrap with nostr_nip17_receive_dm");
|
||||
cJSON* inner_dm = nostr_nip17_receive_dm(gift_wrap_event, relay_privkey);
|
||||
if (!inner_dm) {
|
||||
log_error("NIP-17: nostr_nip17_receive_dm returned NULL");
|
||||
// Debug: Print the gift wrap event
|
||||
char* gift_wrap_debug = cJSON_Print(gift_wrap_event);
|
||||
if (gift_wrap_debug) {
|
||||
char debug_msg[1024];
|
||||
snprintf(debug_msg, sizeof(debug_msg), "NIP-17: Gift wrap event: %.500s", gift_wrap_debug);
|
||||
log_error(debug_msg);
|
||||
free(gift_wrap_debug);
|
||||
}
|
||||
// Debug: Check if private key is valid
|
||||
char privkey_hex[65];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
sprintf(privkey_hex + (i * 2), "%02x", relay_privkey[i]);
|
||||
}
|
||||
privkey_hex[64] = '\0';
|
||||
char privkey_msg[128];
|
||||
snprintf(privkey_msg, sizeof(privkey_msg), "NIP-17: Using relay private key: %.16s...", privkey_hex);
|
||||
log_info(privkey_msg);
|
||||
|
||||
strncpy(error_message, "NIP-17: Failed to decrypt and parse inner DM event", error_size - 1);
|
||||
return -1;
|
||||
}
|
||||
log_info("NIP-17: Successfully decrypted gift wrap");
|
||||
|
||||
// Step 4: Process admin command
|
||||
int result = process_nip17_admin_command(inner_dm, error_message, error_size, wsi);
|
||||
|
||||
// Step 5: Create response if command was processed successfully
|
||||
if (result == 0) {
|
||||
// Get sender pubkey for response
|
||||
cJSON* sender_pubkey_obj = cJSON_GetObjectItem(gift_wrap_event, "pubkey");
|
||||
if (sender_pubkey_obj && cJSON_IsString(sender_pubkey_obj)) {
|
||||
const char* sender_pubkey = cJSON_GetStringValue(sender_pubkey_obj);
|
||||
|
||||
// Create success response using library function
|
||||
char response_content[1024];
|
||||
snprintf(response_content, sizeof(response_content),
|
||||
"[\"command_processed\", \"success\", \"%s\"]", "NIP-17 admin command executed");
|
||||
|
||||
// Get relay pubkey for creating DM event
|
||||
const char* relay_pubkey = get_relay_pubkey_cached();
|
||||
if (relay_pubkey) {
|
||||
cJSON* success_dm = nostr_nip17_create_chat_event(
|
||||
response_content, // message content
|
||||
(const char**)&sender_pubkey, // recipient pubkeys
|
||||
1, // num recipients
|
||||
NULL, // subject (optional)
|
||||
NULL, // reply_to_event_id (optional)
|
||||
NULL, // reply_relay_url (optional)
|
||||
relay_pubkey // sender pubkey
|
||||
);
|
||||
|
||||
if (success_dm) {
|
||||
cJSON* success_gift_wraps[1];
|
||||
int send_result = nostr_nip17_send_dm(
|
||||
success_dm, // dm_event
|
||||
(const char**)&sender_pubkey, // recipient_pubkeys
|
||||
1, // num_recipients
|
||||
relay_privkey, // sender_private_key
|
||||
success_gift_wraps, // gift_wraps_out
|
||||
1 // max_gift_wraps
|
||||
);
|
||||
|
||||
cJSON_Delete(success_dm);
|
||||
|
||||
if (send_result == 1 && success_gift_wraps[0]) {
|
||||
store_event(success_gift_wraps[0]);
|
||||
cJSON_Delete(success_gift_wraps[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(inner_dm);
|
||||
return result;
|
||||
}
|
||||
63
src/config.c
63
src/config.c
@@ -16,6 +16,9 @@
|
||||
// External database connection (from main.c)
|
||||
extern sqlite3* g_db;
|
||||
|
||||
// External shutdown flag (from main.c)
|
||||
extern volatile sig_atomic_t g_shutdown_flag;
|
||||
|
||||
// Global unified configuration cache instance
|
||||
unified_config_cache_t g_unified_cache = {
|
||||
.cache_lock = PTHREAD_MUTEX_INITIALIZER,
|
||||
@@ -3673,19 +3676,19 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(response, "command", "system_status");
|
||||
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
|
||||
|
||||
|
||||
cJSON* status_data = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(status_data, "database", g_db ? "connected" : "not_available");
|
||||
cJSON_AddStringToObject(status_data, "cache_status", g_unified_cache.cache_valid ? "valid" : "invalid");
|
||||
|
||||
|
||||
if (strlen(g_database_path) > 0) {
|
||||
cJSON_AddStringToObject(status_data, "database_path", g_database_path);
|
||||
}
|
||||
|
||||
|
||||
// Count configuration items and auth rules
|
||||
if (g_db) {
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
|
||||
// Config count
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
@@ -3693,7 +3696,7 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
|
||||
// Auth rules count
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM auth_rules", -1, &stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
@@ -3702,34 +3705,72 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
cJSON_AddItemToObject(response, "data", status_data);
|
||||
|
||||
|
||||
printf("=== System Status ===\n");
|
||||
printf("Database: %s\n", g_db ? "Connected" : "Not available");
|
||||
printf("Cache status: %s\n", g_unified_cache.cache_valid ? "Valid" : "Invalid");
|
||||
|
||||
|
||||
// Get admin pubkey from event for response
|
||||
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
|
||||
const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL;
|
||||
|
||||
|
||||
if (!admin_pubkey) {
|
||||
cJSON_Delete(response);
|
||||
snprintf(error_message, error_size, "missing admin pubkey for response");
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
// Send response as signed kind 23457 event
|
||||
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
|
||||
log_success("System status query completed successfully with signed response");
|
||||
cJSON_Delete(response);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
cJSON_Delete(response);
|
||||
snprintf(error_message, error_size, "failed to send system status response");
|
||||
return -1;
|
||||
}
|
||||
else if (strcmp(command, "restart") == 0) {
|
||||
// Build restart acknowledgment response
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(response, "command", "restart");
|
||||
cJSON_AddStringToObject(response, "status", "initiating_restart");
|
||||
cJSON_AddStringToObject(response, "message", "Relay restart initiated - shutting down gracefully");
|
||||
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
|
||||
|
||||
printf("=== Relay Restart Initiated ===\n");
|
||||
printf("Admin requested system restart\n");
|
||||
printf("Sending acknowledgment and initiating shutdown...\n");
|
||||
|
||||
// Get admin pubkey from event for response
|
||||
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
|
||||
const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL;
|
||||
|
||||
if (!admin_pubkey) {
|
||||
cJSON_Delete(response);
|
||||
snprintf(error_message, error_size, "missing admin pubkey for response");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Send acknowledgment response as signed kind 23457 event
|
||||
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
|
||||
log_success("Restart acknowledgment sent successfully - initiating shutdown");
|
||||
|
||||
// Trigger graceful shutdown by setting the global shutdown flag
|
||||
g_shutdown_flag = 1;
|
||||
log_info("Shutdown flag set - relay will restart gracefully");
|
||||
|
||||
cJSON_Delete(response);
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON_Delete(response);
|
||||
snprintf(error_message, error_size, "failed to send restart acknowledgment");
|
||||
return -1;
|
||||
}
|
||||
else {
|
||||
snprintf(error_message, error_size, "invalid: unknown system command '%s'", command);
|
||||
return -1;
|
||||
|
||||
1620
src/dm_admin.c
1620
src/dm_admin.c
File diff suppressed because it is too large
Load Diff
@@ -12,4 +12,16 @@ int process_dm_admin_command(cJSON* command_array, cJSON* event, char* error_mes
|
||||
// Returns 1 if it's a valid command array, 0 if it should fall back to inner event parsing
|
||||
int is_dm_command_array(const char* decrypted_content);
|
||||
|
||||
// NIP-17 gift wrap processing functions
|
||||
cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
int is_nip17_gift_wrap_for_relay(cJSON* gift_wrap_event);
|
||||
char* generate_stats_json(void);
|
||||
|
||||
// Unified NIP-17 response functions
|
||||
int send_nip17_response(const char* sender_pubkey, const char* response_content,
|
||||
char* error_message, size_t error_size);
|
||||
char* generate_config_text(void);
|
||||
char* generate_stats_text(void);
|
||||
|
||||
#endif // DM_ADMIN_H
|
||||
File diff suppressed because one or more lines are too long
@@ -45,6 +45,8 @@ int nostr_nip42_verify_auth_event(cJSON *event, const char *challenge_id,
|
||||
// Global state
|
||||
sqlite3* g_db = NULL; // Non-static so config.c can access it
|
||||
int g_server_running = 1; // Non-static so websockets.c can access it
|
||||
volatile sig_atomic_t g_shutdown_flag = 0; // Non-static so config.c can access it for restart functionality
|
||||
int g_restart_requested = 0; // Non-static so config.c can access it for restart functionality
|
||||
struct lws_context *ws_context = NULL; // Non-static so websockets.c can access it
|
||||
|
||||
// NIP-11 relay information structure
|
||||
|
||||
@@ -269,26 +269,39 @@ int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
|
||||
if (!event || !filter) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// Check kinds filter
|
||||
if (filter->kinds && cJSON_IsArray(filter->kinds)) {
|
||||
cJSON* event_kind = cJSON_GetObjectItem(event, "kind");
|
||||
if (!event_kind || !cJSON_IsNumber(event_kind)) {
|
||||
log_info("DEBUG FILTER: kinds filter present but event has no valid kind");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int event_kind_val = (int)cJSON_GetNumberValue(event_kind);
|
||||
int kind_match = 0;
|
||||
|
||||
|
||||
char debug_kinds_msg[256];
|
||||
snprintf(debug_kinds_msg, sizeof(debug_kinds_msg), "DEBUG FILTER: Checking kinds - event kind: %d", event_kind_val);
|
||||
log_info(debug_kinds_msg);
|
||||
|
||||
cJSON* kind_item = NULL;
|
||||
cJSON_ArrayForEach(kind_item, filter->kinds) {
|
||||
if (cJSON_IsNumber(kind_item) && (int)cJSON_GetNumberValue(kind_item) == event_kind_val) {
|
||||
kind_match = 1;
|
||||
break;
|
||||
if (cJSON_IsNumber(kind_item)) {
|
||||
int filter_kind = (int)cJSON_GetNumberValue(kind_item);
|
||||
char debug_kind_check_msg[256];
|
||||
snprintf(debug_kind_check_msg, sizeof(debug_kind_check_msg), "DEBUG FILTER: Comparing event kind %d with filter kind %d", event_kind_val, filter_kind);
|
||||
log_info(debug_kind_check_msg);
|
||||
if (filter_kind == event_kind_val) {
|
||||
kind_match = 1;
|
||||
log_info("DEBUG FILTER: Kind match found!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!kind_match) {
|
||||
log_info("DEBUG FILTER: No kind match - filter fails");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -377,62 +390,87 @@ int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
|
||||
if (filter->tag_filters && cJSON_IsObject(filter->tag_filters)) {
|
||||
cJSON* event_tags = cJSON_GetObjectItem(event, "tags");
|
||||
if (!event_tags || !cJSON_IsArray(event_tags)) {
|
||||
log_info("DEBUG FILTER: tag filters present but event has no valid tags array");
|
||||
return 0; // Event has no tags but filter requires tags
|
||||
}
|
||||
|
||||
|
||||
char debug_tags_msg[256];
|
||||
snprintf(debug_tags_msg, sizeof(debug_tags_msg), "DEBUG FILTER: Checking tag filters - event has %d tags", cJSON_GetArraySize(event_tags));
|
||||
log_info(debug_tags_msg);
|
||||
|
||||
// Check each tag filter
|
||||
cJSON* tag_filter = NULL;
|
||||
cJSON_ArrayForEach(tag_filter, filter->tag_filters) {
|
||||
if (!tag_filter->string || strlen(tag_filter->string) < 2 || tag_filter->string[0] != '#') {
|
||||
continue; // Invalid tag filter
|
||||
}
|
||||
|
||||
|
||||
const char* tag_name = tag_filter->string + 1; // Skip the '#'
|
||||
|
||||
|
||||
char debug_tag_filter_msg[256];
|
||||
snprintf(debug_tag_filter_msg, sizeof(debug_tag_filter_msg), "DEBUG FILTER: Checking tag filter #%s", tag_name);
|
||||
log_info(debug_tag_filter_msg);
|
||||
|
||||
if (!cJSON_IsArray(tag_filter)) {
|
||||
continue; // Tag filter must be an array
|
||||
}
|
||||
|
||||
|
||||
int tag_match = 0;
|
||||
|
||||
|
||||
// Search through event tags for matching tag name and value
|
||||
cJSON* event_tag = NULL;
|
||||
cJSON_ArrayForEach(event_tag, event_tags) {
|
||||
if (!cJSON_IsArray(event_tag) || cJSON_GetArraySize(event_tag) < 2) {
|
||||
continue; // Invalid tag format
|
||||
}
|
||||
|
||||
|
||||
cJSON* event_tag_name = cJSON_GetArrayItem(event_tag, 0);
|
||||
cJSON* event_tag_value = cJSON_GetArrayItem(event_tag, 1);
|
||||
|
||||
|
||||
if (!cJSON_IsString(event_tag_name) || !cJSON_IsString(event_tag_value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const char* event_tag_name_str = cJSON_GetStringValue(event_tag_name);
|
||||
const char* event_tag_value_str = cJSON_GetStringValue(event_tag_value);
|
||||
|
||||
char debug_event_tag_msg[256];
|
||||
snprintf(debug_event_tag_msg, sizeof(debug_event_tag_msg), "DEBUG FILTER: Event tag: %s = %s", event_tag_name_str, event_tag_value_str);
|
||||
log_info(debug_event_tag_msg);
|
||||
|
||||
// Check if tag name matches
|
||||
if (strcmp(cJSON_GetStringValue(event_tag_name), tag_name) == 0) {
|
||||
const char* event_tag_value_str = cJSON_GetStringValue(event_tag_value);
|
||||
|
||||
if (strcmp(event_tag_name_str, tag_name) == 0) {
|
||||
char debug_tag_name_match_msg[256];
|
||||
snprintf(debug_tag_name_match_msg, sizeof(debug_tag_name_match_msg), "DEBUG FILTER: Tag name '%s' matches filter", tag_name);
|
||||
log_info(debug_tag_name_match_msg);
|
||||
|
||||
// Check if any of the filter values match this tag value
|
||||
cJSON* filter_value = NULL;
|
||||
cJSON_ArrayForEach(filter_value, tag_filter) {
|
||||
if (cJSON_IsString(filter_value)) {
|
||||
const char* filter_value_str = cJSON_GetStringValue(filter_value);
|
||||
char debug_filter_value_msg[256];
|
||||
snprintf(debug_filter_value_msg, sizeof(debug_filter_value_msg), "DEBUG FILTER: Comparing event tag value '%s' with filter value '%s'", event_tag_value_str, filter_value_str);
|
||||
log_info(debug_filter_value_msg);
|
||||
// Support prefix matching for tag values
|
||||
if (strncmp(event_tag_value_str, filter_value_str, strlen(filter_value_str)) == 0) {
|
||||
tag_match = 1;
|
||||
log_info("DEBUG FILTER: Tag value match found!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (tag_match) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!tag_match) {
|
||||
char debug_tag_fail_msg[256];
|
||||
snprintf(debug_tag_fail_msg, sizeof(debug_tag_fail_msg), "DEBUG FILTER: Tag filter #%s failed - no match found", tag_name);
|
||||
log_info(debug_tag_fail_msg);
|
||||
return 0; // This tag filter didn't match, so the event doesn't match
|
||||
}
|
||||
}
|
||||
@@ -444,17 +482,40 @@ int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
|
||||
// Check if an event matches any filter in a subscription (filters are OR'd together)
|
||||
int event_matches_subscription(cJSON* event, subscription_t* subscription) {
|
||||
if (!event || !subscription || !subscription->filters) {
|
||||
char debug_msg[256];
|
||||
snprintf(debug_msg, sizeof(debug_msg), "DEBUG MATCH: Subscription '%s' - invalid parameters (event: %p, subscription: %p, filters: %p)",
|
||||
subscription ? subscription->id : "NULL", event, subscription, subscription ? subscription->filters : NULL);
|
||||
log_info(debug_msg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
char debug_start_msg[256];
|
||||
snprintf(debug_start_msg, sizeof(debug_start_msg), "DEBUG MATCH: Checking subscription '%s' filters", subscription->id);
|
||||
log_info(debug_start_msg);
|
||||
|
||||
subscription_filter_t* filter = subscription->filters;
|
||||
int filter_index = 0;
|
||||
while (filter) {
|
||||
if (event_matches_filter(event, filter)) {
|
||||
int filter_matches = event_matches_filter(event, filter);
|
||||
char debug_filter_msg[256];
|
||||
snprintf(debug_filter_msg, sizeof(debug_filter_msg), "DEBUG MATCH: Subscription '%s' filter %d - %s",
|
||||
subscription->id, filter_index, filter_matches ? "MATCHES" : "no match");
|
||||
log_info(debug_filter_msg);
|
||||
|
||||
if (filter_matches) {
|
||||
char debug_match_msg[256];
|
||||
snprintf(debug_match_msg, sizeof(debug_match_msg), "DEBUG MATCH: Subscription '%s' - MATCH FOUND", subscription->id);
|
||||
log_info(debug_match_msg);
|
||||
return 1; // Match found (OR logic)
|
||||
}
|
||||
filter = filter->next;
|
||||
filter_index++;
|
||||
}
|
||||
|
||||
|
||||
char debug_no_match_msg[256];
|
||||
snprintf(debug_no_match_msg, sizeof(debug_no_match_msg), "DEBUG MATCH: Subscription '%s' - NO MATCHES", subscription->id);
|
||||
log_info(debug_no_match_msg);
|
||||
|
||||
return 0; // No filters matched
|
||||
}
|
||||
|
||||
@@ -483,11 +544,28 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
||||
}
|
||||
|
||||
int broadcasts = 0;
|
||||
|
||||
|
||||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||
|
||||
|
||||
// Debug: Log total active subscriptions
|
||||
int total_active = 0;
|
||||
subscription_t* count_sub = g_subscription_manager.active_subscriptions;
|
||||
while (count_sub) {
|
||||
total_active++;
|
||||
count_sub = count_sub->next;
|
||||
}
|
||||
|
||||
char debug_total_msg[128];
|
||||
snprintf(debug_total_msg, sizeof(debug_total_msg), "DEBUG BROADCAST: Broadcasting event to %d total active subscriptions", total_active);
|
||||
log_info(debug_total_msg);
|
||||
|
||||
subscription_t* sub = g_subscription_manager.active_subscriptions;
|
||||
while (sub) {
|
||||
// Debug: Log each subscription being checked
|
||||
char debug_sub_msg[256];
|
||||
snprintf(debug_sub_msg, sizeof(debug_sub_msg), "DEBUG BROADCAST: Checking subscription '%s' (active: %d)", sub->id, sub->active);
|
||||
log_info(debug_sub_msg);
|
||||
|
||||
if (sub->active && event_matches_subscription(event, sub)) {
|
||||
// Create EVENT message for this subscription
|
||||
cJSON* event_msg = cJSON_CreateArray();
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "subscriptions.h" // Subscription structures and functions
|
||||
#include "embedded_web_content.h" // Embedded web content
|
||||
#include "api.h" // API for embedded files
|
||||
#include "dm_admin.h" // DM admin functions including NIP-17
|
||||
|
||||
// Forward declarations for logging functions
|
||||
void log_info(const char* message);
|
||||
@@ -68,9 +69,6 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length);
|
||||
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
int is_authorized_admin_event(cJSON* event, char* error_message, size_t error_size);
|
||||
|
||||
// Forward declarations for NIP-17 admin messaging
|
||||
int process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
|
||||
// Forward declarations for DM stats command handling
|
||||
int process_dm_stats_command(cJSON* dm_event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
|
||||
@@ -97,6 +95,8 @@ extern unified_config_cache_t g_unified_cache;
|
||||
// Forward declarations for global state
|
||||
extern sqlite3* g_db;
|
||||
extern int g_server_running;
|
||||
extern volatile sig_atomic_t g_shutdown_flag;
|
||||
extern int g_restart_requested;
|
||||
extern struct lws_context *ws_context;
|
||||
|
||||
// Global subscription manager
|
||||
@@ -643,35 +643,62 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
log_info("DEBUG NIP17: Detected kind 1059 gift wrap event");
|
||||
|
||||
char nip17_error[512] = {0};
|
||||
int nip17_result = process_nip17_admin_message(event, nip17_error, sizeof(nip17_error), wsi);
|
||||
cJSON* response_event = process_nip17_admin_message(event, nip17_error, sizeof(nip17_error), wsi);
|
||||
|
||||
if (nip17_result != 0) {
|
||||
log_error("DEBUG NIP17: NIP-17 admin message processing failed");
|
||||
result = -1;
|
||||
size_t error_len = strlen(nip17_error);
|
||||
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
|
||||
memcpy(error_message, nip17_error, copy_len);
|
||||
error_message[copy_len] = '\0';
|
||||
if (!response_event) {
|
||||
// Check if this is an error or if the command was already handled
|
||||
if (strlen(nip17_error) > 0) {
|
||||
// There was an actual error
|
||||
log_error("DEBUG NIP17: NIP-17 admin message processing failed");
|
||||
result = -1;
|
||||
size_t error_len = strlen(nip17_error);
|
||||
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
|
||||
memcpy(error_message, nip17_error, copy_len);
|
||||
error_message[copy_len] = '\0';
|
||||
|
||||
char debug_nip17_error_msg[600];
|
||||
snprintf(debug_nip17_error_msg, sizeof(debug_nip17_error_msg),
|
||||
"DEBUG NIP17 ERROR: %.400s", nip17_error);
|
||||
log_error(debug_nip17_error_msg);
|
||||
char debug_nip17_error_msg[600];
|
||||
snprintf(debug_nip17_error_msg, sizeof(debug_nip17_error_msg),
|
||||
"DEBUG NIP17 ERROR: %.400s", nip17_error);
|
||||
log_error(debug_nip17_error_msg);
|
||||
} else {
|
||||
// No error message means the command was already handled (plain text commands)
|
||||
log_success("DEBUG NIP17: NIP-17 admin message processed successfully (already handled)");
|
||||
// Store the original gift wrap event in database
|
||||
if (store_event(event) != 0) {
|
||||
log_error("DEBUG NIP17: Failed to store gift wrap event in database");
|
||||
result = -1;
|
||||
strncpy(error_message, "error: failed to store gift wrap event", sizeof(error_message) - 1);
|
||||
} else {
|
||||
log_info("DEBUG NIP17: Gift wrap event stored successfully in database");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log_success("DEBUG NIP17: NIP-17 admin message processed successfully");
|
||||
// Store the gift wrap event in database (unlike kind 23456)
|
||||
// Store the original gift wrap event in database (unlike kind 23456)
|
||||
if (store_event(event) != 0) {
|
||||
log_error("DEBUG NIP17: Failed to store gift wrap event in database");
|
||||
result = -1;
|
||||
strncpy(error_message, "error: failed to store gift wrap event", sizeof(error_message) - 1);
|
||||
cJSON_Delete(response_event);
|
||||
} else {
|
||||
log_info("DEBUG NIP17: Gift wrap event stored successfully in database");
|
||||
// Broadcast gift wrap event to matching persistent subscriptions
|
||||
int broadcast_count = broadcast_event_to_subscriptions(event);
|
||||
// Debug: Print response event before broadcasting
|
||||
char* debug_before_broadcast = cJSON_Print(response_event);
|
||||
if (debug_before_broadcast) {
|
||||
log_info("DEBUG EVENT: Before broadcasting response event");
|
||||
printf(" Response Event: %s\n", debug_before_broadcast);
|
||||
free(debug_before_broadcast);
|
||||
}
|
||||
|
||||
// Broadcast RESPONSE event to matching persistent subscriptions
|
||||
int broadcast_count = broadcast_event_to_subscriptions(response_event);
|
||||
char debug_broadcast_msg[128];
|
||||
snprintf(debug_broadcast_msg, sizeof(debug_broadcast_msg),
|
||||
"DEBUG NIP17 BROADCAST: Gift wrap event broadcast to %d subscriptions", broadcast_count);
|
||||
"DEBUG NIP17 BROADCAST: Response event broadcast to %d subscriptions", broadcast_count);
|
||||
log_info(debug_broadcast_msg);
|
||||
|
||||
// Clean up response event
|
||||
cJSON_Delete(response_event);
|
||||
}
|
||||
}
|
||||
} else if (event_kind == 14) {
|
||||
@@ -1128,7 +1155,7 @@ int start_websocket_relay(int port_override, int strict_port) {
|
||||
log_success(startup_msg);
|
||||
|
||||
// Main event loop with proper signal handling
|
||||
while (g_server_running) {
|
||||
while (g_server_running && !g_shutdown_flag) {
|
||||
int result = lws_service(ws_context, 1000);
|
||||
|
||||
if (result < 0) {
|
||||
|
||||
Reference in New Issue
Block a user