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
|
||||
|
||||
Reference in New Issue
Block a user