PageSourceSearch

https://hp2p.net/static/js/messages.js

js hp2p.net collected 2026-09-28 07:08:57 UTC 14,792 bytes, 380 lines download raw bytes

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.