import { relayInit, generatePrivateKey, getPublicKey, getEventHash, signEvent, nip19, nip04 } from 'nostr-tools'; // CONFIGURATION - Only need CS team's pubkey! const CONFIG = { relays: [ 'wss://relay.damus.io', 'wss://relay.primal.net', 'wss://nos.lol', 'wss://relay.btcforplebs.com', 'wss://relay.logemedia.com' ], csPubkey: 'PUBKEY_TO_RECEICE_MESSAGES' // Replace with actual pubkey }; // State let state = { isOpen: false, messages: [], inputMessage: '', myPrivKey: null, myPubKey: null, relays: [], connected: false, sessionId: null }; // Generate or retrieve session key from localStorage function getSessionKey() { const stored = localStorage.getItem('nostr_chat_session'); if (stored) { try { const session = JSON.parse(stored); // Reuse if less than 24 hours old if (Date.now() - session.created < 24 * 60 * 60 * 1000) { return session.privKey; } } catch (e) {} } // Generate new ephemeral key const privKey = generatePrivateKey(); localStorage.setItem('nostr_chat_session', JSON.stringify({ privKey, created: Date.now() })); return privKey; } // Initialize async function init() { // Get or create session key state.myPrivKey = getSessionKey(); state.myPubKey = getPublicKey(state.myPrivKey); state.sessionId = state.myPubKey.substring(0, 8); console.log('🔑 Session Identity:', nip19.npubEncode(state.myPubKey)); // Connect to relays const relayPromises = CONFIG.relays.map(async (url) => { try { const relay = relayInit(url); relay.on('connect', () => { console.log(`✓ Connected to ${url}`); checkConnection(); }); relay.on('disconnect', () => { console.log(`✗ Disconnected from ${url}`); }); await relay.connect(); return relay; } catch (error) { console.error(`Failed: ${url}:`, error); return null; } }); state.relays = (await Promise.all(relayPromises)).filter(r => r !== null); if (state.relays.length === 0) { addMessage('system', '⚠️ Failed to connect to any relays'); return; } console.log(`✓ Connected to ${state.relays.length}/${CONFIG.relays.length} relays`); // Subscribe to replies from CS team subscribeToReplies(); // Load any previous messages from this session loadPreviousMessages(); state.connected = true; render(); } function checkConnection() { const connected = state.relays.some(r => r.status === 1); // 1 = connected state.connected = connected; render(); } // Subscribe to DMs from CS team function subscribeToReplies() { const filters = [{ kinds: [4], '#p': [state.myPubKey], authors: [CONFIG.csPubkey], since: Math.floor(Date.now() / 1000) - 86400 // Last 24 hours }]; console.log('🔔 Subscribing to CS team replies...'); state.relays.forEach(relay => { const sub = relay.sub(filters); sub.on('event', (event) => { handleIncomingMessage(event); }); sub.on('eose', () => { console.log(`✓ Subscribed: ${relay.url}`); }); }); } // Load previous messages from localStorage function loadPreviousMessages() { const stored = localStorage.getItem(`nostr_chat_messages_${state.sessionId}`); if (stored) { try { const messages = JSON.parse(stored); messages.forEach(msg => state.messages.push(msg)); render(); } catch (e) {} } } // Save messages to localStorage function saveMessages() { localStorage.setItem(`nostr_chat_messages_${state.sessionId}`, JSON.stringify(state.messages)); } // Handle incoming DM from CS team async function handleIncomingMessage(event) { try { // Check for duplicates if (state.messages.find(m => m.id === event.id)) { return; } console.log('📨 Received message from CS team'); // Decrypt const decryptedText = await nip04.decrypt( state.myPrivKey, event.pubkey, event.content ); const message = { id: event.id, text: decryptedText, sender: 'cs', timestamp: new Date(event.created_at * 1000).toISOString() }; addMessage('cs', decryptedText, message); // Notification if (!document.hasFocus()) { document.title = '💬 New message!'; setTimeout(() => { document.title = 'Nostr Support Chat'; }, 3000); } } catch (error) { console.error('Error decrypting message:', error); } } // Send message to CS team async function sendMessage() { if (!state.inputMessage.trim()) return; const messageText = state.inputMessage; try { console.log('🔐 Encrypting and sending...'); // Encrypt const encrypted = await nip04.encrypt( state.myPrivKey, CONFIG.csPubkey, messageText ); // Create event let event = { kind: 4, created_at: Math.floor(Date.now() / 1000), tags: [['p', CONFIG.csPubkey]], content: encrypted, pubkey: state.myPubKey }; event.id = getEventHash(event); event.sig = signEvent(event, state.myPrivKey); // Publish to all relays let published = 0; for (const relay of state.relays) { try { await relay.publish(event); published++; console.log(`✓ Published to ${relay.url}`); } catch (err) { console.error(`✗ Failed: ${relay.url}:`, err); } } if (published === 0) { addMessage('system', '⚠️ Failed to send - no relay connections'); return; } console.log(`✓ Published to ${published}/${state.relays.length} relays`); // Add to local messages const message = { id: event.id, text: messageText, sender: 'user', timestamp: new Date().toISOString() }; addMessage('user', messageText, message); state.inputMessage = ''; render(); } catch (error) { console.error('Error sending:', error); addMessage('system', '⚠️ Failed to send message'); } } function addMessage(sender, text, fullMessage = null) { const msg = fullMessage || { id: Date.now().toString(), text, sender, timestamp: new Date().toISOString() }; state.messages.push(msg); saveMessages(); render(); scrollToBottom(); } function scrollToBottom() { setTimeout(() => { const container = document.getElementById('messages'); if (container) { container.scrollTop = container.scrollHeight; } }, 100); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function formatTime(timestamp) { const date = new Date(timestamp); return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }); } // Render function with mobile responsiveness function render() { const container = document.getElementById('chat-widget-root'); if (!container) return; if (!state.isOpen) { container.innerHTML = `
`; return; } // Full chat box rendering with mobile responsiveness container.innerHTML = `

Instant Chat

${state.connected ? `P2P E2EE • ${state.relays.length} relays` : 'Connecting...'}
${state.messages.length === 0 ? `

Start a conversation

` : state.messages.map(msg => { if (msg.sender === 'system') { return `
${escapeHtml(msg.text)}
`; } else if (msg.sender === 'user') { return `
${escapeHtml(msg.text)}
${formatTime(msg.timestamp)}
`; } else if (msg.sender === 'cs') { return `
Loge Media Team
${escapeHtml(msg.text)}
${formatTime(msg.timestamp)}
`; } return ''; }).join('')}
`; const messageInput = document.getElementById('message-input'); if (messageInput) { messageInput.addEventListener('input', (e) => { state.inputMessage = e.target.value; const sendButton = document.querySelector('button[onclick="window.sendMessage()"]'); if (sendButton) { sendButton.disabled = !state.connected || !e.target.value.trim(); } }); messageInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); // Auto-scroll to bottom on mobile const messagesContainer = document.getElementById('messages'); if (messagesContainer && state.messages.length > 0) { setTimeout(() => { messagesContainer.scrollTop = messagesContainer.scrollHeight; }, 100); } messageInput.focus(); } } // Global functions window.openChat = async () => { state.isOpen = true; render(); if (state.relays.length === 0) { await init(); } }; window.closeChat = () => { state.isOpen = false; render(); }; window.sendMessage = sendMessage; // Initial render render();