bitchat-node 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +223 -0
  2. package/dist/bin/bitchat.d.ts +7 -0
  3. package/dist/bin/bitchat.d.ts.map +1 -0
  4. package/dist/bin/bitchat.js +69 -0
  5. package/dist/bin/bitchat.js.map +1 -0
  6. package/dist/client.d.ts +77 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +411 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/crypto/index.d.ts +6 -0
  11. package/dist/crypto/index.d.ts.map +1 -0
  12. package/dist/crypto/index.js +6 -0
  13. package/dist/crypto/index.js.map +1 -0
  14. package/dist/crypto/noise.d.ts +72 -0
  15. package/dist/crypto/noise.d.ts.map +1 -0
  16. package/dist/crypto/noise.js +470 -0
  17. package/dist/crypto/noise.js.map +1 -0
  18. package/dist/crypto/signing.d.ts +34 -0
  19. package/dist/crypto/signing.d.ts.map +1 -0
  20. package/dist/crypto/signing.js +56 -0
  21. package/dist/crypto/signing.js.map +1 -0
  22. package/dist/index.d.ts +32 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +48 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/mesh/deduplicator.d.ts +48 -0
  27. package/dist/mesh/deduplicator.d.ts.map +1 -0
  28. package/dist/mesh/deduplicator.js +107 -0
  29. package/dist/mesh/deduplicator.js.map +1 -0
  30. package/dist/mesh/index.d.ts +6 -0
  31. package/dist/mesh/index.d.ts.map +1 -0
  32. package/dist/mesh/index.js +6 -0
  33. package/dist/mesh/index.js.map +1 -0
  34. package/dist/mesh/router.d.ts +90 -0
  35. package/dist/mesh/router.d.ts.map +1 -0
  36. package/dist/mesh/router.js +204 -0
  37. package/dist/mesh/router.js.map +1 -0
  38. package/dist/protocol/binary.d.ts +37 -0
  39. package/dist/protocol/binary.d.ts.map +1 -0
  40. package/dist/protocol/binary.js +310 -0
  41. package/dist/protocol/binary.js.map +1 -0
  42. package/dist/protocol/constants.d.ts +30 -0
  43. package/dist/protocol/constants.d.ts.map +1 -0
  44. package/dist/protocol/constants.js +37 -0
  45. package/dist/protocol/constants.js.map +1 -0
  46. package/dist/protocol/index.d.ts +8 -0
  47. package/dist/protocol/index.d.ts.map +1 -0
  48. package/dist/protocol/index.js +8 -0
  49. package/dist/protocol/index.js.map +1 -0
  50. package/dist/protocol/packets.d.ts +38 -0
  51. package/dist/protocol/packets.d.ts.map +1 -0
  52. package/dist/protocol/packets.js +177 -0
  53. package/dist/protocol/packets.js.map +1 -0
  54. package/dist/protocol/types.d.ts +134 -0
  55. package/dist/protocol/types.d.ts.map +1 -0
  56. package/dist/protocol/types.js +108 -0
  57. package/dist/protocol/types.js.map +1 -0
  58. package/dist/session/index.d.ts +5 -0
  59. package/dist/session/index.d.ts.map +1 -0
  60. package/dist/session/index.js +5 -0
  61. package/dist/session/index.js.map +1 -0
  62. package/dist/session/manager.d.ts +113 -0
  63. package/dist/session/manager.d.ts.map +1 -0
  64. package/dist/session/manager.js +371 -0
  65. package/dist/session/manager.js.map +1 -0
  66. package/dist/transport/ble.d.ts +92 -0
  67. package/dist/transport/ble.d.ts.map +1 -0
  68. package/dist/transport/ble.js +434 -0
  69. package/dist/transport/ble.js.map +1 -0
  70. package/dist/transport/index.d.ts +5 -0
  71. package/dist/transport/index.d.ts.map +1 -0
  72. package/dist/transport/index.js +5 -0
  73. package/dist/transport/index.js.map +1 -0
  74. package/dist/ui/index.d.ts +2 -0
  75. package/dist/ui/index.d.ts.map +1 -0
  76. package/dist/ui/index.js +2 -0
  77. package/dist/ui/index.js.map +1 -0
  78. package/dist/ui/server.d.ts +16 -0
  79. package/dist/ui/server.d.ts.map +1 -0
  80. package/dist/ui/server.js +510 -0
  81. package/dist/ui/server.js.map +1 -0
  82. package/package.json +79 -0
  83. package/src/bin/bitchat.ts +87 -0
  84. package/src/client.ts +519 -0
  85. package/src/crypto/index.ts +22 -0
  86. package/src/crypto/noise.ts +574 -0
  87. package/src/crypto/signing.ts +66 -0
  88. package/src/index.ts +95 -0
  89. package/src/mesh/deduplicator.ts +129 -0
  90. package/src/mesh/index.ts +6 -0
  91. package/src/mesh/router.ts +258 -0
  92. package/src/protocol/binary.ts +345 -0
  93. package/src/protocol/constants.ts +43 -0
  94. package/src/protocol/index.ts +15 -0
  95. package/src/protocol/packets.ts +223 -0
  96. package/src/protocol/types.ts +182 -0
  97. package/src/session/index.ts +9 -0
  98. package/src/session/manager.ts +476 -0
  99. package/src/transport/ble.ts +553 -0
  100. package/src/transport/index.ts +10 -0
  101. package/src/ui/index.ts +1 -0
  102. package/src/ui/server.ts +569 -0
