fix: search for npub
This commit is contained in:
parent
6a5b9805bc
commit
c976746b07
@ -21,9 +21,17 @@
|
||||
<h2>Server Information:</h2>
|
||||
<div style="margin-bottom: 15px;">
|
||||
<div style="margin-bottom: 10px;">
|
||||
<label for="serverPubkey">Server Pubkey:</label><br>
|
||||
<input type="text" id="serverPubkey" value="npub1r6knexka25dn9w9jnf5kf8xh6gfq7n3p38zfl7nn7cjjjsp4umcqnk0aun" style="width: 100%; padding: 8px;">
|
||||
<label for="serverPubkey">Server Pubkey or Search Term:</label><br>
|
||||
<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>
|
||||
<label for="relay">Response Relay (optional):</label><br>
|
||||
<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>
|
||||
<h2>Converted Event:</h2>
|
||||
<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>
|
||||
</body>
|
||||
</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": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/qrcode-generator": "^1.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.29.0",
|
||||
"@typescript-eslint/parser": "^8.29.0",
|
||||
"buffer": "^6.0.3",
|
||||
@ -31,6 +33,7 @@
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"husky": "^9.1.7",
|
||||
"node-polyfill-webpack-plugin": "^4.1.0",
|
||||
"process": "^0.11.10",
|
||||
"serve": "^14.0.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
@ -42,6 +45,8 @@
|
||||
"webpack-dev-server": "^5.2.1"
|
||||
},
|
||||
"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
|
||||
// This follows strict CSP policies by avoiding inline scripts
|
||||
|
||||
// Import the converter function
|
||||
// Import functions from other modules
|
||||
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
|
||||
document.addEventListener('DOMContentLoaded', function(): void {
|
||||
// Set up the convert button click handler
|
||||
const convertButton = document.getElementById('convertButton');
|
||||
const searchButton = document.getElementById('searchServerBtn');
|
||||
const publishButton = document.getElementById('publishButton');
|
||||
|
||||
if (convertButton) {
|
||||
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');
|
||||
});
|
@ -7,6 +7,9 @@ declare global {
|
||||
}
|
||||
import { defaultServerConfig, appSettings } from './config';
|
||||
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)
|
||||
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
|
||||
let serverPubkeyHex = serverPubkey;
|
||||
|
||||
// Convert npub to hex if needed using nostr-tools
|
||||
// Convert npub to hex if needed
|
||||
if (serverPubkey.startsWith('npub')) {
|
||||
try {
|
||||
const decoded = nostrTools.nip19.decode(serverPubkey);
|
||||
if (decoded.type === 'npub' && decoded.data) {
|
||||
serverPubkeyHex = decoded.data;
|
||||
console.log("Converted npub to hex format:", serverPubkeyHex);
|
||||
}
|
||||
} catch (decodeError) {
|
||||
console.error("Failed to decode npub:", decodeError);
|
||||
const hexPubkey = convertNpubToHex(serverPubkey);
|
||||
if (hexPubkey) {
|
||||
serverPubkeyHex = hexPubkey;
|
||||
console.log("Converted npub to hex format:", serverPubkeyHex);
|
||||
} else {
|
||||
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.");
|
||||
}
|
||||
|
||||
// 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 = {
|
||||
kind: 21120,
|
||||
pubkey: pubkey,
|
||||
content: encryptedContent, // Encrypted HTTP request using NIP-44
|
||||
tags: [
|
||||
// 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
|
||||
["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) {
|
||||
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);
|
||||
}
|
||||
@ -156,6 +175,9 @@ export async function displayConvertedEvent(): Promise<void> {
|
||||
);
|
||||
|
||||
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
|
||||
const parsedEvent = JSON.parse(convertedEvent);
|
||||
const nostrEvent = {
|
||||
@ -165,6 +187,9 @@ export async function displayConvertedEvent(): Promise<void> {
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: parsedEvent.pubkey
|
||||
};
|
||||
|
||||
// Log the event being signed
|
||||
console.log("Event to be signed:", JSON.stringify(nostrEvent, null, 2));
|
||||
|
||||
let signedEvent;
|
||||
|
||||
@ -172,6 +197,7 @@ export async function displayConvertedEvent(): Promise<void> {
|
||||
try {
|
||||
// Try to sign with the NIP-07 extension
|
||||
signedEvent = await window.nostr.signEvent(nostrEvent);
|
||||
console.log("Event signed with extension:", signedEvent);
|
||||
} catch (error) {
|
||||
console.error("Error signing event with extension:", error);
|
||||
// Fall back to signing with nostr-tools
|
||||
@ -191,9 +217,211 @@ export async function displayConvertedEvent(): Promise<void> {
|
||||
outputDiv.hidden = false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -201,9 +429,28 @@ export async function displayConvertedEvent(): Promise<void> {
|
||||
|
||||
// Initialize event listeners when the DOM is fully loaded
|
||||
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 publishButton = document.getElementById('publishButton');
|
||||
|
||||
if (convertButton) {
|
||||
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 {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
@ -38,6 +248,91 @@ button:hover {
|
||||
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 {
|
||||
background-color: #f5f5f5;
|
||||
padding: 10px;
|
||||
@ -60,4 +355,70 @@ h1 {
|
||||
|
||||
h2 {
|
||||
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 CopyPlugin = require('copy-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
// Polyfills for Node.js core modules in the browser
|
||||
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
@ -21,10 +23,7 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
process: 'process/browser',
|
||||
}),
|
||||
new NodePolyfillPlugin(),
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{ from: 'src/styles.css', to: 'styles.css' },
|
||||
@ -33,20 +32,12 @@ module.exports = {
|
||||
],
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
},
|
||||
output: {
|
||||
filename: 'bundle.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
fallback: {
|
||||
"crypto": false,
|
||||
"buffer": false,
|
||||
"stream": false
|
||||
}
|
||||
},
|
||||
devServer: {
|
||||
static: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user