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.
- package/README.md +223 -0
- package/dist/bin/bitchat.d.ts +7 -0
- package/dist/bin/bitchat.d.ts.map +1 -0
- package/dist/bin/bitchat.js +69 -0
- package/dist/bin/bitchat.js.map +1 -0
- package/dist/client.d.ts +77 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +411 -0
- package/dist/client.js.map +1 -0
- package/dist/crypto/index.d.ts +6 -0
- package/dist/crypto/index.d.ts.map +1 -0
- package/dist/crypto/index.js +6 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/crypto/noise.d.ts +72 -0
- package/dist/crypto/noise.d.ts.map +1 -0
- package/dist/crypto/noise.js +470 -0
- package/dist/crypto/noise.js.map +1 -0
- package/dist/crypto/signing.d.ts +34 -0
- package/dist/crypto/signing.d.ts.map +1 -0
- package/dist/crypto/signing.js +56 -0
- package/dist/crypto/signing.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/mesh/deduplicator.d.ts +48 -0
- package/dist/mesh/deduplicator.d.ts.map +1 -0
- package/dist/mesh/deduplicator.js +107 -0
- package/dist/mesh/deduplicator.js.map +1 -0
- package/dist/mesh/index.d.ts +6 -0
- package/dist/mesh/index.d.ts.map +1 -0
- package/dist/mesh/index.js +6 -0
- package/dist/mesh/index.js.map +1 -0
- package/dist/mesh/router.d.ts +90 -0
- package/dist/mesh/router.d.ts.map +1 -0
- package/dist/mesh/router.js +204 -0
- package/dist/mesh/router.js.map +1 -0
- package/dist/protocol/binary.d.ts +37 -0
- package/dist/protocol/binary.d.ts.map +1 -0
- package/dist/protocol/binary.js +310 -0
- package/dist/protocol/binary.js.map +1 -0
- package/dist/protocol/constants.d.ts +30 -0
- package/dist/protocol/constants.d.ts.map +1 -0
- package/dist/protocol/constants.js +37 -0
- package/dist/protocol/constants.js.map +1 -0
- package/dist/protocol/index.d.ts +8 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +8 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/protocol/packets.d.ts +38 -0
- package/dist/protocol/packets.d.ts.map +1 -0
- package/dist/protocol/packets.js +177 -0
- package/dist/protocol/packets.js.map +1 -0
- package/dist/protocol/types.d.ts +134 -0
- package/dist/protocol/types.d.ts.map +1 -0
- package/dist/protocol/types.js +108 -0
- package/dist/protocol/types.js.map +1 -0
- package/dist/session/index.d.ts +5 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +5 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/manager.d.ts +113 -0
- package/dist/session/manager.d.ts.map +1 -0
- package/dist/session/manager.js +371 -0
- package/dist/session/manager.js.map +1 -0
- package/dist/transport/ble.d.ts +92 -0
- package/dist/transport/ble.d.ts.map +1 -0
- package/dist/transport/ble.js +434 -0
- package/dist/transport/ble.js.map +1 -0
- package/dist/transport/index.d.ts +5 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +5 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/server.d.ts +16 -0
- package/dist/ui/server.d.ts.map +1 -0
- package/dist/ui/server.js +510 -0
- package/dist/ui/server.js.map +1 -0
- package/package.json +79 -0
- package/src/bin/bitchat.ts +87 -0
- package/src/client.ts +519 -0
- package/src/crypto/index.ts +22 -0
- package/src/crypto/noise.ts +574 -0
- package/src/crypto/signing.ts +66 -0
- package/src/index.ts +95 -0
- package/src/mesh/deduplicator.ts +129 -0
- package/src/mesh/index.ts +6 -0
- package/src/mesh/router.ts +258 -0
- package/src/protocol/binary.ts +345 -0
- package/src/protocol/constants.ts +43 -0
- package/src/protocol/index.ts +15 -0
- package/src/protocol/packets.ts +223 -0
- package/src/protocol/types.ts +182 -0
- package/src/session/index.ts +9 -0
- package/src/session/manager.ts +476 -0
- package/src/transport/ble.ts +553 -0
- package/src/transport/index.ts +10 -0
- package/src/ui/index.ts +1 -0
- package/src/ui/server.ts +569 -0
package/src/ui/server.ts
ADDED
|
@@ -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
|
+
}
|