@@ -0,0 +1,569 @@
1
+ /**
2
+ * Bitchat Web UI Server
3
+ * Simple HTTP + WebSocket server for testing
4
+ */
5
+
6
+ import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
7
+ import { WebSocket, WebSocketServer } from 'ws';
8
+ import type { BitchatClient } from '../client.js';
9
+ import type { ChatMessage, PeerInfo } from '../protocol/types.js';
10
+
11
+ const HTML = `<!DOCTYPE html>
12
+ <html>
13
+ <head>
14
+ <meta charset="UTF-8">
15
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
16
+ <title>Bitchat Node</title>
17
+ <style>
18
+ * { box-sizing: border-box; margin: 0; padding: 0; }
19
+ body {
20
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
21
+ background: #1a1a2e; color: #eee; height: 100vh; display: flex; flex-direction: column;
22
+ }
23
+ header {
24
+ background: #16213e; padding: 16px 20px; border-bottom: 1px solid #0f3460;
25
+ display: flex; justify-content: space-between; align-items: center;
26
+ }
27
+ header h1 { font-size: 18px; font-weight: 600; }
28
+ .status { font-size: 12px; color: #888; }
29
+ .status.connected { color: #4ade80; }
30
+ .peers { font-size: 12px; color: #60a5fa; margin-left: 16px; }
31
+ main { flex: 1; display: flex; overflow: hidden; }
32
+ .messages {
33
+ flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 8px;
34
+ }
35
+ .message {
36
+ background: #16213e; padding: 12px 16px; border-radius: 12px; max-width: 80%;
37
+ animation: fadeIn 0.2s ease;
38
+ }
39
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } }
40
+ .message.mine { background: #0f3460; align-self: flex-end; }
41
+ .message.private { border-left: 3px solid #f472b6; }
42
+ .message .meta { font-size: 11px; color: #888; margin-bottom: 4px; }
43
+ .message .meta .nickname { color: #60a5fa; font-weight: 500; }
44
+ .message .meta .private-badge { color: #f472b6; margin-left: 8px; }
45
+ .message .content { line-height: 1.4; word-wrap: break-word; }
46
+ .system {
47
+ text-align: center; font-size: 12px; color: #666; padding: 8px;
48
+ }
49
+ footer { background: #16213e; padding: 16px; border-top: 1px solid #0f3460; }
50
+ .input-row { display: flex; gap: 8px; }
51
+ input[type="text"] {
52
+ flex: 1; background: #1a1a2e; border: 1px solid #0f3460; border-radius: 8px;
53
+ padding: 12px 16px; color: #eee; font-size: 14px; outline: none;
54
+ }
55
+ input[type="text"]:focus { border-color: #3b82f6; }
56
+ input[type="text"]::placeholder { color: #555; }
57
+ button {
58
+ background: #3b82f6; color: white; border: none; border-radius: 8px;
59
+ padding: 12px 24px; font-size: 14px; font-weight: 500; cursor: pointer;
60
+ transition: background 0.2s;
61
+ }
62
+ button:hover { background: #2563eb; }
63
+ button:disabled { background: #374151; cursor: not-allowed; }
64
+ .peers-panel {
65
+ width: 200px; background: #16213e; border-left: 1px solid #0f3460;
66
+ padding: 16px; overflow-y: auto;
67
+ }
68
+ .peers-panel h3 { font-size: 12px; color: #888; margin-bottom: 12px; text-transform: uppercase; }
69
+ .peer {
70
+ padding: 8px 12px; background: #1a1a2e; border-radius: 6px; margin-bottom: 8px;
71
+ font-size: 13px; cursor: pointer; transition: background 0.2s;
72
+ }
73
+ .peer:hover { background: #0f3460; }
74
+ .peer .peer-id { font-size: 10px; color: #666; margin-top: 2px; font-family: monospace; }
75
+ .peer.selected { background: #3b82f6; }
76
+ </style>
77
+ </head>
78
+ <body>
79
+ <header>
80
+ <div>
81
+ <h1>🔗 Bitchat Node</h1>
82
+ <span class="status" id="status">Connecting...</span>
83
+ <span class="peers" id="peer-count"></span>
84
+ </div>
85
+ <div id="my-info" style="font-size: 12px; color: #888;"></div>
86
+ </header>
87
+ <main>
88
+ <div class="messages" id="messages"></div>
89
+ <div class="peers-panel">
90
+ <h3>Peers</h3>
91
+ <div id="peers-list"></div>
92
+ </div>
93
+ </main>
94
+ <footer>
95
+ <div class="input-row">
96
+ <input type="text" id="input" placeholder="Type a message..." autocomplete="off">
97
+ <button id="send">Send</button>
98
+ </div>
99
+ </footer>
100
+ <script>
101
+ const messagesEl = document.getElementById('messages');
102
+ const inputEl = document.getElementById('input');
103
+ const sendBtn = document.getElementById('send');
104
+ const statusEl = document.getElementById('status');
105
+ const peerCountEl = document.getElementById('peer-count');
106
+ const peersListEl = document.getElementById('peers-list');
107
+ const myInfoEl = document.getElementById('my-info');
108
+
109
+ let ws;
110
+ let myPeerID = '';
111
+ let selectedPeer = null;
112
+ const peers = new Map();
113
+
114
+ function connect() {
115
+ ws = new WebSocket('ws://' + location.host + '/ws');
116
+
117
+ ws.onopen = () => {
118
+ statusEl.textContent = 'Connected';
119
+ statusEl.className = 'status connected';
120
+ };
121
+
122
+ ws.onclose = () => {
123
+ statusEl.textContent = 'Disconnected';
124
+ statusEl.className = 'status';
125
+ setTimeout(connect, 2000);
126
+ };
127
+
128
+ ws.onmessage = (e) => {
129
+ const msg = JSON.parse(e.data);
130
+ handleMessage(msg);
131
+ };
132
+ }
133
+
134
+ function handleMessage(msg) {
135
+ switch (msg.type) {
136
+ case 'init':
137
+ myPeerID = msg.peerID;
138
+ myInfoEl.textContent = msg.nickname + ' · ' + msg.peerID.slice(0, 8) + '...';
139
+ break;
140
+
141
+ case 'message':
142
+ addMessage(msg.message);
143
+ break;
144
+
145
+ case 'peer:connected':
146
+ peers.set(msg.peer.peerID, msg.peer);
147
+ updatePeersList();
148
+ addSystem(msg.peer.nickname + ' joined the mesh');
149
+ break;
150
+
151
+ case 'peer:disconnected':
152
+ const peer = peers.get(msg.peerID);
153
+ peers.delete(msg.peerID);
154
+ updatePeersList();
155
+ if (peer) addSystem(peer.nickname + ' left the mesh');
156
+ break;
157
+
158
+ case 'peers':
159
+ peers.clear();
160
+ msg.peers.forEach(p => peers.set(p.peerID, p));
161
+ updatePeersList();
162
+ break;
163
+
164
+ case 'sent':
165
+ // Could update delivery status
166
+ break;
167
+
168
+ case 'error':
169
+ addSystem('Error: ' + msg.error);
170
+ break;
171
+ }
172
+ }
173
+
174
+ function addMessage(msg) {
175
+ const div = document.createElement('div');
176
+ div.className = 'message' + (msg.sender === myPeerID ? ' mine' : '') + (msg.isPrivate ? ' private' : '');
177
+ div.innerHTML =
178
+ '<div class="meta">' +
179
+ '<span class="nickname">' + escapeHtml(msg.senderNickname) + '</span> · ' +
180
+ new Date(msg.timestamp).toLocaleTimeString() +
181
+ (msg.isPrivate ? '<span class="private-badge">private</span>' : '') +
182
+ '</div>' +
183
+ '<div class="content">' + escapeHtml(msg.content) + '</div>';
184
+ messagesEl.appendChild(div);
185
+ messagesEl.scrollTop = messagesEl.scrollHeight;
186
+ }
187
+
188
+ function addSystem(text) {
189
+ const div = document.createElement('div');
190
+ div.className = 'system';
191
+ div.textContent = text;
192
+ messagesEl.appendChild(div);
193
+ messagesEl.scrollTop = messagesEl.scrollHeight;
194
+ }
195
+
196
+ function updatePeersList() {
197
+ peerCountEl.textContent = peers.size + ' peer' + (peers.size !== 1 ? 's' : '');
198
+ peersListEl.innerHTML = '';
199
+ peers.forEach((peer, id) => {
200
+ const div = document.createElement('div');
201
+ div.className = 'peer' + (selectedPeer === id ? ' selected' : '');
202
+ div.innerHTML =
203
+ '<div>' + escapeHtml(peer.nickname) + '</div>' +
204
+ '<div class="peer-id">' + id.slice(0, 12) + '...</div>';
205
+ div.onclick = () => {
206
+ selectedPeer = selectedPeer === id ? null : id;
207
+ updatePeersList();
208
+ inputEl.placeholder = selectedPeer ? 'Private message to ' + peer.nickname + '...' : 'Type a message...';
209
+ };
210
+ peersListEl.appendChild(div);
211
+ });
212
+ }
213
+
214
+ function send() {
215
+ const text = inputEl.value.trim();
216
+ if (!text) return;
217
+
218
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
219
+ addSystem('Not connected - please wait');
220
+ return;
221
+ }
222
+
223
+ // Show our own message immediately
224
+ addMessage({
225
+ id: Date.now().toString(),
226
+ sender: myPeerID,
227
+ senderNickname: 'me',
228
+ content: text,
229
+ timestamp: new Date().toISOString(),
230
+ isPrivate: !!selectedPeer
231
+ });
232
+
233
+ ws.send(JSON.stringify({
234
+ type: 'send',
235
+ text: text,
236
+ to: selectedPeer || null
237
+ }));
238
+
239
+ inputEl.value = '';
240
+ }
241
+
242
+ function escapeHtml(text) {
243
+ const div = document.createElement('div');
244
+ div.textContent = text;
245
+ return div.innerHTML;
246
+ }
247
+
248
+ sendBtn.onclick = send;
249
+ inputEl.onkeydown = (e) => { if (e.key === 'Enter') send(); };
250
+
251
+ connect();
252
+ </script>
253
+ </body>
254
+ </html>`;
255
+
256
+ export interface UIServerConfig {
257
+ port: number;
258
+ webhookUrl?: string;
259
+ }
260
+
261
+ // Store recent messages for polling
262
+ interface StoredMessage {
263
+ id: string;
264
+ type: 'public' | 'direct';
265
+ senderPeerID: string;
266
+ senderNickname: string;
267
+ text: string;
268
+ timestamp: number;
269
+ }
270
+
271
+ /**
272
+ * Start a web UI server for the Bitchat client
273
+ */
274
+ export function startUIServer(client: BitchatClient, config: UIServerConfig): { stop: () => void } {
275
+ const { port } = config;
276
+ const clients = new Set<WebSocket>();
277
+
278
+ // Message store for polling (keep last 100 messages)
279
+ const messageStore: StoredMessage[] = [];
280
+ const MAX_MESSAGES = 100;
281
+
282
+ // Registered webhooks
283
+ const webhooks: string[] = [];
284
+ if (config.webhookUrl) {
285
+ webhooks.push(config.webhookUrl);
286
+ }
287
+
288
+ // Helper to parse JSON body
289
+ const parseBody = (req: IncomingMessage): Promise<Record<string, unknown>> => {
290
+ return new Promise((resolve, reject) => {
291
+ let body = '';
292
+ req.on('data', (chunk) => (body += chunk));
293
+ req.on('end', () => {
294
+ try {
295
+ resolve(body ? JSON.parse(body) : {});
296
+ } catch {
297
+ reject(new Error('Invalid JSON'));
298
+ }
299
+ });
300
+ req.on('error', reject);
301
+ });
302
+ };
303
+
304
+ // Helper to send JSON response
305
+ const sendJson = (res: ServerResponse, status: number, data: unknown) => {
306
+ res.writeHead(status, { 'Content-Type': 'application/json' });
307
+ res.end(JSON.stringify(data));
308
+ };
309
+
310
+ // Notify webhooks of new message
311
+ const notifyWebhooks = async (message: StoredMessage) => {
312
+ for (const url of webhooks) {
313
+ try {
314
+ await fetch(url, {
315
+ method: 'POST',
316
+ headers: { 'Content-Type': 'application/json' },
317
+ body: JSON.stringify(message),
318
+ });
319
+ } catch (err) {
320
+ console.error(`[Webhook] Failed to notify ${url}:`, err);
321
+ }
322
+ }
323
+ };
324
+
325
+ // HTTP server with REST API
326
+ const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
327
+ const url = new URL(req.url ?? '/', `http://localhost:${port}`);
328
+ const path = url.pathname;
329
+ const method = req.method ?? 'GET';
330
+
331
+ // CORS headers
332
+ res.setHeader('Access-Control-Allow-Origin', '*');
333
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
334
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
335
+
336
+ if (method === 'OPTIONS') {
337
+ res.writeHead(204);
338
+ res.end();
339
+ return;
340
+ }
341
+
342
+ try {
343
+ // REST API endpoints
344
+ if (path === '/api/status' && method === 'GET') {
345
+ sendJson(res, 200, {
346
+ connected: true,
347
+ peerID: client.peerID.toHex(),
348
+ nickname: client.nickname,
349
+ fingerprint: client.fingerprint,
350
+ peersCount: client.getConnectedPeers().length,
351
+ });
352
+ return;
353
+ }
354
+
355
+ if (path === '/api/peers' && method === 'GET') {
356
+ const peers = client.getConnectedPeers().map((p) => ({
357
+ peerID: p.peerID.toHex(),
358
+ nickname: p.nickname,
359
+ isConnected: p.isConnected,
360
+ lastSeen: Date.now(), // TODO: track actual last seen
361
+ }));
362
+ sendJson(res, 200, peers);
363
+ return;
364
+ }
365
+
366
+ if (path === '/api/messages' && method === 'GET') {
367
+ const since = parseInt(url.searchParams.get('since') ?? '0', 10);
368
+ const messages = messageStore.filter((m) => m.timestamp > since);
369
+ sendJson(res, 200, messages);
370
+ return;
371
+ }
372
+
373
+ if (path === '/api/send' && method === 'POST') {
374
+ const body = await parseBody(req);
375
+ const { type, text, recipientPeerID } = body as {
376
+ type?: string;
377
+ text?: string;
378
+ recipientPeerID?: string;
379
+ };
380
+
381
+ if (!text || typeof text !== 'string') {
382
+ sendJson(res, 400, { error: 'Missing text field' });
383
+ return;
384
+ }
385
+
386
+ if (type === 'direct' && recipientPeerID) {
387
+ const { PeerID } = await import('../protocol/types.js');
388
+ await client.sendPrivateMessage(text, new PeerID(recipientPeerID));
389
+ } else {
390
+ await client.sendPublicMessage(text);
391
+ }
392
+
393
+ sendJson(res, 200, { ok: true });
394
+ return;
395
+ }
396
+
397
+ if (path === '/api/webhook' && method === 'POST') {
398
+ const body = await parseBody(req);
399
+ const { url: webhookUrl } = body as { url?: string };
400
+
401
+ if (!webhookUrl || typeof webhookUrl !== 'string') {
402
+ sendJson(res, 400, { error: 'Missing url field' });
403
+ return;
404
+ }
405
+
406
+ if (!webhooks.includes(webhookUrl)) {
407
+ webhooks.push(webhookUrl);
408
+ }
409
+
410
+ sendJson(res, 200, { ok: true, registered: webhooks.length });
411
+ return;
412
+ }
413
+
414
+ if (path === '/api/webhook' && method === 'GET') {
415
+ sendJson(res, 200, { webhooks });
416
+ return;
417
+ }
418
+
419
+ // Web UI
420
+ if (path === '/' || path === '/index.html') {
421
+ res.writeHead(200, { 'Content-Type': 'text/html' });
422
+ res.end(HTML);
423
+ return;
424
+ }
425
+
426
+ res.writeHead(404);
427
+ res.end('Not found');
428
+ } catch (err) {
429
+ console.error('[HTTP] Error:', err);
430
+ sendJson(res, 500, { error: (err as Error).message });
431
+ }
432
+ });
433
+
434
+ // WebSocket server
435
+ const wss = new WebSocketServer({ server, path: '/ws' });
436
+
437
+ wss.on('connection', (ws: WebSocket) => {
438
+ console.log('[WS] Client connected, total:', clients.size + 1);
439
+ clients.add(ws);
440
+
441
+ // Send init
442
+ ws.send(
443
+ JSON.stringify({
444
+ type: 'init',
445
+ peerID: client.peerID.toHex(),
446
+ nickname: client.nickname,
447
+ fingerprint: client.fingerprint,
448
+ })
449
+ );
450
+
451
+ // Send current peers
452
+ ws.send(
453
+ JSON.stringify({
454
+ type: 'peers',
455
+ peers: client.getConnectedPeers().map((p) => ({
456
+ peerID: p.peerID.toHex(),
457
+ nickname: p.nickname,
458
+ isConnected: p.isConnected,
459
+ })),
460
+ })
461
+ );
462
+
463
+ ws.on('message', async (data: Buffer) => {
464
+ console.log('[WS] Received:', data.toString().slice(0, 100));
465
+ try {
466
+ const msg = JSON.parse(data.toString());
467
+
468
+ if (msg.type === 'send') {
469
+ console.log('[WS] Sending message:', msg.text, 'to:', msg.to || 'broadcast');
470
+ if (msg.to) {
471
+ // Private message
472
+ const { PeerID } = await import('../protocol/types.js');
473
+ await client.sendPrivateMessage(msg.text, new PeerID(msg.to));
474
+ } else {
475
+ // Public message
476
+ await client.sendPublicMessage(msg.text);
477
+ }
478
+ console.log('[WS] Message sent successfully');
479
+ ws.send(JSON.stringify({ type: 'sent', text: msg.text }));
480
+ }
481
+ } catch (error) {
482
+ console.error('[WS] Send error:', (error as Error).message);
483
+ ws.send(JSON.stringify({ type: 'error', error: (error as Error).message }));
484
+ }
485
+ });
486
+
487
+ ws.on('close', () => {
488
+ console.log('[WS] Client disconnected, remaining:', clients.size - 1);
489
+ clients.delete(ws);
490
+ });
491
+ });
492
+
493
+ // Forward client events to WebSocket clients
494
+ const broadcast = (msg: object) => {
495
+ const data = JSON.stringify(msg);
496
+ clients.forEach((ws) => {
497
+ if (ws.readyState === WebSocket.OPEN) {
498
+ ws.send(data);
499
+ }
500
+ });
501
+ };
502
+
503
+ client.on('message', (message: ChatMessage) => {
504
+ // Store message for polling
505
+ const storedMessage: StoredMessage = {
506
+ id: message.id,
507
+ type: message.isPrivate ? 'direct' : 'public',
508
+ senderPeerID: message.sender.toHex(),
509
+ senderNickname: message.senderNickname,
510
+ text: message.content,
511
+ timestamp: message.timestamp.getTime(),
512
+ };
513
+
514
+ messageStore.push(storedMessage);
515
+ if (messageStore.length > MAX_MESSAGES) {
516
+ messageStore.shift(); // Remove oldest
517
+ }
518
+
519
+ // Notify webhooks
520
+ notifyWebhooks(storedMessage).catch((err) => {
521
+ console.error('[Webhook] Notification error:', err);
522
+ });
523
+
524
+ // Broadcast to WebSocket clients
525
+ broadcast({
526
+ type: 'message',
527
+ message: {
528
+ id: message.id,
529
+ sender: message.sender.toHex(),
530
+ senderPeerID: message.sender.toHex(),
531
+ senderNickname: message.senderNickname,
532
+ content: message.content,
533
+ text: message.content,
534
+ timestamp: message.timestamp.toISOString(),
535
+ isPrivate: message.isPrivate,
536
+ isDirect: message.isPrivate,
537
+ },
538
+ });
539
+ });
540
+
541
+ client.on('peer:connected', (peer: PeerInfo) => {
542
+ broadcast({
543
+ type: 'peer:connected',
544
+ peer: {
545
+ peerID: peer.peerID.toHex(),
546
+ nickname: peer.nickname,
547
+ isConnected: peer.isConnected,
548
+ },
549
+ });
550
+ });
551
+
552
+ client.on('peer:disconnected', (peerID) => {
553
+ broadcast({
554
+ type: 'peer:disconnected',
555
+ peerID: peerID.toHex(),
556
+ });
557
+ });
558
+
559
+ server.listen(port, () => {
560
+ console.log(`Bitchat UI: http://localhost:${port}`);
561
+ });
562
+
563
+ return {
564
+ stop: () => {
565
+ wss.close();
566
+ server.close();
567
+ },
568
+ };
569
+ }