fix: search for npub
This commit is contained in:
parent
6a5b9805bc
commit
c976746b07
@ -21,9 +21,17 @@
|
|||||||
<h2>Server Information:</h2>
|
<h2>Server Information:</h2>
|
||||||
<div style="margin-bottom: 15px;">
|
<div style="margin-bottom: 15px;">
|
||||||
<div style="margin-bottom: 10px;">
|
<div style="margin-bottom: 10px;">
|
||||||
<label for="serverPubkey">Server Pubkey:</label><br>
|
<label for="serverPubkey">Server Pubkey or Search Term:</label><br>
|
||||||
<input type="text" id="serverPubkey" value="npub1r6knexka25dn9w9jnf5kf8xh6gfq7n3p38zfl7nn7cjjjsp4umcqnk0aun" style="width: 100%; padding: 8px;">
|
<div class="server-input-container">
|
||||||
|
<input type="text" id="serverPubkey" placeholder="npub, username, or NIP-05 identifier" value="npub1r6knexka25dn9w9jnf5kf8xh6gfq7n3p38zfl7nn7cjjjsp4umcqnk0aun" class="server-input">
|
||||||
|
<button id="searchServerBtn" class="server-search-button">Search</button>
|
||||||
|
</div>
|
||||||
|
<div id="serverSearchResult" class="server-search-result" style="display: none;">
|
||||||
|
<!-- Search results will be shown here -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="relay">Response Relay (optional):</label><br>
|
<label for="relay">Response Relay (optional):</label><br>
|
||||||
<input type="text" id="relay" value="wss://relay.damus.io" style="width: 100%; padding: 8px;">
|
<input type="text" id="relay" value="wss://relay.damus.io" style="width: 100%; padding: 8px;">
|
||||||
@ -42,6 +50,23 @@ User-Agent: Browser/1.0
|
|||||||
<div id="output" hidden>
|
<div id="output" hidden>
|
||||||
<h2>Converted Event:</h2>
|
<h2>Converted Event:</h2>
|
||||||
<pre id="eventOutput"></pre>
|
<pre id="eventOutput"></pre>
|
||||||
|
|
||||||
|
<div class="publish-container">
|
||||||
|
<h2>Publish to Relay:</h2>
|
||||||
|
<div class="publish-input-container">
|
||||||
|
<input type="text" id="publishRelay" value="wss://relay.nostrdev.com" placeholder="wss://relay.example.com" class="publish-input">
|
||||||
|
<button id="publishButton" class="publish-button">Publish Event</button>
|
||||||
|
</div>
|
||||||
|
<div id="publishResult" class="publish-result" style="display: none;">
|
||||||
|
<!-- Publish results will be shown here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="qr-container">
|
||||||
|
<h2>QR Code:</h2>
|
||||||
|
<div id="qrCode"></div>
|
||||||
|
<p><small>Scan this QR code to share the Nostr event</small></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
1190
client/package-lock.json
generated
1190
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -22,6 +22,8 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/qrcode": "^1.5.5",
|
||||||
|
"@types/qrcode-generator": "^1.0.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.29.0",
|
"@typescript-eslint/eslint-plugin": "^8.29.0",
|
||||||
"@typescript-eslint/parser": "^8.29.0",
|
"@typescript-eslint/parser": "^8.29.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
@ -31,6 +33,7 @@
|
|||||||
"eslint": "^9.24.0",
|
"eslint": "^9.24.0",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
|
"node-polyfill-webpack-plugin": "^4.1.0",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"serve": "^14.0.0",
|
"serve": "^14.0.0",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
@ -42,6 +45,8 @@
|
|||||||
"webpack-dev-server": "^5.2.1"
|
"webpack-dev-server": "^5.2.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nostr-tools": "^2.12.0"
|
"nostr-tools": "^2.12.0",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
|
"qrcode-generator": "^1.4.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,258 @@
|
|||||||
// client.ts - External TypeScript file for HTTP to Nostr converter
|
// client.ts - External TypeScript file for HTTP to Nostr converter
|
||||||
// This follows strict CSP policies by avoiding inline scripts
|
// This follows strict CSP policies by avoiding inline scripts
|
||||||
|
|
||||||
// Import the converter function
|
// Import functions from other modules
|
||||||
import { displayConvertedEvent } from './converter';
|
import { displayConvertedEvent } from './converter';
|
||||||
|
import { lookupNip05, searchUsers } from './search';
|
||||||
|
import { publishToRelay, convertNpubToHex, verifyEvent } from './relay';
|
||||||
|
import {
|
||||||
|
setDefaultHttpRequest,
|
||||||
|
sanitizeText,
|
||||||
|
processTags,
|
||||||
|
standardizeEvent,
|
||||||
|
showError,
|
||||||
|
showSuccess,
|
||||||
|
showLoading
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the server search button click
|
||||||
|
*/
|
||||||
|
async function handleServerSearch(): Promise<void> {
|
||||||
|
const serverPubkeyInput = document.getElementById('serverPubkey') as HTMLInputElement;
|
||||||
|
const resultDiv = document.getElementById('serverSearchResult');
|
||||||
|
|
||||||
|
if (!serverPubkeyInput || !resultDiv) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchTerm = serverPubkeyInput.value.trim();
|
||||||
|
if (!searchTerm) {
|
||||||
|
showError(resultDiv, 'Please enter a search term');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a valid npub, no need to search
|
||||||
|
if (searchTerm.startsWith('npub')) {
|
||||||
|
try {
|
||||||
|
const hexPubkey = convertNpubToHex(searchTerm);
|
||||||
|
if (hexPubkey) {
|
||||||
|
// It's a valid npub, hide any existing results
|
||||||
|
resultDiv.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Not a valid npub, continue with search
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display loading state
|
||||||
|
showLoading(resultDiv, 'Searching relays...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await searchUsers(searchTerm);
|
||||||
|
|
||||||
|
if (results.length > 0) {
|
||||||
|
// If there's only one result and it's a valid npub, use it directly
|
||||||
|
if (results.length === 1 && results[0].name === 'Valid npub') {
|
||||||
|
serverPubkeyInput.value = results[0].npub;
|
||||||
|
resultDiv.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the results list
|
||||||
|
let resultsHtml = '<div class="search-results-list">';
|
||||||
|
|
||||||
|
results.forEach(result => {
|
||||||
|
const truncatedNpub = `${result.npub.substring(0, 10)}...${result.npub.substring(result.npub.length - 5)}`;
|
||||||
|
resultsHtml += `
|
||||||
|
<div class="search-result-item" data-npub="${result.npub}">
|
||||||
|
<div class="result-name">${result.name}</div>
|
||||||
|
<div class="result-npub">${truncatedNpub}</div>
|
||||||
|
${result.nip05 ? `<div class="result-nip05">${result.nip05}</div>` : ''}
|
||||||
|
<button class="use-npub-btn">Use</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
resultsHtml += '</div>';
|
||||||
|
resultDiv.innerHTML = resultsHtml;
|
||||||
|
|
||||||
|
// Add click handlers for the "Use" buttons
|
||||||
|
document.querySelectorAll('.use-npub-btn').forEach(button => {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
const resultItem = (e.target as HTMLElement).closest('.search-result-item');
|
||||||
|
if (resultItem) {
|
||||||
|
const npub = resultItem.getAttribute('data-npub');
|
||||||
|
if (npub) {
|
||||||
|
serverPubkeyInput.value = npub;
|
||||||
|
resultDiv.innerHTML += '<br><span style="color: #008800;">✓ Applied!</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
showError(resultDiv, 'No users found matching your search term');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(resultDiv, String(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the publish button click
|
||||||
|
*/
|
||||||
|
async function handlePublishEvent(): Promise<void> {
|
||||||
|
const eventOutputPre = document.getElementById('eventOutput') as HTMLElement;
|
||||||
|
const publishRelayInput = document.getElementById('publishRelay') as HTMLInputElement;
|
||||||
|
const publishResultDiv = document.getElementById('publishResult') as HTMLElement;
|
||||||
|
|
||||||
|
if (!eventOutputPre || !publishRelayInput || !publishResultDiv) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have a stored event from the creation process
|
||||||
|
if ((window as any).currentSignedEvent) {
|
||||||
|
console.log("Using stored signed event from creation process");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const event = (window as any).currentSignedEvent;
|
||||||
|
const relayUrl = publishRelayInput.value.trim();
|
||||||
|
|
||||||
|
if (!relayUrl || !relayUrl.startsWith('wss://')) {
|
||||||
|
showError(publishResultDiv, 'Please enter a valid relay URL (must start with wss://)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display loading state
|
||||||
|
showLoading(publishResultDiv, 'Publishing to relay...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await publishToRelay(event, relayUrl);
|
||||||
|
console.log('Publish success:', result);
|
||||||
|
showSuccess(publishResultDiv, result);
|
||||||
|
} catch (publishError) {
|
||||||
|
console.error('Publish error:', publishError);
|
||||||
|
showError(publishResultDiv, String(publishError));
|
||||||
|
// Don't rethrow, just handle it here so the UI shows the error
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error using stored event, falling back to parsed event");
|
||||||
|
// Continue with normal flow if this fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventText = eventOutputPre.textContent || '';
|
||||||
|
if (!eventText) {
|
||||||
|
showError(publishResultDiv, 'No event to publish');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Raw event text:', eventText);
|
||||||
|
|
||||||
|
let event;
|
||||||
|
try {
|
||||||
|
// Check for non-printable characters or hidden characters that might cause issues
|
||||||
|
const sanitizedEventText = sanitizeText(eventText);
|
||||||
|
console.log('Sanitized event text:', sanitizedEventText);
|
||||||
|
|
||||||
|
event = JSON.parse(sanitizedEventText);
|
||||||
|
console.log('Parsed event:', event);
|
||||||
|
|
||||||
|
// Validate that it's a proper Nostr event
|
||||||
|
if (!event || typeof event !== 'object') {
|
||||||
|
throw new Error('Invalid event object');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it has id, pubkey, and sig properties which are required for a valid Nostr event
|
||||||
|
if (!event.id || !event.pubkey || !event.sig) {
|
||||||
|
throw new Error('Event is missing required properties (id, pubkey, or sig)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if pubkey is in npub format and convert it if needed
|
||||||
|
if (event.pubkey.startsWith('npub')) {
|
||||||
|
const hexPubkey = convertNpubToHex(event.pubkey);
|
||||||
|
if (hexPubkey) {
|
||||||
|
console.log('Converting npub to hex pubkey...');
|
||||||
|
event.pubkey = hexPubkey;
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid npub format in pubkey');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a clean event with exactly the fields we need
|
||||||
|
console.log("Creating a clean event for publishing...");
|
||||||
|
event = standardizeEvent(event);
|
||||||
|
console.log("Clean event created:", JSON.stringify(event, null, 2));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
showError(publishResultDiv, `Invalid event: ${String(error)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relayUrl = publishRelayInput.value.trim();
|
||||||
|
if (!relayUrl || !relayUrl.startsWith('wss://')) {
|
||||||
|
showError(publishResultDiv, 'Please enter a valid relay URL (must start with wss://)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display loading state
|
||||||
|
showLoading(publishResultDiv, 'Publishing to relay...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check the event structure
|
||||||
|
console.log('Full event object before publish:', JSON.stringify(event, null, 2));
|
||||||
|
|
||||||
|
// Verify event first
|
||||||
|
console.log('Verifying event...');
|
||||||
|
const isValid = verifyEvent(event);
|
||||||
|
console.log('Event verification result:', isValid);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
console.log('Event verification failed. Proceeding anyway as the relay will validate.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proceed with publish even if verification failed - the relay will validate
|
||||||
|
publishResultDiv.innerHTML += '<br><span>Attempting to publish...</span>';
|
||||||
|
console.log('Attempting to publish event to relay:', relayUrl);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await publishToRelay(event, relayUrl);
|
||||||
|
console.log('Publish success:', result);
|
||||||
|
showSuccess(publishResultDiv, result);
|
||||||
|
} catch (publishError) {
|
||||||
|
console.error('Publish error:', publishError);
|
||||||
|
showError(publishResultDiv, String(publishError));
|
||||||
|
// Don't rethrow, just handle it here so the UI shows the error
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error preparing event for publish:', error);
|
||||||
|
showError(publishResultDiv, `Error preparing event: ${String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the event handlers when the DOM is loaded
|
// Initialize the event handlers when the DOM is loaded
|
||||||
document.addEventListener('DOMContentLoaded', function(): void {
|
document.addEventListener('DOMContentLoaded', function(): void {
|
||||||
// Set up the convert button click handler
|
// Set up the convert button click handler
|
||||||
const convertButton = document.getElementById('convertButton');
|
const convertButton = document.getElementById('convertButton');
|
||||||
|
const searchButton = document.getElementById('searchServerBtn');
|
||||||
|
const publishButton = document.getElementById('publishButton');
|
||||||
|
|
||||||
if (convertButton) {
|
if (convertButton) {
|
||||||
convertButton.addEventListener('click', displayConvertedEvent);
|
convertButton.addEventListener('click', displayConvertedEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (searchButton) {
|
||||||
|
searchButton.addEventListener('click', handleServerSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (publishButton) {
|
||||||
|
publishButton.addEventListener('click', handlePublishEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default HTTP request
|
||||||
|
setDefaultHttpRequest();
|
||||||
|
|
||||||
console.log('HTTP to Nostr converter initialized');
|
console.log('HTTP to Nostr converter initialized');
|
||||||
});
|
});
|
@ -7,6 +7,9 @@ declare global {
|
|||||||
}
|
}
|
||||||
import { defaultServerConfig, appSettings } from './config';
|
import { defaultServerConfig, appSettings } from './config';
|
||||||
import * as nostrTools from 'nostr-tools';
|
import * as nostrTools from 'nostr-tools';
|
||||||
|
import qrcode from 'qrcode-generator';
|
||||||
|
import { convertNpubToHex } from './relay';
|
||||||
|
import { processTags, showSuccess } from './utils';
|
||||||
|
|
||||||
// Generate a keypair for standalone mode (when no extension is available)
|
// Generate a keypair for standalone mode (when no extension is available)
|
||||||
let standaloneSecretKey: Uint8Array | null = null;
|
let standaloneSecretKey: Uint8Array | null = null;
|
||||||
@ -60,16 +63,13 @@ export function convertToEvent(
|
|||||||
// The second argument MUST be the server's public key in hex format
|
// The second argument MUST be the server's public key in hex format
|
||||||
let serverPubkeyHex = serverPubkey;
|
let serverPubkeyHex = serverPubkey;
|
||||||
|
|
||||||
// Convert npub to hex if needed using nostr-tools
|
// Convert npub to hex if needed
|
||||||
if (serverPubkey.startsWith('npub')) {
|
if (serverPubkey.startsWith('npub')) {
|
||||||
try {
|
const hexPubkey = convertNpubToHex(serverPubkey);
|
||||||
const decoded = nostrTools.nip19.decode(serverPubkey);
|
if (hexPubkey) {
|
||||||
if (decoded.type === 'npub' && decoded.data) {
|
serverPubkeyHex = hexPubkey;
|
||||||
serverPubkeyHex = decoded.data;
|
console.log("Converted npub to hex format:", serverPubkeyHex);
|
||||||
console.log("Converted npub to hex format:", serverPubkeyHex);
|
} else {
|
||||||
}
|
|
||||||
} catch (decodeError) {
|
|
||||||
console.error("Failed to decode npub:", decodeError);
|
|
||||||
throw new Error("Failed to decode npub. Please use a valid npub format.");
|
throw new Error("Failed to decode npub. Please use a valid npub format.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -89,15 +89,31 @@ export function convertToEvent(
|
|||||||
console.warn("NIP-44 encryption not available. Content will not be encrypted properly.");
|
console.warn("NIP-44 encryption not available. Content will not be encrypted properly.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert serverPubkey to hex if it's an npub
|
||||||
|
let pTagValue = serverPubkey;
|
||||||
|
if (serverPubkey.startsWith('npub')) {
|
||||||
|
try {
|
||||||
|
console.log(`Converting p tag npub to hex: ${serverPubkey}`);
|
||||||
|
const decoded = nostrTools.nip19.decode(serverPubkey);
|
||||||
|
if (decoded.type === 'npub' && decoded.data) {
|
||||||
|
pTagValue = decoded.data as string;
|
||||||
|
console.log(`Converted to hex pubkey: ${pTagValue}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error decoding npub: ${serverPubkey}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the event with the proper structure
|
||||||
const event = {
|
const event = {
|
||||||
kind: 21120,
|
kind: 21120,
|
||||||
pubkey: pubkey,
|
pubkey: pubkey,
|
||||||
content: encryptedContent, // Encrypted HTTP request using NIP-44
|
content: encryptedContent, // Encrypted HTTP request using NIP-44
|
||||||
tags: [
|
tags: [
|
||||||
// Required tags per README specification
|
// Required tags per README specification
|
||||||
["p", serverPubkey], // P tag to indicate this is a REQUEST
|
["p", pTagValue], // P tag with hex pubkey (converted from npub if needed)
|
||||||
["key", decryptkey], // Key for decryption
|
["key", decryptkey], // Key for decryption
|
||||||
["expiration", Math.floor(Date.now() / 1000) + appSettings.expirationTime]
|
["expiration", String(Math.floor(Date.now() / 1000) + appSettings.expirationTime)]
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -105,6 +121,9 @@ export function convertToEvent(
|
|||||||
if (relay) {
|
if (relay) {
|
||||||
event.tags.push(["r", relay]);
|
event.tags.push(["r", relay]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process tags to ensure proper format (convert any npub in tags to hex, etc.)
|
||||||
|
event.tags = processTags(event.tags);
|
||||||
|
|
||||||
return JSON.stringify(event, null, 2);
|
return JSON.stringify(event, null, 2);
|
||||||
}
|
}
|
||||||
@ -156,6 +175,9 @@ export async function displayConvertedEvent(): Promise<void> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (convertedEvent) {
|
if (convertedEvent) {
|
||||||
|
// Store the original event in case we need to reference it
|
||||||
|
(window as any).originalEvent = convertedEvent;
|
||||||
|
|
||||||
// Parse the event to create a proper Nostr event object for signing
|
// Parse the event to create a proper Nostr event object for signing
|
||||||
const parsedEvent = JSON.parse(convertedEvent);
|
const parsedEvent = JSON.parse(convertedEvent);
|
||||||
const nostrEvent = {
|
const nostrEvent = {
|
||||||
@ -165,6 +187,9 @@ export async function displayConvertedEvent(): Promise<void> {
|
|||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
pubkey: parsedEvent.pubkey
|
pubkey: parsedEvent.pubkey
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Log the event being signed
|
||||||
|
console.log("Event to be signed:", JSON.stringify(nostrEvent, null, 2));
|
||||||
|
|
||||||
let signedEvent;
|
let signedEvent;
|
||||||
|
|
||||||
@ -172,6 +197,7 @@ export async function displayConvertedEvent(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
// Try to sign with the NIP-07 extension
|
// Try to sign with the NIP-07 extension
|
||||||
signedEvent = await window.nostr.signEvent(nostrEvent);
|
signedEvent = await window.nostr.signEvent(nostrEvent);
|
||||||
|
console.log("Event signed with extension:", signedEvent);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error signing event with extension:", error);
|
console.error("Error signing event with extension:", error);
|
||||||
// Fall back to signing with nostr-tools
|
// Fall back to signing with nostr-tools
|
||||||
@ -191,9 +217,211 @@ export async function displayConvertedEvent(): Promise<void> {
|
|||||||
outputDiv.hidden = false;
|
outputDiv.hidden = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Store the event in a global variable for easier access during publishing
|
||||||
|
(window as any).currentSignedEvent = signedEvent;
|
||||||
|
|
||||||
|
// Display the event JSON
|
||||||
|
eventOutputPre.textContent = JSON.stringify(signedEvent, null, 2);
|
||||||
|
|
||||||
|
// Add a helpful message about publishing the event
|
||||||
|
const publishRelayInput = document.getElementById('publishRelay') as HTMLInputElement;
|
||||||
|
if (publishRelayInput) {
|
||||||
|
const publishResult = document.getElementById('publishResult');
|
||||||
|
if (publishResult) {
|
||||||
|
showSuccess(publishResult, 'Event created successfully. Ready to publish!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
eventOutputPre.textContent = JSON.stringify(signedEvent, null, 2);
|
|
||||||
|
|
||||||
|
// Generate animated QR code using multiple frames
|
||||||
|
const qrCodeContainer = document.getElementById('qrCode') as HTMLElement;
|
||||||
|
if (qrCodeContainer) {
|
||||||
|
try {
|
||||||
|
// Convert the event to a JSON string
|
||||||
|
const eventJson = JSON.stringify(signedEvent);
|
||||||
|
|
||||||
|
// Calculate how many QR codes we need based on the data size
|
||||||
|
// Use a much smaller chunk size to ensure it fits in the QR code
|
||||||
|
const maxChunkSize = 500; // Significantly reduced chunk size
|
||||||
|
const chunks = [];
|
||||||
|
|
||||||
|
// Split the data into chunks
|
||||||
|
for (let i = 0; i < eventJson.length; i += maxChunkSize) {
|
||||||
|
chunks.push(eventJson.slice(i, i + maxChunkSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare container for the animated QR code
|
||||||
|
qrCodeContainer.innerHTML = '';
|
||||||
|
const qrFrameContainer = document.createElement('div');
|
||||||
|
qrFrameContainer.className = 'qr-frame-container';
|
||||||
|
qrCodeContainer.appendChild(qrFrameContainer);
|
||||||
|
|
||||||
|
// Create QR codes for each chunk with chunk number and total chunks
|
||||||
|
const qrFrames: HTMLElement[] = [];
|
||||||
|
chunks.forEach((chunk, index) => {
|
||||||
|
// Add metadata to identify part of sequence: [current/total]:[data]
|
||||||
|
const dataWithMeta = `${index + 1}/${chunks.length}:${chunk}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create QR code with maximum version and lower error correction
|
||||||
|
const qr = qrcode(15, 'L'); // Version 15 (higher capacity), Low error correction
|
||||||
|
qr.addData(dataWithMeta);
|
||||||
|
qr.make();
|
||||||
|
|
||||||
|
// Create frame
|
||||||
|
const frameDiv = document.createElement('div');
|
||||||
|
frameDiv.className = 'qr-frame';
|
||||||
|
frameDiv.style.display = index === 0 ? 'block' : 'none';
|
||||||
|
frameDiv.innerHTML = qr.createSvgTag({
|
||||||
|
cellSize: 3, // Smaller cell size
|
||||||
|
margin: 2
|
||||||
|
});
|
||||||
|
|
||||||
|
qrFrameContainer.appendChild(frameDiv);
|
||||||
|
qrFrames.push(frameDiv);
|
||||||
|
} catch (qrError) {
|
||||||
|
console.error(`Error generating QR code for chunk ${index + 1}:`, qrError);
|
||||||
|
// Create an error frame instead
|
||||||
|
const errorDiv = document.createElement('div');
|
||||||
|
errorDiv.className = 'qr-frame qr-error';
|
||||||
|
errorDiv.style.display = index === 0 ? 'block' : 'none';
|
||||||
|
errorDiv.innerHTML = `
|
||||||
|
<div class="error-message">
|
||||||
|
<p>Error in frame ${index + 1}</p>
|
||||||
|
<p>Chunk too large for QR code</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
qrFrameContainer.appendChild(errorDiv);
|
||||||
|
qrFrames.push(errorDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add information about the animated QR code
|
||||||
|
const infoElement = document.createElement('div');
|
||||||
|
infoElement.innerHTML = `
|
||||||
|
<p class="qr-info">Animated QR code: ${chunks.length} frames containing full event data</p>
|
||||||
|
<p class="qr-info current-frame">Showing frame 1 of ${chunks.length}</p>
|
||||||
|
<p class="qr-info"><small>The QR code will cycle through all frames automatically</small></p>
|
||||||
|
`;
|
||||||
|
qrCodeContainer.appendChild(infoElement);
|
||||||
|
|
||||||
|
// Animation controls
|
||||||
|
const controlsDiv = document.createElement('div');
|
||||||
|
controlsDiv.className = 'qr-controls';
|
||||||
|
controlsDiv.innerHTML = `
|
||||||
|
<button id="qrPauseBtn">Pause</button>
|
||||||
|
<button id="qrPrevBtn">◀ Prev</button>
|
||||||
|
<button id="qrNextBtn">Next ▶</button>
|
||||||
|
`;
|
||||||
|
qrCodeContainer.appendChild(controlsDiv);
|
||||||
|
|
||||||
|
// Set up animation
|
||||||
|
let currentFrame = 0;
|
||||||
|
let animationInterval: number | null = null;
|
||||||
|
let isPaused = false;
|
||||||
|
|
||||||
|
const updateFrameInfo = () => {
|
||||||
|
const frameInfo = qrCodeContainer.querySelector('.current-frame');
|
||||||
|
if (frameInfo) {
|
||||||
|
frameInfo.textContent = `Showing frame ${currentFrame + 1} of ${chunks.length}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showFrame = (index: number) => {
|
||||||
|
qrFrames.forEach((frame, i) => {
|
||||||
|
frame.style.display = i === index ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
currentFrame = index;
|
||||||
|
updateFrameInfo();
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextFrame = () => {
|
||||||
|
showFrame((currentFrame + 1) % qrFrames.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevFrame = () => {
|
||||||
|
showFrame((currentFrame - 1 + qrFrames.length) % qrFrames.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the animation
|
||||||
|
animationInterval = window.setInterval(nextFrame, 2000); // Change frame every 2 seconds
|
||||||
|
|
||||||
|
// Set up button event handlers
|
||||||
|
const pauseBtn = document.getElementById('qrPauseBtn');
|
||||||
|
const prevBtn = document.getElementById('qrPrevBtn');
|
||||||
|
const nextBtn = document.getElementById('qrNextBtn');
|
||||||
|
|
||||||
|
if (pauseBtn) {
|
||||||
|
pauseBtn.addEventListener('click', () => {
|
||||||
|
if (isPaused) {
|
||||||
|
animationInterval = window.setInterval(nextFrame, 2000);
|
||||||
|
pauseBtn.textContent = 'Pause';
|
||||||
|
} else {
|
||||||
|
if (animationInterval !== null) {
|
||||||
|
clearInterval(animationInterval);
|
||||||
|
animationInterval = null;
|
||||||
|
}
|
||||||
|
pauseBtn.textContent = 'Play';
|
||||||
|
}
|
||||||
|
isPaused = !isPaused;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevBtn) {
|
||||||
|
prevBtn.addEventListener('click', () => {
|
||||||
|
if (isPaused) {
|
||||||
|
prevFrame();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextBtn) {
|
||||||
|
nextBtn.addEventListener('click', () => {
|
||||||
|
if (isPaused) {
|
||||||
|
nextFrame();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error generating QR code:", error);
|
||||||
|
|
||||||
|
// Create a fallback display with error information and a more compact representation
|
||||||
|
qrCodeContainer.innerHTML = '';
|
||||||
|
|
||||||
|
// Create a backup QR code with just the event ID and relay
|
||||||
|
try {
|
||||||
|
const eventId = signedEvent.id || '';
|
||||||
|
const relay = encodeURIComponent(defaultServerConfig.defaultRelay);
|
||||||
|
const nostrUri = `nostr:${eventId}?relay=${relay}`;
|
||||||
|
|
||||||
|
const qr = qrcode(10, 'M');
|
||||||
|
qr.addData(nostrUri);
|
||||||
|
qr.make();
|
||||||
|
|
||||||
|
qrCodeContainer.innerHTML = `
|
||||||
|
<div class="qr-error-container">
|
||||||
|
<h3>Event Too Large for Animated QR</h3>
|
||||||
|
<p>Error: ${String(error)}</p>
|
||||||
|
<p>Using event reference instead:</p>
|
||||||
|
${qr.createSvgTag({ cellSize: 4, margin: 4 })}
|
||||||
|
<p class="qr-info">This QR code contains a reference to the event: ${eventId.substring(0, 8)}...</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} catch (fallbackError) {
|
||||||
|
qrCodeContainer.innerHTML = `
|
||||||
|
<div class="qr-error-container">
|
||||||
|
<h3>QR Generation Failed</h3>
|
||||||
|
<p>Error: ${String(error)}</p>
|
||||||
|
<p>Try sharing the event ID manually: ${signedEvent.id || 'Unknown'}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
qrCodeContainer.innerHTML = `<p>Error generating QR code: ${error}</p>
|
||||||
|
<p>Try using a URL shortener with the event ID instead.</p>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
outputDiv.hidden = false;
|
outputDiv.hidden = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -201,9 +429,28 @@ export async function displayConvertedEvent(): Promise<void> {
|
|||||||
|
|
||||||
// Initialize event listeners when the DOM is fully loaded
|
// Initialize event listeners when the DOM is fully loaded
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// Only set up the click event handler without any automatic encryption
|
// Set up the click event handler without any automatic encryption
|
||||||
const convertButton = document.getElementById('convertButton');
|
const convertButton = document.getElementById('convertButton');
|
||||||
|
const publishButton = document.getElementById('publishButton');
|
||||||
|
|
||||||
if (convertButton) {
|
if (convertButton) {
|
||||||
convertButton.addEventListener('click', displayConvertedEvent);
|
convertButton.addEventListener('click', displayConvertedEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a handler for the publish button to check if an event is available
|
||||||
|
if (publishButton) {
|
||||||
|
publishButton.addEventListener('click', () => {
|
||||||
|
const eventOutput = document.getElementById('eventOutput');
|
||||||
|
const publishResult = document.getElementById('publishResult');
|
||||||
|
|
||||||
|
if (!eventOutput || !publishResult) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!eventOutput.textContent || eventOutput.textContent.trim() === '') {
|
||||||
|
publishResult.innerHTML = '<span style="color: #cc0000;">You need to convert an HTTP request first</span>';
|
||||||
|
publishResult.style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
258
client/src/relay.ts
Normal file
258
client/src/relay.ts
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
// relay.ts - Functions for communicating with Nostr relays
|
||||||
|
import * as nostrTools from 'nostr-tools';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a hex string
|
||||||
|
* @param str The string to validate
|
||||||
|
* @param expectedLength The expected length of the hex string
|
||||||
|
* @returns true if valid, false otherwise
|
||||||
|
*/
|
||||||
|
export function isValidHexString(str: string, expectedLength?: number): boolean {
|
||||||
|
const hexRegex = /^[0-9a-f]*$/i;
|
||||||
|
if (!hexRegex.test(str)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it has even length (hex strings should have even length)
|
||||||
|
if (str.length % 2 !== 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check expected length if provided
|
||||||
|
if (expectedLength !== undefined && str.length !== expectedLength) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish an event to a Nostr relay
|
||||||
|
* @param event The Nostr event to publish
|
||||||
|
* @param relayUrl The URL of the relay to publish to
|
||||||
|
* @returns Promise that resolves to a success or error message
|
||||||
|
*/
|
||||||
|
export async function publishToRelay(event: any, relayUrl: string): Promise<string> {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Additional validation specifically for publishing
|
||||||
|
if (!event || typeof event !== 'object') {
|
||||||
|
reject(new Error('Invalid event object'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check required fields
|
||||||
|
if (!event.id || !event.pubkey || !event.sig) {
|
||||||
|
reject(new Error('Event missing required fields (id, pubkey, sig)'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate hex strings
|
||||||
|
if (!isValidHexString(event.id, 64)) {
|
||||||
|
reject(new Error(`Invalid event ID: ${event.id} (must be 64 hex chars)`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidHexString(event.pubkey, 64)) {
|
||||||
|
reject(new Error(`Invalid pubkey: ${event.pubkey} (must be 64 hex chars)`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidHexString(event.sig, 128)) {
|
||||||
|
reject(new Error(`Invalid signature: ${event.sig} (must be 128 hex chars)`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a relay pool for publishing
|
||||||
|
const relayPool = new nostrTools.SimplePool();
|
||||||
|
|
||||||
|
// Set a timeout for the publish operation
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
relayPool.close([relayUrl]);
|
||||||
|
reject(new Error(`Timed out connecting to relay: ${relayUrl}`));
|
||||||
|
}, 10000); // 10 second timeout
|
||||||
|
|
||||||
|
// Publish the event to the relay
|
||||||
|
console.log(`Publishing event to relay: ${relayUrl}`);
|
||||||
|
|
||||||
|
// Create a standard formatted event to ensure it follows the relay protocol
|
||||||
|
const standardEvent = {
|
||||||
|
id: event.id,
|
||||||
|
pubkey: event.pubkey,
|
||||||
|
created_at: Number(event.created_at),
|
||||||
|
kind: Number(event.kind),
|
||||||
|
tags: event.tags,
|
||||||
|
content: event.content,
|
||||||
|
sig: event.sig
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make sure all fields are in the correct format
|
||||||
|
if (typeof standardEvent.created_at !== 'number') {
|
||||||
|
standardEvent.created_at = Math.floor(Date.now() / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof standardEvent.kind !== 'number') {
|
||||||
|
standardEvent.kind = 21120;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Standard event format:', JSON.stringify(standardEvent, null, 2));
|
||||||
|
|
||||||
|
// Debug log the important hex values
|
||||||
|
console.log('ID:', standardEvent.id, 'length:', standardEvent.id.length, 'valid hex?', isValidHexString(standardEvent.id, 64));
|
||||||
|
console.log('Pubkey:', standardEvent.pubkey, 'length:', standardEvent.pubkey.length, 'valid hex?', isValidHexString(standardEvent.pubkey, 64));
|
||||||
|
console.log('Sig:', standardEvent.sig, 'length:', standardEvent.sig.length, 'valid hex?', isValidHexString(standardEvent.sig, 128));
|
||||||
|
|
||||||
|
// Use the standardized event for publishing
|
||||||
|
event = standardEvent;
|
||||||
|
|
||||||
|
// Ensure all hex strings are valid (this is a sanity check beyond our validation)
|
||||||
|
if (!isValidHexString(event.id, 64)) {
|
||||||
|
reject(new Error(`ID is not a valid hex string: ${event.id}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidHexString(event.pubkey, 64)) {
|
||||||
|
reject(new Error(`Pubkey is not a valid hex string: ${event.pubkey}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidHexString(event.sig, 128)) {
|
||||||
|
reject(new Error(`Sig is not a valid hex string: ${event.sig}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try direct WebSocket approach as a fallback
|
||||||
|
try {
|
||||||
|
// Use the WebSocket API directly
|
||||||
|
const ws = new WebSocket(relayUrl);
|
||||||
|
|
||||||
|
let wsTimeout = setTimeout(() => {
|
||||||
|
try {
|
||||||
|
ws.close();
|
||||||
|
} catch (e) {}
|
||||||
|
reject(new Error("WebSocket connection timed out"));
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
// Create a flag to track if we've handled response
|
||||||
|
let responseHandled = false;
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
// Send the event directly using the relay protocol format ["EVENT", event]
|
||||||
|
const messageToSend = JSON.stringify(["EVENT", event]);
|
||||||
|
console.log("Sending WebSocket message:", messageToSend);
|
||||||
|
ws.send(messageToSend);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (msg) => {
|
||||||
|
console.log("WebSocket message received:", msg.data);
|
||||||
|
|
||||||
|
if (responseHandled) {
|
||||||
|
return; // Skip if we've already handled a response
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof msg.data === 'string' && msg.data.startsWith('["OK"')) {
|
||||||
|
responseHandled = true;
|
||||||
|
clearTimeout(wsTimeout);
|
||||||
|
resolve(`Event published successfully via WebSocket`);
|
||||||
|
try {
|
||||||
|
ws.close();
|
||||||
|
} catch (e) {}
|
||||||
|
} else if (typeof msg.data === 'string' && (msg.data.includes('invalid') || msg.data.includes('error'))) {
|
||||||
|
responseHandled = true;
|
||||||
|
clearTimeout(wsTimeout);
|
||||||
|
console.error("WebSocket error response:", msg.data);
|
||||||
|
reject(new Error(`Relay rejected event: ${msg.data}`));
|
||||||
|
try {
|
||||||
|
ws.close();
|
||||||
|
} catch (e) {}
|
||||||
|
} else {
|
||||||
|
console.log("Received other message, waiting for OK or error response");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
if (responseHandled) {
|
||||||
|
return; // Skip if we've already handled a response
|
||||||
|
}
|
||||||
|
|
||||||
|
responseHandled = true;
|
||||||
|
clearTimeout(wsTimeout);
|
||||||
|
console.error("WebSocket error:", error);
|
||||||
|
reject(new Error(`WebSocket error: ${String(error)}`));
|
||||||
|
try {
|
||||||
|
ws.close();
|
||||||
|
} catch (e) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
clearTimeout(wsTimeout);
|
||||||
|
console.log("WebSocket closed");
|
||||||
|
};
|
||||||
|
} catch (wsError) {
|
||||||
|
// If WebSocket fails, try the regular method
|
||||||
|
console.error("WebSocket approach failed:", wsError);
|
||||||
|
|
||||||
|
// Use the nostr-tools publish method as a fallback
|
||||||
|
console.log("Trying nostr-tools publish method...");
|
||||||
|
|
||||||
|
const publishPromises = relayPool.publish([relayUrl], event);
|
||||||
|
|
||||||
|
// Use Promise.all to wait for all promises to resolve
|
||||||
|
Promise.all(publishPromises)
|
||||||
|
.then((relayResults: string[]) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
relayPool.close([relayUrl]);
|
||||||
|
if (relayResults && relayResults.length > 0) {
|
||||||
|
resolve(`Event published to ${relayResults[0]}`);
|
||||||
|
} else {
|
||||||
|
resolve(`Event published successfully`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error: Error) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
relayPool.close([relayUrl]);
|
||||||
|
console.error('Error details:', error);
|
||||||
|
reject(new Error(`Failed to publish event: ${error.message}`));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error(`Error setting up relay connection: ${String(error)}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an npub to a hex pubkey
|
||||||
|
* @param npub The npub to convert
|
||||||
|
* @returns The hex pubkey
|
||||||
|
*/
|
||||||
|
export function convertNpubToHex(npub: string): string | null {
|
||||||
|
if (!npub.startsWith('npub')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = nostrTools.nip19.decode(npub);
|
||||||
|
if (decoded.type === 'npub') {
|
||||||
|
return decoded.data as string;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error decoding npub:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a Nostr event
|
||||||
|
* @param event The event to verify
|
||||||
|
* @returns True if valid, false otherwise
|
||||||
|
*/
|
||||||
|
export function verifyEvent(event: any): boolean {
|
||||||
|
try {
|
||||||
|
return nostrTools.verifyEvent(event);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error verifying event:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
154
client/src/search.ts
Normal file
154
client/src/search.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
// search.ts - Functions for searching for users on Nostr
|
||||||
|
|
||||||
|
import * as nostrTools from 'nostr-tools';
|
||||||
|
|
||||||
|
// Define popular relays to search for users
|
||||||
|
export const POPULAR_RELAYS = [
|
||||||
|
"wss://relay.damus.io",
|
||||||
|
"wss://relay.nostr.band",
|
||||||
|
"wss://nos.lol",
|
||||||
|
"wss://nostr.wine"
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up a NIP-05 address to find the corresponding public key
|
||||||
|
* @param nip05Address The NIP-05 address to look up (e.g. user@example.com)
|
||||||
|
* @returns Promise that resolves to the public key if found, or null if not found
|
||||||
|
*/
|
||||||
|
export async function lookupNip05(nip05Address: string): Promise<{pubkey: string, relays?: string[]} | null> {
|
||||||
|
try {
|
||||||
|
// Use the NIP-05 lookup function from nostr-tools
|
||||||
|
const result = await nostrTools.nip05.queryProfile(nip05Address);
|
||||||
|
|
||||||
|
// If the result has a pubkey, return it
|
||||||
|
if (result && result.pubkey) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error looking up NIP-05 address:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for users based on a search term across popular relays
|
||||||
|
* @param searchTerm The username or NIP-05 identifier to search for
|
||||||
|
* @returns Promise that resolves to an array of user profiles
|
||||||
|
*/
|
||||||
|
export async function searchUsers(searchTerm: string): Promise<Array<{name: string, pubkey: string, npub: string, nip05?: string}>> {
|
||||||
|
const results: Array<{name: string, pubkey: string, npub: string, nip05?: string}> = [];
|
||||||
|
const processedPubkeys = new Set<string>();
|
||||||
|
|
||||||
|
// First, check if the input might already be an npub
|
||||||
|
if (searchTerm.startsWith('npub')) {
|
||||||
|
try {
|
||||||
|
const decoded = nostrTools.nip19.decode(searchTerm);
|
||||||
|
if (decoded.type === 'npub' && decoded.data) {
|
||||||
|
// It's a valid npub, no need to search
|
||||||
|
return [{
|
||||||
|
name: 'Valid npub',
|
||||||
|
pubkey: decoded.data,
|
||||||
|
npub: searchTerm
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Not a valid npub, continue with search
|
||||||
|
console.log("Not a valid npub, continuing with search");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the search term looks like a NIP-05 identifier
|
||||||
|
if (searchTerm.includes('@')) {
|
||||||
|
try {
|
||||||
|
const nip05Result = await lookupNip05(searchTerm);
|
||||||
|
if (nip05Result && nip05Result.pubkey) {
|
||||||
|
const npub = nostrTools.nip19.npubEncode(nip05Result.pubkey);
|
||||||
|
results.push({
|
||||||
|
name: searchTerm,
|
||||||
|
pubkey: nip05Result.pubkey,
|
||||||
|
npub: npub,
|
||||||
|
nip05: searchTerm
|
||||||
|
});
|
||||||
|
processedPubkeys.add(nip05Result.pubkey);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error looking up NIP-05:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a pool of relays to search
|
||||||
|
const relayPool = new nostrTools.SimplePool();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a filter for the subscription
|
||||||
|
const filter = {
|
||||||
|
kinds: [0], // Only metadata events
|
||||||
|
limit: 20 // Limit to 20 results
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set a timeout to ensure we don't wait forever
|
||||||
|
const timeoutPromise = new Promise<void>((resolve) => {
|
||||||
|
setTimeout(() => resolve(), 5000); // 5 second timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an event handler
|
||||||
|
const eventHandler = (event: any) => {
|
||||||
|
try {
|
||||||
|
// Skip if we've already processed this pubkey
|
||||||
|
if (processedPubkeys.has(event.pubkey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the profile metadata from content
|
||||||
|
const profile = JSON.parse(event.content);
|
||||||
|
|
||||||
|
// Check if the profile matches our search term
|
||||||
|
const searchTermLower = searchTerm.toLowerCase();
|
||||||
|
const nameLower = (profile.name || '').toLowerCase();
|
||||||
|
const displayNameLower = (profile.display_name || '').toLowerCase();
|
||||||
|
const nip05Lower = (profile.nip05 || '').toLowerCase();
|
||||||
|
|
||||||
|
if (
|
||||||
|
nameLower.includes(searchTermLower) ||
|
||||||
|
displayNameLower.includes(searchTermLower) ||
|
||||||
|
nip05Lower.includes(searchTermLower)
|
||||||
|
) {
|
||||||
|
// Add to results
|
||||||
|
const npub = nostrTools.nip19.npubEncode(event.pubkey);
|
||||||
|
results.push({
|
||||||
|
name: profile.display_name || profile.name || 'Unknown',
|
||||||
|
pubkey: event.pubkey,
|
||||||
|
npub: npub,
|
||||||
|
nip05: profile.nip05
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark as processed
|
||||||
|
processedPubkeys.add(event.pubkey);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing event:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subscribe to events matching the filter
|
||||||
|
const sub = relayPool.subscribeMany(
|
||||||
|
POPULAR_RELAYS,
|
||||||
|
[filter],
|
||||||
|
{ onevent: eventHandler }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for timeout
|
||||||
|
await timeoutPromise;
|
||||||
|
|
||||||
|
// Close the subscription
|
||||||
|
sub.close();
|
||||||
|
relayPool.close(POPULAR_RELAYS);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error searching relays:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
@ -1,3 +1,213 @@
|
|||||||
|
/* Styles for the HTTP to Nostr converter */
|
||||||
|
|
||||||
|
/* General layout */
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #212529;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Headings */
|
||||||
|
h1 {
|
||||||
|
color: #343a40;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #6c757d;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #495057;
|
||||||
|
margin-top: 25px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Info box */
|
||||||
|
.info-box {
|
||||||
|
background-color: #e2f0fd;
|
||||||
|
border-left: 4px solid #0d6efd;
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form elements */
|
||||||
|
input[type="text"], textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
height: 150px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #0b5ed7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Server input section */
|
||||||
|
.server-input-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-input {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-search-button {
|
||||||
|
padding: 8px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-search-result {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Output section */
|
||||||
|
#output {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Publish container */
|
||||||
|
.publish-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f1f8ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #b3d7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-input-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-input {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-button {
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-button:hover {
|
||||||
|
background-color: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-result {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* QR code container */
|
||||||
|
.qr-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#qrCode {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search results styling */
|
||||||
|
.search-results-list {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-item {
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-name {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-npub {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-nip05 {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-npub-btn {
|
||||||
|
align-self: flex-end;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
@ -38,6 +248,91 @@ button:hover {
|
|||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.qr-container {
|
||||||
|
margin-top: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#qrCode {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: block;
|
||||||
|
background-color: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-info {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #555;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-frame-container {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-frame {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-controls {
|
||||||
|
margin: 15px auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-controls button {
|
||||||
|
margin: 0 5px;
|
||||||
|
padding: 8px 15px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-controls button:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-error {
|
||||||
|
background-color: #ffeeee;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid #ffcccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
text-align: center;
|
||||||
|
color: #cc0000;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-error-container {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-error-container h3 {
|
||||||
|
color: #cc0000;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@ -60,4 +355,70 @@ h1 {
|
|||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
color: #555;
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NIP-05 lookup styles */
|
||||||
|
.nip05-section {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nip05-input-container {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nip05-input {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nip05-button {
|
||||||
|
white-space: nowrap;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nip05-button:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nip05-result {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nip05-success {
|
||||||
|
color: #008800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nip05-error {
|
||||||
|
color: #cc0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-pubkey-button {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-pubkey-button:hover {
|
||||||
|
background-color: #45a049;
|
||||||
}
|
}
|
124
client/src/utils.ts
Normal file
124
client/src/utils.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
// utils.ts - Utility functions for the HTTP to Nostr converter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populate the HTTP request textarea with a default example
|
||||||
|
*/
|
||||||
|
export function setDefaultHttpRequest(): void {
|
||||||
|
const httpRequestBox = document.getElementById('httpRequest') as HTMLTextAreaElement;
|
||||||
|
if (httpRequestBox) {
|
||||||
|
// Only set default if the textarea is empty
|
||||||
|
if (!httpRequestBox.value.trim()) {
|
||||||
|
const defaultRequest = `GET /index.html HTTP/1.1
|
||||||
|
Host: example.com
|
||||||
|
User-Agent: NostrClient/1.0
|
||||||
|
Accept: text/html,application/xhtml+xml,application/xml
|
||||||
|
Connection: keep-alive
|
||||||
|
|
||||||
|
`;
|
||||||
|
httpRequestBox.value = defaultRequest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize text to remove non-printable characters
|
||||||
|
* @param text Text to sanitize
|
||||||
|
* @returns Sanitized text
|
||||||
|
*/
|
||||||
|
export function sanitizeText(text: string): string {
|
||||||
|
return text.replace(/[^\x20-\x7E]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process event tags to convert any npub values in "p" tags to hex pubkeys
|
||||||
|
* @param tags The event tags to process
|
||||||
|
* @returns The processed tags
|
||||||
|
*/
|
||||||
|
export function processTags(tags: string[][]): string[][] {
|
||||||
|
if (!tags || !Array.isArray(tags)) {
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a deep copy of tags to avoid modifying the original
|
||||||
|
const processedTags = JSON.parse(JSON.stringify(tags));
|
||||||
|
|
||||||
|
for (let i = 0; i < processedTags.length; i++) {
|
||||||
|
const tag = processedTags[i];
|
||||||
|
if (!tag || !Array.isArray(tag) || tag.length < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert npub in "p" tags to hex pubkeys
|
||||||
|
if (tag[0] === 'p' && typeof tag[1] === 'string' && tag[1].startsWith('npub')) {
|
||||||
|
try {
|
||||||
|
console.log(`Processing p tag: ${tag[1]}`);
|
||||||
|
const { convertNpubToHex } = require('./relay');
|
||||||
|
const hexPubkey = convertNpubToHex(tag[1]);
|
||||||
|
if (hexPubkey) {
|
||||||
|
processedTags[i][1] = hexPubkey;
|
||||||
|
console.log(`Converted npub to hex: ${hexPubkey}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error processing p tag: ${tag[1]}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure expiration tag value is a string
|
||||||
|
if (tag[0] === 'expiration' && tag[1]) {
|
||||||
|
processedTags[i][1] = String(tag[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedTags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a standardized event object with proper types
|
||||||
|
* @param event The event to standardize
|
||||||
|
* @returns Standardized event
|
||||||
|
*/
|
||||||
|
export function standardizeEvent(event: any): any {
|
||||||
|
if (!event || typeof event !== 'object') {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: event.id,
|
||||||
|
pubkey: event.pubkey,
|
||||||
|
created_at: Number(event.created_at),
|
||||||
|
kind: Number(event.kind),
|
||||||
|
tags: processTags(event.tags),
|
||||||
|
content: event.content,
|
||||||
|
sig: event.sig
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display an error message in the specified element
|
||||||
|
* @param element The element to display the error in
|
||||||
|
* @param message The error message
|
||||||
|
*/
|
||||||
|
export function showError(element: HTMLElement, message: string): void {
|
||||||
|
element.innerHTML = `<span style="color: #cc0000;">Error: ${message}</span>`;
|
||||||
|
element.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a success message in the specified element
|
||||||
|
* @param element The element to display the success message in
|
||||||
|
* @param message The success message
|
||||||
|
*/
|
||||||
|
export function showSuccess(element: HTMLElement, message: string): void {
|
||||||
|
element.innerHTML = `<span style="color: #008800;">✓ ${message}</span>`;
|
||||||
|
element.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a loading message in the specified element
|
||||||
|
* @param element The element to display the loading message in
|
||||||
|
* @param message The loading message
|
||||||
|
*/
|
||||||
|
export function showLoading(element: HTMLElement, message: string = 'Processing...'): void {
|
||||||
|
element.innerHTML = `<span>${message}</span>`;
|
||||||
|
element.style.display = 'block';
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const CopyPlugin = require('copy-webpack-plugin');
|
const CopyPlugin = require('copy-webpack-plugin');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
|
// Polyfills for Node.js core modules in the browser
|
||||||
|
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode: 'development',
|
mode: 'development',
|
||||||
@ -21,10 +23,7 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.ProvidePlugin({
|
new NodePolyfillPlugin(),
|
||||||
Buffer: ['buffer', 'Buffer'],
|
|
||||||
process: 'process/browser',
|
|
||||||
}),
|
|
||||||
new CopyPlugin({
|
new CopyPlugin({
|
||||||
patterns: [
|
patterns: [
|
||||||
{ from: 'src/styles.css', to: 'styles.css' },
|
{ from: 'src/styles.css', to: 'styles.css' },
|
||||||
@ -33,20 +32,12 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
resolve: {
|
|
||||||
extensions: ['.tsx', '.ts', '.js'],
|
|
||||||
},
|
|
||||||
output: {
|
output: {
|
||||||
filename: 'bundle.js',
|
filename: 'bundle.js',
|
||||||
path: path.resolve(__dirname, 'dist'),
|
path: path.resolve(__dirname, 'dist'),
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.tsx', '.ts', '.js'],
|
extensions: ['.tsx', '.ts', '.js'],
|
||||||
fallback: {
|
|
||||||
"crypto": false,
|
|
||||||
"buffer": false,
|
|
||||||
"stream": false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
devServer: {
|
devServer: {
|
||||||
static: {
|
static: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user