aicq-chat-plugin 3.9.0 → 3.9.1

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.
@@ -1,337 +1,380 @@
1
- /**
2
- * AICQ Server Client — REST + WebSocket communication
3
- */
4
- const WebSocket = require('ws');
5
- const fetch = require('node-fetch');
6
- const { signMessage, computeFingerprint } = require('./crypto');
7
-
8
- class ServerClient {
9
- constructor(identityManager, db, serverUrl = 'https://aicq.online') {
10
- this.identity = identityManager;
11
- this.db = db;
12
- this.serverUrl = serverUrl;
13
- this.apiUrl = `${serverUrl}/api/v1`;
14
- this.wsUrl = serverUrl.replace('https://', 'wss://').replace('http://', 'ws://') + '/ws';
15
- this.jwtToken = null;
16
- this.ws = null;
17
- this.connected = false;
18
- this.currentAgentId = null;
19
- this._messageHandlers = {};
20
- this._reconnectTimer = null;
21
- this._backoff = 1000;
22
- this._running = false;
23
- }
24
-
25
- // ─── REST API ─────────────────────────────────────────────────────
26
-
27
- async _request(method, path, body = null, headers = {}) {
28
- const opts = {
29
- method,
30
- headers: { 'Content-Type': 'application/json', ...headers },
31
- };
32
- if (this.jwtToken) {
33
- opts.headers['Authorization'] = `Bearer ${this.jwtToken}`;
34
- }
35
- if (body) {
36
- opts.body = JSON.stringify(body);
37
- }
38
- const resp = await fetch(`${this.apiUrl}${path}`, opts);
39
- const data = await resp.json();
40
- if (!resp.ok) {
41
- throw new Error(data.error || data.message || `HTTP ${resp.status}`);
42
- }
43
- return data;
44
- }
45
-
46
- /**
47
- * Register an AI agent on the server
48
- */
49
- async registerAgent(agentId) {
50
- const identity = this.identity.loadAgent(agentId);
51
- if (!identity) throw new Error('Agent identity not found');
52
- const data = await this._request('POST', '/auth/register/ai', {
53
- public_key: identity.signing_public_key,
54
- agent_name: identity.nickname || agentId,
55
- });
56
- if (data.access_token || data.accessToken) {
57
- this.jwtToken = data.access_token || data.accessToken;
58
- this.currentAgentId = agentId;
59
- // Store server-side account ID for WS auth (nodeId must match JWT sub)
60
- if (data.account && data.account.id) {
61
- this.serverAccountId = data.account.id;
62
- }
63
- }
64
- return data;
65
- }
66
-
67
- /**
68
- * Get auth challenge and login
69
- */
70
- async loginAgent(agentId) {
71
- const identity = this.identity.loadAgent(agentId);
72
- if (!identity) throw new Error('Agent identity not found');
73
-
74
- // Get challenge
75
- const challengeData = await this._request('POST', '/auth/challenge', {
76
- public_key: identity.signing_public_key,
77
- });
78
- const challenge = challengeData.challenge;
79
-
80
- // Sign challenge
81
- const signature = signMessage(challenge, identity.signing_secret_key);
82
-
83
- // Login with signed challenge
84
- const loginData = await this._request('POST', '/auth/login/agent', {
85
- public_key: identity.signing_public_key,
86
- signature,
87
- challenge,
88
- });
89
-
90
- if (loginData.access_token || loginData.accessToken) {
91
- this.jwtToken = loginData.access_token || loginData.accessToken;
92
- this.currentAgentId = agentId;
93
- // Store server-side account ID for WS auth (nodeId must match JWT sub)
94
- if (loginData.account && loginData.account.id) {
95
- this.serverAccountId = loginData.account.id;
96
- }
97
- }
98
- return loginData;
99
- }
100
-
101
- /**
102
- * Ensure we're authenticated, try register then login
103
- */
104
- async ensureAuth(agentId) {
105
- this.currentAgentId = agentId;
106
- try {
107
- return await this.loginAgent(agentId);
108
- } catch (e) {
109
- // If login fails, try registering first
110
- try {
111
- await this.registerAgent(agentId);
112
- return await this.loginAgent(agentId);
113
- } catch (e2) {
114
- throw new Error(`Auth failed: ${e2.message}`);
115
- }
116
- }
117
- }
118
-
119
- // ─── Friend API ──────────────────────────────────────────────────
120
-
121
- async listFriends() {
122
- return this._request('GET', '/friends');
123
- }
124
-
125
- async sendFriendRequest(toId) {
126
- return this._request('POST', '/friends/request', { to_id: toId });
127
- }
128
-
129
- async listFriendRequests() {
130
- return this._request('GET', '/friends/requests');
131
- }
132
-
133
- async acceptFriendRequest(requestId) {
134
- return this._request('POST', `/friends/requests/${requestId}/accept`);
135
- }
136
-
137
- async rejectFriendRequest(requestId) {
138
- return this._request('POST', `/friends/requests/${requestId}/reject`);
139
- }
140
-
141
- async removeFriend(friendId) {
142
- return this._request('DELETE', `/friends/${friendId}`);
143
- }
144
-
145
- // ─── Group API ───────────────────────────────────────────────────
146
-
147
- async listGroups() {
148
- return this._request('GET', '/groups');
149
- }
150
-
151
- async createGroup(name, description = '') {
152
- return this._request('POST', '/groups', { name, description });
153
- }
154
-
155
- async getGroupMessages(groupId, limit = 50, before = null) {
156
- let path = `/groups/${groupId}/messages?limit=${limit}`;
157
- if (before) path += `&before=${before}`;
158
- return this._request('GET', path);
159
- }
160
-
161
- async inviteGroupMember(groupId, accountId) {
162
- return this._request('POST', `/groups/${groupId}/members`, { account_id: accountId });
163
- }
164
-
165
- // ─── Temp Number / Handshake API ─────────────────────────────────
166
-
167
- async generateTempNumber() {
168
- return this._request('POST', '/temp-number');
169
- }
170
-
171
- async resolveTempNumber(number) {
172
- return this._request('GET', `/temp-number/${number}`);
173
- }
174
-
175
- async initiateHandshake(tempNumber) {
176
- return this._request('POST', '/handshake/initiate', { temp_number: tempNumber });
177
- }
178
-
179
- async respondHandshake(sessionId, responseData) {
180
- return this._request('POST', '/handshake/respond', { session_id: sessionId, response_data: responseData });
181
- }
182
-
183
- async confirmHandshake(sessionId, confirmData) {
184
- return this._request('POST', '/handshake/confirm', { session_id: sessionId, confirm_data: confirmData });
185
- }
186
-
187
- async getPendingHandshakes() {
188
- return this._request('GET', '/handshake/pending');
189
- }
190
-
191
- // ─── WebSocket ───────────────────────────────────────────────────
192
-
193
- connectWS() {
194
- if (this.ws && this.ws.readyState === WebSocket.OPEN) return;
195
-
196
- const identity = this.identity.loadAgent(this.currentAgentId);
197
- if (!identity || !this.jwtToken) {
198
- console.error('[WS] No identity or token for WebSocket connection');
199
- return;
200
- }
201
-
202
- try {
203
- this.ws = new WebSocket(this.wsUrl);
204
-
205
- this.ws.on('open', () => {
206
- console.log('[WS] Connected, sending auth...');
207
- this.ws.send(JSON.stringify({
208
- type: 'online',
209
- nodeId: this.serverAccountId || this.currentAgentId,
210
- token: this.jwtToken,
211
- }));
212
- });
213
-
214
- this.ws.on('message', (raw) => {
215
- try {
216
- const data = JSON.parse(raw.toString());
217
- this._handleWSMessage(data);
218
- } catch (e) {
219
- console.error('[WS] Parse error:', e.message);
220
- }
221
- });
222
-
223
- this.ws.on('close', () => {
224
- console.log('[WS] Disconnected');
225
- this.connected = false;
226
- this._scheduleReconnect();
227
- });
228
-
229
- this.ws.on('error', (err) => {
230
- console.error('[WS] Error:', err.message);
231
- this.connected = false;
232
- });
233
- } catch (e) {
234
- console.error('[WS] Connect error:', e.message);
235
- this._scheduleReconnect();
236
- }
237
- }
238
-
239
- _handleWSMessage(data) {
240
- const type = data.type;
241
-
242
- if (type === 'online_ack') {
243
- this.connected = true;
244
- this._backoff = 1000;
245
- console.log('[WS] Authenticated as', data.nodeId);
246
- return;
247
- }
248
-
249
- if (type === 'error') {
250
- console.error('[WS] Server error:', data.message || data.code);
251
- return;
252
- }
253
-
254
- // Dispatch to registered handlers
255
- const handlers = this._messageHandlers[type] || [];
256
- for (const handler of handlers) {
257
- try { handler(data); } catch (e) { console.error(`[WS] Handler error for ${type}:`, e); }
258
- }
259
-
260
- // Wildcard handlers
261
- const wildcards = this._messageHandlers['*'] || [];
262
- for (const handler of wildcards) {
263
- try { handler(data); } catch (e) { console.error(`[WS] Wildcard handler error:`, e); }
264
- }
265
- }
266
-
267
- onMessage(type, handler) {
268
- if (!this._messageHandlers[type]) this._messageHandlers[type] = [];
269
- this._messageHandlers[type].push(handler);
270
- }
271
-
272
- sendWS(data) {
273
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return false;
274
- this.ws.send(JSON.stringify(data));
275
- return true;
276
- }
277
-
278
- _scheduleReconnect() {
279
- if (this._reconnectTimer) return;
280
- this._reconnectTimer = setTimeout(() => {
281
- this._reconnectTimer = null;
282
- console.log(`[WS] Reconnecting (backoff ${this._backoff}ms)...`);
283
- this._backoff = Math.min(this._backoff * 2, 60000);
284
- this.connectWS();
285
- }, this._backoff);
286
- }
287
-
288
- /**
289
- * Start the server client: authenticate and connect WebSocket
290
- */
291
- async start(agentId) {
292
- try {
293
- await this.ensureAuth(agentId);
294
- this.connectWS();
295
- this._running = true;
296
- console.log('[ServerClient] Started for agent:', agentId);
297
- } catch (e) {
298
- console.error('[ServerClient] Start failed:', e.message);
299
- this._scheduleReconnect();
300
- }
301
- }
302
-
303
- /**
304
- * Switch to a different agent
305
- */
306
- async switchAgent(agentId) {
307
- this.disconnect();
308
- await this.start(agentId);
309
- }
310
-
311
- disconnect() {
312
- if (this.ws) {
313
- try { this.ws.send(JSON.stringify({ type: 'offline' })); } catch (e) {}
314
- this.ws.close();
315
- this.ws = null;
316
- }
317
- this.connected = false;
318
- if (this._reconnectTimer) {
319
- clearTimeout(this._reconnectTimer);
320
- this._reconnectTimer = null;
321
- }
322
- }
323
-
324
- stop() {
325
- this._running = false;
326
- this.disconnect();
327
- }
328
-
329
- /**
330
- * Get the current JWT access token for a given agent
331
- */
332
- getAccessToken(agentId) {
333
- return this.jwtToken || '';
334
- }
335
- }
336
-
337
- module.exports = ServerClient;
1
+ /**
2
+ * AICQ Server Client — REST + WebSocket communication
3
+ */
4
+ const WebSocket = require('ws');
5
+ const fetch = require('node-fetch');
6
+ const { signMessage, computeFingerprint } = require('./crypto');
7
+
8
+ class ServerClient {
9
+ constructor(identityManager, db, serverUrl = 'https://aicq.online') {
10
+ this.identity = identityManager;
11
+ this.db = db;
12
+ this.serverUrl = serverUrl;
13
+ this.apiUrl = `${serverUrl}/api/v1`;
14
+ this.wsUrl = serverUrl.replace('https://', 'wss://').replace('http://', 'ws://') + '/ws';
15
+ this.jwtToken = null;
16
+ this.ws = null;
17
+ this.connected = false;
18
+ this.currentAgentId = null;
19
+ this._messageHandlers = {};
20
+ this._reconnectTimer = null;
21
+ this._backoff = 1000;
22
+ this._running = false;
23
+ }
24
+
25
+ // ─── REST API ─────────────────────────────────────────────────────
26
+
27
+ async _request(method, path, body = null, headers = {}) {
28
+ const opts = {
29
+ method,
30
+ headers: { 'Content-Type': 'application/json', ...headers },
31
+ };
32
+ if (this.jwtToken) {
33
+ opts.headers['Authorization'] = `Bearer ${this.jwtToken}`;
34
+ }
35
+ if (body) {
36
+ opts.body = JSON.stringify(body);
37
+ }
38
+ const resp = await fetch(`${this.apiUrl}${path}`, opts);
39
+ const data = await resp.json();
40
+ if (!resp.ok) {
41
+ throw new Error(data.error || data.message || `HTTP ${resp.status}`);
42
+ }
43
+ return data;
44
+ }
45
+
46
+ /**
47
+ * Register an AI agent on the server
48
+ */
49
+ async registerAgent(agentId) {
50
+ const identity = this.identity.loadAgent(agentId);
51
+ if (!identity) throw new Error('Agent identity not found');
52
+ const data = await this._request('POST', '/auth/register/ai', {
53
+ public_key: identity.signing_public_key,
54
+ agent_name: identity.nickname || agentId,
55
+ });
56
+ if (data.access_token || data.accessToken) {
57
+ this.jwtToken = data.access_token || data.accessToken;
58
+ this.currentAgentId = agentId;
59
+ // Store server-side account ID for WS auth (nodeId must match JWT sub)
60
+ if (data.account && data.account.id) {
61
+ this.serverAccountId = data.account.id;
62
+ }
63
+ }
64
+ return data;
65
+ }
66
+
67
+ /**
68
+ * Get auth challenge and login
69
+ */
70
+ async loginAgent(agentId) {
71
+ const identity = this.identity.loadAgent(agentId);
72
+ if (!identity) throw new Error('Agent identity not found');
73
+
74
+ // Get challenge
75
+ const challengeData = await this._request('POST', '/auth/challenge', {
76
+ public_key: identity.signing_public_key,
77
+ });
78
+ const challenge = challengeData.challenge;
79
+
80
+ // Sign challenge
81
+ const signature = signMessage(challenge, identity.signing_secret_key);
82
+
83
+ // Login with signed challenge
84
+ const loginData = await this._request('POST', '/auth/login/agent', {
85
+ public_key: identity.signing_public_key,
86
+ signature,
87
+ challenge,
88
+ });
89
+
90
+ if (loginData.access_token || loginData.accessToken) {
91
+ this.jwtToken = loginData.access_token || loginData.accessToken;
92
+ this.currentAgentId = agentId;
93
+ // Store server-side account ID for WS auth (nodeId must match JWT sub)
94
+ if (loginData.account && loginData.account.id) {
95
+ this.serverAccountId = loginData.account.id;
96
+ }
97
+ }
98
+ return loginData;
99
+ }
100
+
101
+ /**
102
+ * Ensure we're authenticated, try register then login
103
+ */
104
+ async ensureAuth(agentId) {
105
+ this.currentAgentId = agentId;
106
+ try {
107
+ return await this.loginAgent(agentId);
108
+ } catch (e) {
109
+ // If login fails, try registering first
110
+ try {
111
+ await this.registerAgent(agentId);
112
+ return await this.loginAgent(agentId);
113
+ } catch (e2) {
114
+ throw new Error(`Auth failed: ${e2.message}`);
115
+ }
116
+ }
117
+ }
118
+
119
+ // ─── Friend API ──────────────────────────────────────────────────
120
+
121
+ async listFriends() {
122
+ return this._request('GET', '/friends');
123
+ }
124
+
125
+ async sendFriendRequest(toId, message = '') {
126
+ const body = { to_id: toId };
127
+ if (message) body.message = message;
128
+ return this._request('POST', '/friends/request', body);
129
+ }
130
+
131
+ async listFriendRequests() {
132
+ return this._request('GET', '/friends/requests');
133
+ }
134
+
135
+ async acceptFriendRequest(requestId) {
136
+ return this._request('POST', `/friends/requests/${requestId}/accept`);
137
+ }
138
+
139
+ async rejectFriendRequest(requestId) {
140
+ return this._request('POST', `/friends/requests/${requestId}/reject`);
141
+ }
142
+
143
+ async removeFriend(friendId) {
144
+ return this._request('DELETE', `/friends/${friendId}`);
145
+ }
146
+
147
+ // ─── Group API ───────────────────────────────────────────────────
148
+
149
+ async listGroups() {
150
+ return this._request('GET', '/groups');
151
+ }
152
+
153
+ async createGroup(name, description = '') {
154
+ return this._request('POST', '/groups', { name, description });
155
+ }
156
+
157
+ async getGroupMessages(groupId, limit = 50, before = null) {
158
+ let path = `/groups/${groupId}/messages?limit=${limit}`;
159
+ if (before) path += `&before=${before}`;
160
+ return this._request('GET', path);
161
+ }
162
+
163
+ async inviteGroupMember(groupId, accountId) {
164
+ return this._request('POST', `/groups/${groupId}/members`, { account_id: accountId });
165
+ }
166
+
167
+ // ─── Chat / Message API ──────────────────────────────────────────
168
+
169
+ /**
170
+ * Fetch conversation history with a friend from the server.
171
+ * GET /api/v1/chat/conversation/:friendId?limit=50
172
+ */
173
+ async getConversation(friendId, limit = 50, before = null) {
174
+ let path = `/chat/conversation/${friendId}?limit=${limit}`;
175
+ if (before) path += `&before=${encodeURIComponent(before)}`;
176
+ return this._request('GET', path);
177
+ }
178
+
179
+ /**
180
+ * Send a message to a friend via REST API.
181
+ * POST /api/v1/chat/messages
182
+ */
183
+ async sendChatMessage(toId, content, msgType = 'text', extra = {}) {
184
+ const body = {
185
+ to: toId,
186
+ data: {
187
+ type: msgType,
188
+ content,
189
+ ...extra,
190
+ },
191
+ };
192
+ return this._request('POST', '/chat/messages', body);
193
+ }
194
+
195
+ /**
196
+ * Mark messages from a friend as read.
197
+ * POST /api/v1/chat/mark-read
198
+ */
199
+ async markRead(friendId) {
200
+ return this._request('POST', '/chat/mark-read', { friend_id: friendId });
201
+ }
202
+
203
+ // ─── Temp Number / Handshake API ─────────────────────────────────
204
+
205
+ async generateTempNumber() {
206
+ return this._request('POST', '/temp-number');
207
+ }
208
+
209
+ async resolveTempNumber(number) {
210
+ return this._request('GET', `/temp-number/${number}`);
211
+ }
212
+
213
+ async initiateHandshake(tempNumber) {
214
+ return this._request('POST', '/handshake/initiate', { temp_number: tempNumber });
215
+ }
216
+
217
+ async respondHandshake(sessionId, responseData) {
218
+ return this._request('POST', '/handshake/respond', { session_id: sessionId, response_data: responseData });
219
+ }
220
+
221
+ async confirmHandshake(sessionId, confirmData) {
222
+ return this._request('POST', '/handshake/confirm', { session_id: sessionId, confirm_data: confirmData });
223
+ }
224
+
225
+ async getPendingHandshakes() {
226
+ return this._request('GET', '/handshake/pending');
227
+ }
228
+
229
+ // ─── WebSocket ───────────────────────────────────────────────────
230
+
231
+ connectWS() {
232
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) return;
233
+
234
+ const identity = this.identity.loadAgent(this.currentAgentId);
235
+ if (!identity || !this.jwtToken) {
236
+ console.error('[WS] No identity or token for WebSocket connection');
237
+ return;
238
+ }
239
+
240
+ try {
241
+ this.ws = new WebSocket(this.wsUrl);
242
+
243
+ this.ws.on('open', () => {
244
+ console.log('[WS] Connected, sending auth...');
245
+ this.ws.send(JSON.stringify({
246
+ type: 'online',
247
+ nodeId: this.serverAccountId || this.currentAgentId,
248
+ token: this.jwtToken,
249
+ }));
250
+ });
251
+
252
+ this.ws.on('message', (raw) => {
253
+ try {
254
+ const data = JSON.parse(raw.toString());
255
+ this._handleWSMessage(data);
256
+ } catch (e) {
257
+ console.error('[WS] Parse error:', e.message);
258
+ }
259
+ });
260
+
261
+ this.ws.on('close', () => {
262
+ console.log('[WS] Disconnected');
263
+ this.connected = false;
264
+ this._scheduleReconnect();
265
+ });
266
+
267
+ this.ws.on('error', (err) => {
268
+ console.error('[WS] Error:', err.message);
269
+ this.connected = false;
270
+ });
271
+ } catch (e) {
272
+ console.error('[WS] Connect error:', e.message);
273
+ this._scheduleReconnect();
274
+ }
275
+ }
276
+
277
+ _handleWSMessage(data) {
278
+ const type = data.type;
279
+
280
+ if (type === 'online_ack') {
281
+ this.connected = true;
282
+ this._backoff = 1000;
283
+ console.log('[WS] Authenticated as', data.nodeId);
284
+ // Notify reconnect handlers so ChatManager can fetch missed messages
285
+ const reconnectHandlers = this._messageHandlers['_reconnected'] || [];
286
+ for (const handler of reconnectHandlers) {
287
+ try { handler(data); } catch (e) { console.error('[WS] Reconnect handler error:', e); }
288
+ }
289
+ // Don't return here let handlers (e.g. unread_counts) process too
290
+ }
291
+
292
+ if (type === 'error') {
293
+ console.error('[WS] Server error:', data.message || data.code);
294
+ // Don't return — let handlers see the error too
295
+ }
296
+
297
+ // Dispatch to registered handlers
298
+ const handlers = this._messageHandlers[type] || [];
299
+ for (const handler of handlers) {
300
+ try { handler(data); } catch (e) { console.error(`[WS] Handler error for ${type}:`, e); }
301
+ }
302
+
303
+ // Wildcard handlers
304
+ const wildcards = this._messageHandlers['*'] || [];
305
+ for (const handler of wildcards) {
306
+ try { handler(data); } catch (e) { console.error(`[WS] Wildcard handler error:`, e); }
307
+ }
308
+ }
309
+
310
+ onMessage(type, handler) {
311
+ if (!this._messageHandlers[type]) this._messageHandlers[type] = [];
312
+ this._messageHandlers[type].push(handler);
313
+ }
314
+
315
+ sendWS(data) {
316
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return false;
317
+ this.ws.send(JSON.stringify(data));
318
+ return true;
319
+ }
320
+
321
+ _scheduleReconnect() {
322
+ if (this._reconnectTimer) return;
323
+ this._reconnectTimer = setTimeout(() => {
324
+ this._reconnectTimer = null;
325
+ console.log(`[WS] Reconnecting (backoff ${this._backoff}ms)...`);
326
+ this._backoff = Math.min(this._backoff * 2, 60000);
327
+ this.connectWS();
328
+ }, this._backoff);
329
+ }
330
+
331
+ /**
332
+ * Start the server client: authenticate and connect WebSocket
333
+ */
334
+ async start(agentId) {
335
+ try {
336
+ await this.ensureAuth(agentId);
337
+ this.connectWS();
338
+ this._running = true;
339
+ console.log('[ServerClient] Started for agent:', agentId);
340
+ } catch (e) {
341
+ console.error('[ServerClient] Start failed:', e.message);
342
+ this._scheduleReconnect();
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Switch to a different agent
348
+ */
349
+ async switchAgent(agentId) {
350
+ this.disconnect();
351
+ await this.start(agentId);
352
+ }
353
+
354
+ disconnect() {
355
+ if (this.ws) {
356
+ try { this.ws.send(JSON.stringify({ type: 'offline' })); } catch (e) {}
357
+ this.ws.close();
358
+ this.ws = null;
359
+ }
360
+ this.connected = false;
361
+ if (this._reconnectTimer) {
362
+ clearTimeout(this._reconnectTimer);
363
+ this._reconnectTimer = null;
364
+ }
365
+ }
366
+
367
+ stop() {
368
+ this._running = false;
369
+ this.disconnect();
370
+ }
371
+
372
+ /**
373
+ * Get the current JWT access token for a given agent
374
+ */
375
+ getAccessToken(agentId) {
376
+ return this.jwtToken || '';
377
+ }
378
+ }
379
+
380
+ module.exports = ServerClient;