v0.4.11 - Fixed nasty DM bug
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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> -->
|
||||
|
||||
638
api/index.js
638
api/index.js
@@ -43,14 +43,12 @@
|
||||
const disconnectRelayBtn = document.getElementById('disconnect-relay-btn');
|
||||
const testWebSocketBtn = document.getElementById('test-websocket-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
|
||||
// ================================
|
||||
@@ -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) {
|
||||
@@ -2116,10 +2122,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 +2949,156 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 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';
|
||||
messageDiv.innerHTML = `
|
||||
<span class="log-timestamp">${timestamp}</span>
|
||||
<span style="color: ${directionColor}; font-weight: bold;">[${direction.toUpperCase()}]</span>
|
||||
${message}
|
||||
`;
|
||||
|
||||
// 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
|
||||
@@ -3251,6 +3414,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;
|
||||
}
|
||||
761
src/dm_admin.c
761
src/dm_admin.c
@@ -1,6 +1,8 @@
|
||||
#define _GNU_SOURCE
|
||||
#include "config.h"
|
||||
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
#include "../nostr_core_lib/nostr_core/nip017.h"
|
||||
#include "../nostr_core_lib/nostr_core/nip044.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -31,6 +33,24 @@ extern int handle_auth_rule_modification_unified(cJSON* event, char* error_messa
|
||||
extern const char* get_first_tag_name(cJSON* event);
|
||||
extern const char* get_tag_value(cJSON* event, const char* tag_name, int value_index);
|
||||
|
||||
// Forward declarations for config functions
|
||||
extern const char* get_relay_pubkey_cached(void);
|
||||
extern char* get_relay_private_key(void);
|
||||
|
||||
// Forward declarations for database functions
|
||||
extern int store_event(cJSON* event);
|
||||
|
||||
// Forward declarations for stats generation
|
||||
extern char* generate_stats_json(void);
|
||||
|
||||
// Forward declarations for admin event processing
|
||||
extern int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
|
||||
// Forward declarations for NIP-17 processing
|
||||
int is_nip17_gift_wrap_for_relay(cJSON* event);
|
||||
int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
|
||||
// ================================
|
||||
// DIRECT MESSAGING ADMIN SYSTEM
|
||||
// ================================
|
||||
@@ -177,6 +197,239 @@ int process_dm_admin_command(cJSON* command_array, cJSON* event, char* error_mes
|
||||
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
|
||||
cJSON* 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 NULL;
|
||||
}
|
||||
|
||||
// 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 NULL;
|
||||
}
|
||||
|
||||
// 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 NULL;
|
||||
}
|
||||
|
||||
// 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 NULL;
|
||||
}
|
||||
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 NULL;
|
||||
}
|
||||
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 from the decrypted DM event
|
||||
cJSON* sender_pubkey_obj = cJSON_GetObjectItem(inner_dm, "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 the response gift wrap in database
|
||||
store_event(success_gift_wraps[0]);
|
||||
|
||||
// Return the response event for broadcasting
|
||||
cJSON_Delete(inner_dm);
|
||||
return success_gift_wraps[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(inner_dm);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Check if decrypted content is a direct command array (DM system)
|
||||
// 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) {
|
||||
@@ -200,3 +453,511 @@ int is_dm_command_array(const char* decrypted_content) {
|
||||
|
||||
return is_array;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// NIP-17 GIFT WRAP PROCESSING 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);
|
||||
|
||||
// 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)) {
|
||||
log_info("NIP-17: 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);
|
||||
|
||||
// Check if sender is admin
|
||||
const char* admin_pubkey = get_admin_pubkey_cached();
|
||||
int is_admin = admin_pubkey && strlen(admin_pubkey) > 0 && strcmp(sender_pubkey, admin_pubkey) == 0;
|
||||
|
||||
log_info("NIP-17: Processing admin command from DM content");
|
||||
char log_msg[256];
|
||||
snprintf(log_msg, sizeof(log_msg), "NIP-17: Received DM content: '%.50s'%s", dm_content, strlen(dm_content) > 50 ? "..." : "");
|
||||
log_info(log_msg);
|
||||
|
||||
// Parse DM content as JSON array of commands
|
||||
cJSON* command_array = cJSON_Parse(dm_content);
|
||||
if (!command_array || !cJSON_IsArray(command_array)) {
|
||||
// If content is not a JSON array, check for plain text commands
|
||||
if (is_admin) {
|
||||
// Convert content to lowercase for case-insensitive matching
|
||||
char content_lower[256];
|
||||
size_t content_len = strlen(dm_content);
|
||||
size_t copy_len = content_len < sizeof(content_lower) - 1 ? content_len : sizeof(content_lower) - 1;
|
||||
memcpy(content_lower, dm_content, copy_len);
|
||||
content_lower[copy_len] = '\0';
|
||||
|
||||
// Convert to lowercase
|
||||
for (size_t i = 0; i < copy_len; i++) {
|
||||
if (content_lower[i] >= 'A' && content_lower[i] <= 'Z') {
|
||||
content_lower[i] = content_lower[i] + 32;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for stats commands
|
||||
if (strstr(content_lower, "stats") != NULL || strstr(content_lower, "statistics") != NULL) {
|
||||
log_info("NIP-17: Recognized plain text 'stats' command from admin");
|
||||
log_info("NIP-17: Action: Generate and send relay statistics");
|
||||
|
||||
// Generate stats JSON
|
||||
char* stats_json = generate_stats_json();
|
||||
if (!stats_json) {
|
||||
log_error("NIP-17: Failed to generate stats for plain text command");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 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);
|
||||
log_error("NIP-17: Could not get relay keys for stats response");
|
||||
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);
|
||||
log_error("NIP-17: Failed to convert relay private key for stats response");
|
||||
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) {
|
||||
log_error("NIP-17: Failed to create DM response event for stats");
|
||||
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]) {
|
||||
log_error("NIP-17: Failed to create and sign response gift wrap for stats");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Fix the p tag in the gift wrap - library function may use wrong pubkey
|
||||
cJSON* gift_wrap_tags = cJSON_GetObjectItem(gift_wraps[0], "tags");
|
||||
if (gift_wrap_tags && cJSON_IsArray(gift_wrap_tags)) {
|
||||
// Find and replace the p tag with the correct user pubkey
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, gift_wrap_tags) {
|
||||
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
|
||||
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||
if (tag_name && cJSON_IsString(tag_name) &&
|
||||
strcmp(cJSON_GetStringValue(tag_name), "p") == 0) {
|
||||
// Replace the p tag value with the correct user pubkey
|
||||
cJSON_ReplaceItemInArray(tag, 1, cJSON_CreateString(sender_pubkey));
|
||||
log_info("NIP-17: Fixed p tag in stats response gift wrap");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug print to show the response event
|
||||
char* response_debug = cJSON_Print(gift_wraps[0]);
|
||||
if (response_debug) {
|
||||
log_info("DM Admin: Response event created");
|
||||
printf(" Response event: %s\n", response_debug);
|
||||
free(response_debug);
|
||||
}
|
||||
|
||||
// Debug: Print event before storing
|
||||
char* debug_before_store = cJSON_Print(gift_wraps[0]);
|
||||
if (debug_before_store) {
|
||||
log_info("DEBUG EVENT: Before storing in database");
|
||||
printf(" Event: %s\n", debug_before_store);
|
||||
free(debug_before_store);
|
||||
}
|
||||
|
||||
// Store the gift wrap in database
|
||||
int store_result = store_event(gift_wraps[0]);
|
||||
cJSON_Delete(gift_wraps[0]);
|
||||
|
||||
if (store_result != 0) {
|
||||
log_error("NIP-17: Failed to store response gift wrap for stats");
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_success("NIP-17: Stats command processed successfully, response sent");
|
||||
return 0;
|
||||
}
|
||||
// Check for config commands
|
||||
else if (strstr(content_lower, "config") != NULL || strstr(content_lower, "configuration") != NULL) {
|
||||
log_info("NIP-17: Recognized plain text 'config' command from admin");
|
||||
log_info("NIP-17: Action: Generate and send relay configuration");
|
||||
|
||||
// Get relay pubkey for config response
|
||||
const char* relay_pubkey = get_relay_pubkey_cached();
|
||||
|
||||
// Generate config JSON - for now, use a simple config summary
|
||||
cJSON* config_response = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(config_response, "command", "config");
|
||||
cJSON_AddStringToObject(config_response, "relay_pubkey", relay_pubkey ? relay_pubkey : "unknown");
|
||||
|
||||
// Add some basic config values
|
||||
const char* port = get_config_value("relay_port");
|
||||
const char* nip42_auth = get_config_bool("nip42_auth_required_events", 0) ? "enabled" : "disabled";
|
||||
const char* nip42_sub = get_config_bool("nip42_auth_required_subscriptions", 0) ? "enabled" : "disabled";
|
||||
|
||||
cJSON_AddStringToObject(config_response, "relay_port", port ? port : "8888");
|
||||
cJSON_AddStringToObject(config_response, "nip42_auth_events", nip42_auth);
|
||||
cJSON_AddStringToObject(config_response, "nip42_auth_subscriptions", nip42_sub);
|
||||
|
||||
char* config_json = cJSON_Print(config_response);
|
||||
cJSON_Delete(config_response);
|
||||
|
||||
if (!config_json) {
|
||||
log_error("NIP-17: Failed to generate config JSON for plain text command");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Get relay keys for signing
|
||||
char* relay_privkey_hex = get_relay_private_key();
|
||||
if (!relay_privkey_hex) {
|
||||
free(config_json);
|
||||
log_error("NIP-17: Could not get relay private key for config response");
|
||||
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(config_json);
|
||||
free(relay_privkey_hex);
|
||||
log_error("NIP-17: Failed to convert relay private key for config response");
|
||||
return -1;
|
||||
}
|
||||
free(relay_privkey_hex);
|
||||
|
||||
// Create DM response event using library function
|
||||
cJSON* dm_response = nostr_nip17_create_chat_event(
|
||||
config_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 (already declared above)
|
||||
);
|
||||
|
||||
free(config_json);
|
||||
|
||||
if (!dm_response) {
|
||||
log_error("NIP-17: Failed to create DM response event for config");
|
||||
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]) {
|
||||
log_error("NIP-17: Failed to create and sign response gift wrap for config");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Fix the p tag in the gift wrap - library function may use wrong pubkey
|
||||
cJSON* gift_wrap_tags = cJSON_GetObjectItem(gift_wraps[0], "tags");
|
||||
if (gift_wrap_tags && cJSON_IsArray(gift_wrap_tags)) {
|
||||
// Find and replace the p tag with the correct user pubkey
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, gift_wrap_tags) {
|
||||
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
|
||||
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||
if (tag_name && cJSON_IsString(tag_name) &&
|
||||
strcmp(cJSON_GetStringValue(tag_name), "p") == 0) {
|
||||
// Replace the p tag value with the correct user pubkey
|
||||
cJSON_ReplaceItemInArray(tag, 1, cJSON_CreateString(sender_pubkey));
|
||||
log_info("NIP-17: Fixed p tag in config response gift wrap");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: Print event after p tag fix
|
||||
char* debug_after_fix = cJSON_Print(gift_wraps[0]);
|
||||
if (debug_after_fix) {
|
||||
log_info("DEBUG EVENT: After p tag fix");
|
||||
printf(" Event: %s\n", debug_after_fix);
|
||||
free(debug_after_fix);
|
||||
}
|
||||
|
||||
// Debug print to show the response event
|
||||
char* response_debug = cJSON_Print(gift_wraps[0]);
|
||||
if (response_debug) {
|
||||
log_info("DM Admin: Response event created");
|
||||
printf(" Response event: %s\n", response_debug);
|
||||
free(response_debug);
|
||||
}
|
||||
|
||||
// Store the gift wrap in database
|
||||
int store_result = store_event(gift_wraps[0]);
|
||||
cJSON_Delete(gift_wraps[0]);
|
||||
|
||||
if (store_result != 0) {
|
||||
log_error("NIP-17: Failed to store response gift wrap for config");
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_success("NIP-17: Config command processed successfully, response sent");
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
log_info("NIP-17: Plain text content from admin not recognized as command, treating as user DM");
|
||||
return 0; // Admin sent unrecognized plain text, treat as user DM
|
||||
}
|
||||
} else {
|
||||
// Not admin, treat as user DM
|
||||
log_info("NIP-17: Content is not JSON array and sender is not admin, treating as user DM");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Fix the p tag in the gift wrap - library function may use wrong pubkey
|
||||
cJSON* gift_wrap_tags = cJSON_GetObjectItem(gift_wraps[0], "tags");
|
||||
if (gift_wrap_tags && cJSON_IsArray(gift_wrap_tags)) {
|
||||
// Find and replace the p tag with the correct user pubkey
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, gift_wrap_tags) {
|
||||
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
|
||||
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||
if (tag_name && cJSON_IsString(tag_name) &&
|
||||
strcmp(cJSON_GetStringValue(tag_name), "p") == 0) {
|
||||
// Replace the p tag value with the correct user pubkey
|
||||
cJSON_ReplaceItemInArray(tag, 1, cJSON_CreateString(sender_pubkey));
|
||||
log_info("NIP-17: Fixed p tag in JSON array stats response gift wrap");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug print to show the response event
|
||||
char* response_debug = cJSON_Print(gift_wraps[0]);
|
||||
if (response_debug) {
|
||||
log_info("DM Admin: Response event created");
|
||||
printf(" Response event: %s\n", response_debug);
|
||||
free(response_debug);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -12,4 +12,10 @@ 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);
|
||||
|
||||
#endif // DM_ADMIN_H
|
||||
File diff suppressed because one or more lines are too long
@@ -274,21 +274,34 @@ int event_matches_filter(cJSON* event, subscription_filter_t* 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,9 +390,14 @@ 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) {
|
||||
@@ -389,6 +407,10 @@ int event_matches_filter(cJSON* event, subscription_filter_t* 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
|
||||
}
|
||||
@@ -409,18 +431,31 @@ int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -433,6 +468,9 @@ int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -486,8 +547,25 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
||||
|
||||
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);
|
||||
|
||||
@@ -643,9 +641,9 @@ 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) {
|
||||
if (!response_event) {
|
||||
log_error("DEBUG NIP17: NIP-17 admin message processing failed");
|
||||
result = -1;
|
||||
size_t error_len = strlen(nip17_error);
|
||||
@@ -659,19 +657,31 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
log_error(debug_nip17_error_msg);
|
||||
} else {
|
||||
log_success("DEBUG NIP17: NIP-17 admin message processed successfully");
|
||||
// Store the gift wrap event in database (unlike kind 23456)
|
||||
// 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) {
|
||||
|
||||
Reference in New Issue
Block a user