1import { signMessage, encryptChannelMessage, encryptDirectMessage, decryptChannelMessage, decryptDirectMessage } from './crypto.js'; 2import { stampTTL } from './ttl.js'; 3import { padMessage } from './padding.js'; 4import { generatePseudonym, shortenPseudonym } from './pseudonyms.js'; 5import { broadcastMessage } from './network.js'; 6import { addTrafficEntry } from './network-panel.js'; 7 8// Message history storage â also used by bots.js via injectBotMessage 9const messageHistory = {}; 10 11let messageIdCounter = 0; 12 13function generateMessageId() { 14 return `msg_${Date.now()}_${messageIdCounter++}_${Math.random().toString(36).slice(2, 8)}`; 15} 16 17export function updateChannelName(oldName, newName) { 18 if (messageHistory.hasOwnProperty(oldName)) { 19 messageHistory[newName] = messageHistory[oldName]; 20 messageHistory[oldName] = null; 21 } 22} 23 24// Display messages in the UI 25export function displayMessages(name = null) { 26 const messagesDiv = document.getElementById("messages"); 27 const messagesHeader = document.getElementById("messagesHeader"); 28 29 if (name) { 30 messagesHeader.textContent = name; 31 32 if (!messageHistory[name]) { 33 messageHistory[name] = []; 34 } 35 36 messagesDiv.innerHTML = ''; 37 messageHistory[name].forEach(msg => { 38 const messageElement = document.createElement("div"); 39 messageElement.className = "message mb-2 p-2 border-bottom"; 40 41 const headerDiv = document.createElement("div"); 42 headerDiv.className = "d-flex justify-content-between align-items-baseline"; 43 44 const senderStrong = document.createElement("strong"); 45 senderStrong.className = "text-secondary"; 46 senderStrong.textContent = msg.sender; 47 48 const timestampSmall = document.createElement("small"); 49 timestampSmall.className = "text-muted"; 50 timestampSmall.textContent = msg.timestamp; 51 52 headerDiv.appendChild(senderStrong); 53 headerDiv.appendChild(timestampSmall); 54 55 const contentDiv = document.createElement("div"); 56 contentDiv.className = "message-content mt-1"; 57 contentDiv.textContent = msg.content; 58 59 messageElement.appendChild(headerDiv); 60 messageElement.appendChild(contentDiv); 61 messagesDiv.appendChild(messageElement); 62 }); 63 64 if (messageHistory[name].length === 0) { 65 const emptyMessage = document.createElement("p"); 66 emptyMessage.className = "text-center text-muted"; 67 emptyMessage.textContent = `Messages for ${name} will appear here.`; 68 messagesDiv.appendChild(emptyMessage); 69 } 70 } else { 71 messagesHeader.textContent = "Messages"; 72 messagesDiv.innerHTML = ` 73 <div class="text-center p-4"> 74 <h5 class="text-navy mb-3" style="color: #000033;">Welcome to HermesP2P Chat</h5> 75 <p class="mb-3 fs-6" style="color: #000033;">Explore the secure, peer-to-peer network by selecting a channel or friend from the lists on the left. From there, you can view and send messages with ease. Want to personalize your experience? You can expand your network:</p> 76 <div class="px-4"> 77 <ul class="list-unstyled text-start mb-3 ps-5"> 78 <li class="mb-3 ps-3"> 79 <strong class="d-block mb-2">Add a Channel:</strong> 80 <ul class="list-unstyled ps-4"> 81 <li class="mb-1">⢠Public channels require no keys</li> 82 <li class="mb-1">⢠Private channels require a public/private key pair</li> 83 </ul> 84 </li> 85 <li class="ps-3"> 86 <strong class="d-block mb-2">Add a Friend:</strong> 87 <ul class="list-unstyled ps-4"> 88 <li>⢠Enter their public key</li> 89 </ul> 90 </li> 91 </ul> 92 </div> 93 <p class="fs-6" style="color: #000033;">Feel free to remove channels or friends as your preferences change. When you're finished, remember to Save and Exit to download your updated configuration file and keep your settings for next time. Dive in and enjoy a secure, customizable messaging experience!</p> 94 </div>`; 95 } 96 97 messagesDiv.scrollTop = messagesDiv.scrollHeight; 98} 99 100// Append a new message 101function appendMessage(sender, content, type = 'public') { 102 const activeChat = document.querySelector('.list-group-item.active'); 103 if (!activeChat) return; 104 105 const chatName = activeChat.querySelector('span').textContent; 106 if (!messageHistory[chatName]) { 107 messageHistory[chatName] = []; 108 } 109 110 const now = new Date(); 111 const timestamp = now.toLocaleTimeString(); 112 let displayContent = content; 113 114 switch (type) { 115 case 'private': 116 case 'direct': 117 displayContent = `ð âï¸ ${content}`; 118 break; 119 case 'public': 120 displayContent = `âï¸ ${content}`; 121 break; 122 } 123 124 messageHistory[chatName].push({ sender, content: displayContent, timestamp }); 125 displayMessages(chatName); 126} 127 128// Enable message input functionality 129export function enableMessageInput(configuration) { 130 const messageForm = document.getElementById('messageForm'); 131 const messageInput = document.getElementById('messageInput'); 132 133 messageForm.classList.remove('disabled'); 134 messageInput.disabled = false;
135 messageInput.placeholder = 'Type your message...'; 136 137 messageForm.onsubmit = async (e) => { 138 e.preventDefault(); 139 const message = messageInput.value.trim(); 140 if (!message) return; 141 142 const activeChat = document.querySelector('.list-group-item.active'); 143 if (!activeChat) return; 144 145 const chatName = activeChat.querySelector('span').textContent; 146 const isFriend = activeChat.closest('#friends-list') !== null; 147 148 const messageType = isFriend ? 'direct' : 149 (configuration.channels.find(c => c.name === chatName)?.key ? 'private' : 'public'); 150 151 try { 152 const paddedFrame = await packageMessage(message, messageType, chatName, configuration); 153 broadcastMessage(paddedFrame); 154 addTrafficEntry(messageType, chatName, messageType === 'public' ? message : null, configuration.user.name, 'out'); 155 messageInput.value = ''; 156 appendMessage(configuration.user.name, message, messageType); 157 158 // Notify bots about outgoing DMs 159 window.dispatchEvent(new CustomEvent('hermesBotCheck', { 160 detail: { to: chatName, type: messageType } 161 })); 162 } catch (err) { 163 if (err instanceof RangeError) { 164 appendMessage('System', 'Message too long (max 64KB)', 'public'); 165 } else { 166 console.error('Failed to send message:', err); 167 } 168 } 169 }; 170} 171 172/** 173 * Inject a bot message into message history and refresh display if active. 174 * Used by bots.js to post without going through the network. 175 * @param {string} channelOrFriendName - Target channel or friend name 176 * @param {string} senderName - Display name / pseudonym of the sender 177 * @param {string} content - Message content 178 * @param {'public'|'private'|'direct'} type - Message type 179 */ 180export function injectBotMessage(channelOrFriendName, senderName, content, type = 'public') { 181 if (!messageHistory[channelOrFriendName]) { 182 messageHistory[channelOrFriendName] = []; 183 } 184 185 const now = new Date(); 186 const timestamp = now.toLocaleTimeString(); 187 let displayContent = content; 188 189 switch (type) { 190 case 'private': 191 case 'direct': 192 displayContent = `\u{1F512} ${content}`; 193 break; 194 } 195 196 messageHistory[channelOrFriendName].push({ sender: senderName, content: displayContent, timestamp }); 197 198 // Refresh display if this channel/friend is currently active 199 const activeChat = document.querySelector('.list-group-item.active'); 200 if (activeChat) { 201 const activeName = activeChat.querySelector('span').textContent; 202 if (activeName === channelOrFriendName) { 203 displayMessages(channelOrFriendName); 204 } 205 } 206} 207 208/** 209 * Handle an incoming message from the network relay. 210 * Decrypts if needed and injects into the message display. 211 */ 212export function handleIncomingNetworkMessage(url, messagePackage, configuration) { 213 const { type, to, from, message } = messagePackage; 214 let content = message; 215 let displayTarget = to; 216 217 if (type === 'private') { 218 let parsed; 219 try { 220 parsed = JSON.parse(message); 221 } catch { 222 return; 223 } 224 // Try all channel keys â the key is the channel identity, not the name 225 let matched = false; 226 for (const channel of configuration.channels) { 227 if (!channel.key) continue; 228 const decrypted = decryptChannelMessage(parsed.encrypted, parsed.nonce, channel.key); 229 if (decrypted) { 230 content = decrypted; 231 displayTarget = channel.name; 232 matched = true; 233 break; 234 } 235 } 236 if (!matched) return; // no key could decrypt â not our channel 237 } else if (type === 'direct') { 238 if (from?.pubKey) { 239 const friend = configuration.friends.find(f => f.pubKey === from.pubKey); 240 displayTarget = friend ? friend.name : from.name; 241 } else { 242 displayTarget = from?.name || 'Unknown'; 243 } 244 try { 245 const parsed = JSON.parse(message); 246 content = decryptDirectMessage(parsed.encrypted, parsed.nonce, parsed.ephemeralPubKey, configuration.user.privKey); 247 if (!content) return; // can't decrypt â not for us 248 } catch { 249 return; 250 } 251 } 252 253 const senderName = from?.name || 'Anonymous'; 254 const displaySender = configuration.aliases?.[senderName] || senderName; 255 injectBotMessage(displayTarget, displaySender, content, type); 256} 257 258export function disableMessageInput() { 259 const messageForm = document.getElementById('messageForm'); 260 const messageInput = document.getElementById('messageInput'); 261 messageForm.classList.add('disabled'); 262 messageInput.disabled = true; 263 messageInput.placeholder = 'Select a chat to send messages'; 264} 265
266/** 267 * Handle message submission (called from ui.js form handler). 268 * Now async to support pseudonym generation. 269 */ 270export async function handleMessageSubmit(e, configuration) { 271 if (!configuration || !configuration.user) { 272 console.error('Invalid configuration in handleMessageSubmit'); 273 return; 274 } 275 e.preventDefault(); 276 const messageInput = document.getElementById("messageInput"); 277 const message = messageInput.value.trim(); 278 const activeChat = document.querySelector('.list-group-item.active'); 279 280 if (message && activeChat) { 281 const chatName = activeChat.querySelector('span').textContent; 282 const isChannel = configuration.channels.some(channel => channel.name === chatName); 283 let messageType; 284 285 if (isChannel) { 286 const channel = configuration.channels.find(ch => ch.name === chatName); 287 messageType = channel.key ? 'private' : 'public'; 288 } else { 289 messageType = 'direct'; 290 } 291 292 try { 293 const paddedFrame = await packageMessage(message, messageType, chatName, configuration); 294 295 // Dispatch message broadcast event 296 window.dispatchEvent(new CustomEvent('messageBroadcast', { 297 detail: { type: messageType, to: chatName } 298 })); 299 300 broadcastMessage(paddedFrame); 301 addTrafficEntry(messageType, chatName, messageType === 'public' ? message : null, configuration.user.name, 'out'); 302 appendMessage(configuration.user.name, message, messageType); 303 messageInput.value = ''; 304 } catch (err) { 305 if (err instanceof RangeError) { 306 appendMessage('System', 'Message too long (max 64KB)', 'public'); 307 } else { 308 console.error('Failed to send message:', err); 309 } 310 } 311 } 312} 313 314/** 315 * Builds, encrypts, stamps TTL, signs, serializes, and pads a message. 316 * Returns a padded Uint8Array ready for binary broadcast. 317 */ 318export async function packageMessage(plaintext, type, to, configuration) { 319 if (!configuration || !configuration.user) { 320 throw new Error('Invalid configuration passed to packageMessage'); 321 } 322 323 // Encrypt content based on type 324 let messageContent; 325 if (type === 'private') { 326 const channel = configuration.channels.find(ch => ch.name === to); 327 if (channel?.key) { 328 const [encrypted, nonce] = encryptChannelMessage(plaintext, channel.key); 329 messageContent = JSON.stringify({ encrypted, nonce }); 330 } else { 331 messageContent = plaintext; 332 } 333 } else if (type === 'direct') { 334 const friend = configuration.friends.find(f => f.name === to); 335 if (friend) { 336 const [encrypted, nonce, ephemeralPubKey] = encryptDirectMessage(plaintext, friend.pubKey); 337 messageContent = JSON.stringify({ encrypted, nonce, ephemeralPubKey }); 338 } else { 339 messageContent = plaintext; 340 } 341 } else { 342 messageContent = plaintext; 343 } 344 345 // Build message package 346 const messagePackage = { 347 id: generateMessageId(), 348 type: type, 349 timestamp: Date.now(), 350 to: to, 351 from: { 352 name: configuration.user.name, 353 pubKey: configuration.user.pubKey 354 }, 355 message: messageContent, 356 signature: '', 357 }; 358 359 // Stamp TTL 360 stampTTL(messagePackage, type); 361 362 // Channel messages: remove pubKey for anonymity, replace name with pseudonym 363 if (type === 'private' || type === 'public') { 364 delete messagePackage.from.pubKey; 365 try { 366 const pseudonym = await generatePseudonym(configuration.user.privKey, to); 367 messagePackage.from.name = shortenPseudonym(pseudonym, 16); 368 } catch (err) { 369 console.warn('Pseudonym generation failed, using username:', err); 370 } 371 } 372 373 // Sign the message content 374 messagePackage.signature = signMessage(messageContent, configuration.user.privKey); 375 376 // Serialize and pad 377 const jsonStr = JSON.stringify(messagePackage); 378 const jsonBytes = new TextEncoder().encode(jsonStr); 379 return padMessage(jsonBytes); 380}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.