@plosson/agentio 0.4.2 → 0.4.3
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 +4 -4
- package/package.json +3 -1
- package/src/auth/oauth.ts +14 -2
- package/src/commands/gateway.ts +259 -0
- package/src/commands/gcal.ts +383 -0
- package/src/commands/gtasks.ts +326 -0
- package/src/commands/status.ts +85 -0
- package/src/commands/telegram.ts +209 -1
- package/src/commands/update.ts +2 -2
- package/src/commands/whatsapp.ts +853 -0
- package/src/config/config-manager.ts +1 -1
- package/src/gateway/adapters/telegram.ts +357 -0
- package/src/gateway/adapters/types.ts +147 -0
- package/src/gateway/adapters/whatsapp-auth.ts +172 -0
- package/src/gateway/adapters/whatsapp.ts +723 -0
- package/src/gateway/api.ts +791 -0
- package/src/gateway/client.ts +402 -0
- package/src/gateway/daemon.ts +461 -0
- package/src/gateway/store.ts +637 -0
- package/src/gateway/types.ts +325 -0
- package/src/gateway/webhook.ts +109 -0
- package/src/index.ts +32 -16
- package/src/polyfills.ts +10 -0
- package/src/services/gcal/client.ts +380 -0
- package/src/services/gtasks/client.ts +301 -0
- package/src/types/config.ts +36 -1
- package/src/types/gcal.ts +135 -0
- package/src/types/gtasks.ts +58 -0
- package/src/types/qrcode-terminal.d.ts +8 -0
- package/src/types/whatsapp.ts +116 -0
- package/src/utils/output.ts +505 -0
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
import type { Server } from 'bun';
|
|
2
|
+
import type { ServiceName } from '../types/config';
|
|
3
|
+
import type {
|
|
4
|
+
GatewayConfig,
|
|
5
|
+
InboxPullRequest,
|
|
6
|
+
InboxPullResponse,
|
|
7
|
+
InboxGetRequest,
|
|
8
|
+
InboxGetResponse,
|
|
9
|
+
InboxAckRequest,
|
|
10
|
+
InboxAckResponse,
|
|
11
|
+
InboxReplyRequest,
|
|
12
|
+
InboxReplyResponse,
|
|
13
|
+
InboxStatsRequest,
|
|
14
|
+
InboxStatsResponse,
|
|
15
|
+
OutboxSendRequest,
|
|
16
|
+
OutboxSendResponse,
|
|
17
|
+
OutboxStatusRequest,
|
|
18
|
+
OutboxStatusResponse,
|
|
19
|
+
OutboxListRequest,
|
|
20
|
+
OutboxListResponse,
|
|
21
|
+
GatewayStatusResponse,
|
|
22
|
+
HealthResponse,
|
|
23
|
+
WhatsAppPairResponse,
|
|
24
|
+
WhatsAppGroupListRequest,
|
|
25
|
+
WhatsAppGroupListResponse,
|
|
26
|
+
WhatsAppGroupGetRequest,
|
|
27
|
+
WhatsAppGroupGetResponse,
|
|
28
|
+
WhatsAppGroupCreateRequest,
|
|
29
|
+
WhatsAppGroupCreateResponse,
|
|
30
|
+
WhatsAppGroupUpdateRequest,
|
|
31
|
+
WhatsAppGroupUpdateResponse,
|
|
32
|
+
WhatsAppGroupParticipantsRequest,
|
|
33
|
+
WhatsAppGroupParticipantsResponse,
|
|
34
|
+
WhatsAppGroupLeaveRequest,
|
|
35
|
+
WhatsAppGroupLeaveResponse,
|
|
36
|
+
WhatsAppGroupInviteRequest,
|
|
37
|
+
WhatsAppGroupInviteResponse,
|
|
38
|
+
WhatsAppGroupJoinRequest,
|
|
39
|
+
WhatsAppGroupJoinResponse,
|
|
40
|
+
WhatsAppGroupResolveRequest,
|
|
41
|
+
WhatsAppGroupResolveResponse,
|
|
42
|
+
} from './types';
|
|
43
|
+
import { DEFAULT_GATEWAY_CONFIG } from './types';
|
|
44
|
+
import {
|
|
45
|
+
getInboxMessages,
|
|
46
|
+
getInboxMessage,
|
|
47
|
+
ackInboxMessage,
|
|
48
|
+
getInboxStats,
|
|
49
|
+
queueOutboxMessage,
|
|
50
|
+
getOutboxMessage,
|
|
51
|
+
getOutboxMessages,
|
|
52
|
+
importWhatsAppAuthState,
|
|
53
|
+
type WhatsAppAuthExport,
|
|
54
|
+
} from './store';
|
|
55
|
+
import type { ServiceAdapter } from './adapters/types';
|
|
56
|
+
import type { WhatsAppAdapter } from './adapters/whatsapp';
|
|
57
|
+
|
|
58
|
+
let server: Server<unknown> | null = null;
|
|
59
|
+
let apiSecret: string = '';
|
|
60
|
+
let startTime: number = 0;
|
|
61
|
+
let adapters: Map<ServiceName, ServiceAdapter> = new Map();
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* JSON error response helper
|
|
65
|
+
*/
|
|
66
|
+
function jsonError(message: string, status: number = 400): Response {
|
|
67
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
68
|
+
status,
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* JSON success response helper
|
|
75
|
+
*/
|
|
76
|
+
function jsonResponse<T>(data: T, status: number = 200): Response {
|
|
77
|
+
return new Response(JSON.stringify(data), {
|
|
78
|
+
status,
|
|
79
|
+
headers: { 'Content-Type': 'application/json' },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Verify authorization header
|
|
85
|
+
*/
|
|
86
|
+
function verifyAuth(request: Request): boolean {
|
|
87
|
+
if (!apiSecret) return true; // No auth configured
|
|
88
|
+
|
|
89
|
+
const authHeader = request.headers.get('Authorization');
|
|
90
|
+
if (!authHeader) return false;
|
|
91
|
+
|
|
92
|
+
const [type, token] = authHeader.split(' ');
|
|
93
|
+
return type === 'Bearer' && token === apiSecret;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parse JSON body safely
|
|
98
|
+
*/
|
|
99
|
+
async function parseJsonBody<T>(request: Request): Promise<T | null> {
|
|
100
|
+
try {
|
|
101
|
+
return await request.json() as T;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Handle inbox pull request
|
|
109
|
+
*/
|
|
110
|
+
async function handleInboxPull(request: Request): Promise<Response> {
|
|
111
|
+
const body = await parseJsonBody<InboxPullRequest>(request);
|
|
112
|
+
|
|
113
|
+
const messages = getInboxMessages({
|
|
114
|
+
service: body?.service,
|
|
115
|
+
profile: body?.profile,
|
|
116
|
+
conversationId: body?.conversationId,
|
|
117
|
+
status: body?.status ?? 'pending',
|
|
118
|
+
limit: body?.limit ?? 50,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const response: InboxPullResponse = { messages };
|
|
122
|
+
return jsonResponse(response);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Handle inbox get request
|
|
127
|
+
*/
|
|
128
|
+
async function handleInboxGet(request: Request): Promise<Response> {
|
|
129
|
+
const body = await parseJsonBody<InboxGetRequest>(request);
|
|
130
|
+
|
|
131
|
+
if (!body?.id) {
|
|
132
|
+
return jsonError('Message ID is required');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const message = getInboxMessage(body.id);
|
|
136
|
+
const response: InboxGetResponse = { message };
|
|
137
|
+
return jsonResponse(response);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Handle inbox ack request
|
|
142
|
+
*/
|
|
143
|
+
async function handleInboxAck(request: Request): Promise<Response> {
|
|
144
|
+
const body = await parseJsonBody<InboxAckRequest>(request);
|
|
145
|
+
|
|
146
|
+
if (!body?.id) {
|
|
147
|
+
return jsonError('Message ID is required');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const success = ackInboxMessage(body.id);
|
|
151
|
+
const response: InboxAckResponse = { success };
|
|
152
|
+
return jsonResponse(response);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Handle inbox reply request
|
|
157
|
+
*/
|
|
158
|
+
async function handleInboxReply(request: Request): Promise<Response> {
|
|
159
|
+
const body = await parseJsonBody<InboxReplyRequest>(request);
|
|
160
|
+
|
|
161
|
+
if (!body?.id) {
|
|
162
|
+
return jsonError('Message ID is required');
|
|
163
|
+
}
|
|
164
|
+
if (!body?.content) {
|
|
165
|
+
return jsonError('Content is required');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Get the original inbox message
|
|
169
|
+
const inboxMessage = getInboxMessage(body.id);
|
|
170
|
+
if (!inboxMessage) {
|
|
171
|
+
return jsonError('Message not found', 404);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Queue reply to outbox
|
|
175
|
+
const outboxMessage = queueOutboxMessage({
|
|
176
|
+
service: inboxMessage.service,
|
|
177
|
+
profile: inboxMessage.profile,
|
|
178
|
+
conversationId: inboxMessage.conversationId,
|
|
179
|
+
content: body.content,
|
|
180
|
+
mediaPath: body.mediaPath,
|
|
181
|
+
mediaType: body.mediaType,
|
|
182
|
+
replyToPlatformId: inboxMessage.platformId,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Auto-ack the inbox message
|
|
186
|
+
ackInboxMessage(body.id);
|
|
187
|
+
|
|
188
|
+
const response: InboxReplyResponse = {
|
|
189
|
+
outboxId: outboxMessage.id,
|
|
190
|
+
status: outboxMessage.status,
|
|
191
|
+
};
|
|
192
|
+
return jsonResponse(response);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Handle inbox stats request
|
|
197
|
+
*/
|
|
198
|
+
async function handleInboxStats(request: Request): Promise<Response> {
|
|
199
|
+
const body = await parseJsonBody<InboxStatsRequest>(request);
|
|
200
|
+
|
|
201
|
+
const stats = getInboxStats({
|
|
202
|
+
service: body?.service,
|
|
203
|
+
profile: body?.profile,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const response: InboxStatsResponse = stats;
|
|
207
|
+
return jsonResponse(response);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Handle outbox send request
|
|
212
|
+
*/
|
|
213
|
+
async function handleOutboxSend(request: Request): Promise<Response> {
|
|
214
|
+
const body = await parseJsonBody<OutboxSendRequest>(request);
|
|
215
|
+
|
|
216
|
+
if (!body?.service) {
|
|
217
|
+
return jsonError('Service is required');
|
|
218
|
+
}
|
|
219
|
+
if (!body?.profile) {
|
|
220
|
+
return jsonError('Profile is required');
|
|
221
|
+
}
|
|
222
|
+
if (!body?.conversationId) {
|
|
223
|
+
return jsonError('Conversation ID is required');
|
|
224
|
+
}
|
|
225
|
+
if (!body?.content && !body?.mediaPath) {
|
|
226
|
+
return jsonError('Content or media is required');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const outboxMessage = queueOutboxMessage({
|
|
230
|
+
service: body.service,
|
|
231
|
+
profile: body.profile,
|
|
232
|
+
conversationId: body.conversationId,
|
|
233
|
+
content: body.content,
|
|
234
|
+
mediaPath: body.mediaPath,
|
|
235
|
+
mediaType: body.mediaType,
|
|
236
|
+
replyToPlatformId: body.replyToPlatformId,
|
|
237
|
+
metadata: body.metadata,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const response: OutboxSendResponse = {
|
|
241
|
+
id: outboxMessage.id,
|
|
242
|
+
status: outboxMessage.status,
|
|
243
|
+
};
|
|
244
|
+
return jsonResponse(response);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Handle outbox status request
|
|
249
|
+
*/
|
|
250
|
+
async function handleOutboxStatus(request: Request): Promise<Response> {
|
|
251
|
+
const body = await parseJsonBody<OutboxStatusRequest>(request);
|
|
252
|
+
|
|
253
|
+
if (!body?.id) {
|
|
254
|
+
return jsonError('Message ID is required');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const message = getOutboxMessage(body.id);
|
|
258
|
+
const response: OutboxStatusResponse = { message };
|
|
259
|
+
return jsonResponse(response);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Handle outbox list request
|
|
264
|
+
*/
|
|
265
|
+
async function handleOutboxList(request: Request): Promise<Response> {
|
|
266
|
+
const body = await parseJsonBody<OutboxListRequest>(request);
|
|
267
|
+
|
|
268
|
+
const messages = getOutboxMessages({
|
|
269
|
+
service: body?.service,
|
|
270
|
+
profile: body?.profile,
|
|
271
|
+
status: body?.status,
|
|
272
|
+
limit: body?.limit ?? 50,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const response: OutboxListResponse = { messages };
|
|
276
|
+
return jsonResponse(response);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Handle health check
|
|
281
|
+
*/
|
|
282
|
+
function handleHealth(): Response {
|
|
283
|
+
const response: HealthResponse = {
|
|
284
|
+
status: 'ok',
|
|
285
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
286
|
+
};
|
|
287
|
+
return jsonResponse(response);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Handle gateway status
|
|
292
|
+
*/
|
|
293
|
+
function handleStatus(): Response {
|
|
294
|
+
const adapterStatus: GatewayStatusResponse['adapters'] = [];
|
|
295
|
+
|
|
296
|
+
for (const [service, adapter] of adapters) {
|
|
297
|
+
for (const profile of adapter.getConnectedProfiles()) {
|
|
298
|
+
const state = adapter.getConnectionState(profile);
|
|
299
|
+
adapterStatus.push({
|
|
300
|
+
service,
|
|
301
|
+
profile,
|
|
302
|
+
connected: state.connected,
|
|
303
|
+
error: state.error,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const response: GatewayStatusResponse = {
|
|
309
|
+
running: true,
|
|
310
|
+
uptime: Math.floor((Date.now() - startTime) / 1000),
|
|
311
|
+
adapters: adapterStatus,
|
|
312
|
+
};
|
|
313
|
+
return jsonResponse(response);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Handle media request
|
|
318
|
+
*/
|
|
319
|
+
function handleMedia(id: string): Response {
|
|
320
|
+
const message = getInboxMessage(id);
|
|
321
|
+
if (!message) {
|
|
322
|
+
return jsonError('Message not found', 404);
|
|
323
|
+
}
|
|
324
|
+
if (!message.mediaPath) {
|
|
325
|
+
return jsonError('No media attached', 404);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const file = Bun.file(message.mediaPath);
|
|
330
|
+
return new Response(file);
|
|
331
|
+
} catch {
|
|
332
|
+
return jsonError('Media file not found', 404);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Handle WhatsApp pairing request
|
|
338
|
+
*/
|
|
339
|
+
function handleWhatsAppPair(profile: string): Response {
|
|
340
|
+
const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
|
|
341
|
+
|
|
342
|
+
if (!whatsappAdapter) {
|
|
343
|
+
const response: WhatsAppPairResponse = {
|
|
344
|
+
status: 'not_configured',
|
|
345
|
+
message: 'WhatsApp is not configured. Add a profile first.',
|
|
346
|
+
};
|
|
347
|
+
return jsonResponse(response);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const state = whatsappAdapter.getWhatsAppState(profile);
|
|
351
|
+
|
|
352
|
+
if (state.connected) {
|
|
353
|
+
const response: WhatsAppPairResponse = {
|
|
354
|
+
status: 'connected',
|
|
355
|
+
phoneNumber: state.phoneNumber,
|
|
356
|
+
displayName: undefined, // Will be filled if available
|
|
357
|
+
message: `Connected as ${state.phoneNumber || 'WhatsApp user'}`,
|
|
358
|
+
};
|
|
359
|
+
return jsonResponse(response);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (state.qrCode) {
|
|
363
|
+
const response: WhatsAppPairResponse = {
|
|
364
|
+
status: 'waiting_qr',
|
|
365
|
+
qrCode: state.qrCode,
|
|
366
|
+
message: 'Scan QR code with WhatsApp on your phone',
|
|
367
|
+
};
|
|
368
|
+
return jsonResponse(response);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const response: WhatsAppPairResponse = {
|
|
372
|
+
status: 'connecting',
|
|
373
|
+
message: state.error || 'Connecting to WhatsApp...',
|
|
374
|
+
};
|
|
375
|
+
return jsonResponse(response);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Handle WhatsApp auth import (for teleport)
|
|
380
|
+
*/
|
|
381
|
+
async function handleWhatsAppImport(profile: string, request: Request): Promise<Response> {
|
|
382
|
+
try {
|
|
383
|
+
const authExport = await parseJsonBody<WhatsAppAuthExport>(request);
|
|
384
|
+
|
|
385
|
+
if (!authExport || !authExport.profile) {
|
|
386
|
+
return jsonError('Invalid auth export data');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Import the auth state
|
|
390
|
+
await importWhatsAppAuthState({
|
|
391
|
+
...authExport,
|
|
392
|
+
profile, // Use URL profile, not body profile (security)
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Disconnect and reconnect WhatsApp adapter to use new credentials
|
|
396
|
+
const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
|
|
397
|
+
if (whatsappAdapter) {
|
|
398
|
+
await whatsappAdapter.disconnect(profile);
|
|
399
|
+
// Note: reconnection will happen on next daemon cycle or manual reload
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return jsonResponse({ success: true, message: 'Auth state imported. Reload gateway to reconnect.' });
|
|
403
|
+
} catch (error) {
|
|
404
|
+
const message = error instanceof Error ? error.message : 'Import failed';
|
|
405
|
+
return jsonError(message, 500);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ============ WHATSAPP GROUP HANDLERS ============
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Handle WhatsApp group list request
|
|
413
|
+
*/
|
|
414
|
+
async function handleWhatsAppGroupList(request: Request): Promise<Response> {
|
|
415
|
+
const body = await parseJsonBody<WhatsAppGroupListRequest>(request);
|
|
416
|
+
|
|
417
|
+
if (!body?.profile) {
|
|
418
|
+
return jsonError('Profile is required');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
|
|
422
|
+
if (!whatsappAdapter) {
|
|
423
|
+
return jsonError('WhatsApp not configured', 404);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const groups = await whatsappAdapter.listGroups(body.profile);
|
|
428
|
+
const response: WhatsAppGroupListResponse = { groups };
|
|
429
|
+
return jsonResponse(response);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
const message = error instanceof Error ? error.message : 'Failed to list groups';
|
|
432
|
+
return jsonError(message, 500);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Handle WhatsApp group get request
|
|
438
|
+
*/
|
|
439
|
+
async function handleWhatsAppGroupGet(request: Request): Promise<Response> {
|
|
440
|
+
const body = await parseJsonBody<WhatsAppGroupGetRequest>(request);
|
|
441
|
+
|
|
442
|
+
if (!body?.profile) {
|
|
443
|
+
return jsonError('Profile is required');
|
|
444
|
+
}
|
|
445
|
+
if (!body?.groupId) {
|
|
446
|
+
return jsonError('Group ID is required');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
|
|
450
|
+
if (!whatsappAdapter) {
|
|
451
|
+
return jsonError('WhatsApp not configured', 404);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
const group = await whatsappAdapter.getGroup(body.profile, body.groupId);
|
|
456
|
+
const response: WhatsAppGroupGetResponse = { group };
|
|
457
|
+
return jsonResponse(response);
|
|
458
|
+
} catch (error) {
|
|
459
|
+
const message = error instanceof Error ? error.message : 'Failed to get group';
|
|
460
|
+
return jsonError(message, 500);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Handle WhatsApp group create request
|
|
466
|
+
*/
|
|
467
|
+
async function handleWhatsAppGroupCreate(request: Request): Promise<Response> {
|
|
468
|
+
const body = await parseJsonBody<WhatsAppGroupCreateRequest>(request);
|
|
469
|
+
|
|
470
|
+
if (!body?.profile) {
|
|
471
|
+
return jsonError('Profile is required');
|
|
472
|
+
}
|
|
473
|
+
if (!body?.name) {
|
|
474
|
+
return jsonError('Group name is required');
|
|
475
|
+
}
|
|
476
|
+
if (!body?.participants || body.participants.length === 0) {
|
|
477
|
+
return jsonError('At least one participant is required');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
|
|
481
|
+
if (!whatsappAdapter) {
|
|
482
|
+
return jsonError('WhatsApp not configured', 404);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
const group = await whatsappAdapter.createGroup(body.profile, body.name, body.participants, body.picture);
|
|
487
|
+
const response: WhatsAppGroupCreateResponse = { group };
|
|
488
|
+
return jsonResponse(response);
|
|
489
|
+
} catch (error) {
|
|
490
|
+
const message = error instanceof Error ? error.message : 'Failed to create group';
|
|
491
|
+
return jsonError(message, 500);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Handle WhatsApp group update request
|
|
497
|
+
*/
|
|
498
|
+
async function handleWhatsAppGroupUpdate(request: Request): Promise<Response> {
|
|
499
|
+
const body = await parseJsonBody<WhatsAppGroupUpdateRequest>(request);
|
|
500
|
+
|
|
501
|
+
if (!body?.profile) {
|
|
502
|
+
return jsonError('Profile is required');
|
|
503
|
+
}
|
|
504
|
+
if (!body?.groupId) {
|
|
505
|
+
return jsonError('Group ID is required');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
|
|
509
|
+
if (!whatsappAdapter) {
|
|
510
|
+
return jsonError('WhatsApp not configured', 404);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
if (body.subject) {
|
|
515
|
+
await whatsappAdapter.updateGroupSubject(body.profile, body.groupId, body.subject);
|
|
516
|
+
}
|
|
517
|
+
if (body.description !== undefined) {
|
|
518
|
+
await whatsappAdapter.updateGroupDescription(body.profile, body.groupId, body.description);
|
|
519
|
+
}
|
|
520
|
+
if (body.picture) {
|
|
521
|
+
await whatsappAdapter.updateGroupPicture(body.profile, body.groupId, body.picture);
|
|
522
|
+
}
|
|
523
|
+
const response: WhatsAppGroupUpdateResponse = { success: true };
|
|
524
|
+
return jsonResponse(response);
|
|
525
|
+
} catch (error) {
|
|
526
|
+
const message = error instanceof Error ? error.message : 'Failed to update group';
|
|
527
|
+
return jsonError(message, 500);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Handle WhatsApp group participants update request
|
|
533
|
+
*/
|
|
534
|
+
async function handleWhatsAppGroupParticipants(request: Request): Promise<Response> {
|
|
535
|
+
const body = await parseJsonBody<WhatsAppGroupParticipantsRequest>(request);
|
|
536
|
+
|
|
537
|
+
if (!body?.profile) {
|
|
538
|
+
return jsonError('Profile is required');
|
|
539
|
+
}
|
|
540
|
+
if (!body?.groupId) {
|
|
541
|
+
return jsonError('Group ID is required');
|
|
542
|
+
}
|
|
543
|
+
if (!body?.participants || body.participants.length === 0) {
|
|
544
|
+
return jsonError('At least one participant is required');
|
|
545
|
+
}
|
|
546
|
+
if (!body?.action) {
|
|
547
|
+
return jsonError('Action is required (add, remove, promote, demote)');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
|
|
551
|
+
if (!whatsappAdapter) {
|
|
552
|
+
return jsonError('WhatsApp not configured', 404);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
const results = await whatsappAdapter.updateParticipants(
|
|
557
|
+
body.profile,
|
|
558
|
+
body.groupId,
|
|
559
|
+
body.participants,
|
|
560
|
+
body.action
|
|
561
|
+
);
|
|
562
|
+
const response: WhatsAppGroupParticipantsResponse = { success: true, results };
|
|
563
|
+
return jsonResponse(response);
|
|
564
|
+
} catch (error) {
|
|
565
|
+
const message = error instanceof Error ? error.message : 'Failed to update participants';
|
|
566
|
+
return jsonError(message, 500);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Handle WhatsApp group leave request
|
|
572
|
+
*/
|
|
573
|
+
async function handleWhatsAppGroupLeave(request: Request): Promise<Response> {
|
|
574
|
+
const body = await parseJsonBody<WhatsAppGroupLeaveRequest>(request);
|
|
575
|
+
|
|
576
|
+
if (!body?.profile) {
|
|
577
|
+
return jsonError('Profile is required');
|
|
578
|
+
}
|
|
579
|
+
if (!body?.groupId) {
|
|
580
|
+
return jsonError('Group ID is required');
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
|
|
584
|
+
if (!whatsappAdapter) {
|
|
585
|
+
return jsonError('WhatsApp not configured', 404);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
await whatsappAdapter.leaveGroup(body.profile, body.groupId);
|
|
590
|
+
const response: WhatsAppGroupLeaveResponse = { success: true };
|
|
591
|
+
return jsonResponse(response);
|
|
592
|
+
} catch (error) {
|
|
593
|
+
const message = error instanceof Error ? error.message : 'Failed to leave group';
|
|
594
|
+
return jsonError(message, 500);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Handle WhatsApp group invite code request
|
|
600
|
+
*/
|
|
601
|
+
async function handleWhatsAppGroupInvite(request: Request): Promise<Response> {
|
|
602
|
+
const body = await parseJsonBody<WhatsAppGroupInviteRequest>(request);
|
|
603
|
+
|
|
604
|
+
if (!body?.profile) {
|
|
605
|
+
return jsonError('Profile is required');
|
|
606
|
+
}
|
|
607
|
+
if (!body?.groupId) {
|
|
608
|
+
return jsonError('Group ID is required');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
|
|
612
|
+
if (!whatsappAdapter) {
|
|
613
|
+
return jsonError('WhatsApp not configured', 404);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
try {
|
|
617
|
+
const inviteCode = await whatsappAdapter.getGroupInviteCode(body.profile, body.groupId);
|
|
618
|
+
const response: WhatsAppGroupInviteResponse = {
|
|
619
|
+
inviteCode,
|
|
620
|
+
inviteLink: `https://chat.whatsapp.com/${inviteCode}`,
|
|
621
|
+
};
|
|
622
|
+
return jsonResponse(response);
|
|
623
|
+
} catch (error) {
|
|
624
|
+
const message = error instanceof Error ? error.message : 'Failed to get invite code';
|
|
625
|
+
return jsonError(message, 500);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Handle WhatsApp group join request
|
|
631
|
+
*/
|
|
632
|
+
async function handleWhatsAppGroupJoin(request: Request): Promise<Response> {
|
|
633
|
+
const body = await parseJsonBody<WhatsAppGroupJoinRequest>(request);
|
|
634
|
+
|
|
635
|
+
if (!body?.profile) {
|
|
636
|
+
return jsonError('Profile is required');
|
|
637
|
+
}
|
|
638
|
+
if (!body?.inviteCode) {
|
|
639
|
+
return jsonError('Invite code is required');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
|
|
643
|
+
if (!whatsappAdapter) {
|
|
644
|
+
return jsonError('WhatsApp not configured', 404);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
try {
|
|
648
|
+
const groupId = await whatsappAdapter.joinGroupViaInvite(body.profile, body.inviteCode);
|
|
649
|
+
const response: WhatsAppGroupJoinResponse = { groupId };
|
|
650
|
+
return jsonResponse(response);
|
|
651
|
+
} catch (error) {
|
|
652
|
+
const message = error instanceof Error ? error.message : 'Failed to join group';
|
|
653
|
+
return jsonError(message, 500);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Handle WhatsApp group resolve request (name to JID or JID to name)
|
|
659
|
+
*/
|
|
660
|
+
async function handleWhatsAppGroupResolve(request: Request): Promise<Response> {
|
|
661
|
+
const body = await parseJsonBody<WhatsAppGroupResolveRequest>(request);
|
|
662
|
+
|
|
663
|
+
if (!body?.profile) {
|
|
664
|
+
return jsonError('Profile is required');
|
|
665
|
+
}
|
|
666
|
+
if (!body?.nameOrId) {
|
|
667
|
+
return jsonError('Name or ID is required');
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
|
|
671
|
+
if (!whatsappAdapter) {
|
|
672
|
+
return jsonError('WhatsApp not configured', 404);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
try {
|
|
676
|
+
const result = await whatsappAdapter.resolveGroup(body.profile, body.nameOrId);
|
|
677
|
+
const response: WhatsAppGroupResolveResponse = result;
|
|
678
|
+
return jsonResponse(response);
|
|
679
|
+
} catch (error) {
|
|
680
|
+
const message = error instanceof Error ? error.message : 'Failed to resolve group';
|
|
681
|
+
return jsonError(message, 500);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Main request handler
|
|
687
|
+
*/
|
|
688
|
+
async function handleRequest(request: Request): Promise<Response> {
|
|
689
|
+
const url = new URL(request.url);
|
|
690
|
+
const path = url.pathname;
|
|
691
|
+
|
|
692
|
+
// CORS preflight
|
|
693
|
+
if (request.method === 'OPTIONS') {
|
|
694
|
+
return new Response(null, {
|
|
695
|
+
status: 204,
|
|
696
|
+
headers: {
|
|
697
|
+
'Access-Control-Allow-Origin': '*',
|
|
698
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
699
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
700
|
+
},
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Health check doesn't require auth
|
|
705
|
+
if (path === '/health' && request.method === 'GET') {
|
|
706
|
+
return handleHealth();
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// All other endpoints require auth
|
|
710
|
+
if (!verifyAuth(request)) {
|
|
711
|
+
return jsonError('Unauthorized', 401);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Route requests
|
|
715
|
+
if (request.method === 'GET') {
|
|
716
|
+
if (path === '/status') return handleStatus();
|
|
717
|
+
if (path.startsWith('/media/')) {
|
|
718
|
+
const id = path.slice('/media/'.length);
|
|
719
|
+
return handleMedia(id);
|
|
720
|
+
}
|
|
721
|
+
if (path.startsWith('/whatsapp/pair/')) {
|
|
722
|
+
const profile = decodeURIComponent(path.slice('/whatsapp/pair/'.length));
|
|
723
|
+
return handleWhatsAppPair(profile);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (request.method === 'POST') {
|
|
728
|
+
if (path === '/inbox/pull') return handleInboxPull(request);
|
|
729
|
+
if (path === '/inbox/get') return handleInboxGet(request);
|
|
730
|
+
if (path === '/inbox/ack') return handleInboxAck(request);
|
|
731
|
+
if (path === '/inbox/reply') return handleInboxReply(request);
|
|
732
|
+
if (path === '/inbox/stats') return handleInboxStats(request);
|
|
733
|
+
if (path === '/outbox/send') return handleOutboxSend(request);
|
|
734
|
+
if (path === '/outbox/status') return handleOutboxStatus(request);
|
|
735
|
+
if (path === '/outbox/list') return handleOutboxList(request);
|
|
736
|
+
// Teleport import endpoints
|
|
737
|
+
if (path.startsWith('/import/whatsapp/')) {
|
|
738
|
+
const profile = decodeURIComponent(path.slice('/import/whatsapp/'.length));
|
|
739
|
+
return handleWhatsAppImport(profile, request);
|
|
740
|
+
}
|
|
741
|
+
// WhatsApp group endpoints
|
|
742
|
+
if (path === '/whatsapp/groups/list') return handleWhatsAppGroupList(request);
|
|
743
|
+
if (path === '/whatsapp/groups/get') return handleWhatsAppGroupGet(request);
|
|
744
|
+
if (path === '/whatsapp/groups/create') return handleWhatsAppGroupCreate(request);
|
|
745
|
+
if (path === '/whatsapp/groups/update') return handleWhatsAppGroupUpdate(request);
|
|
746
|
+
if (path === '/whatsapp/groups/participants') return handleWhatsAppGroupParticipants(request);
|
|
747
|
+
if (path === '/whatsapp/groups/leave') return handleWhatsAppGroupLeave(request);
|
|
748
|
+
if (path === '/whatsapp/groups/invite') return handleWhatsAppGroupInvite(request);
|
|
749
|
+
if (path === '/whatsapp/groups/join') return handleWhatsAppGroupJoin(request);
|
|
750
|
+
if (path === '/whatsapp/groups/resolve') return handleWhatsAppGroupResolve(request);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return jsonError('Not found', 404);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Start the API server
|
|
758
|
+
*/
|
|
759
|
+
export function startApiServer(config: GatewayConfig['api'], serviceAdapters: Map<ServiceName, ServiceAdapter>): Server<unknown> {
|
|
760
|
+
const port = config?.port ?? DEFAULT_GATEWAY_CONFIG.api.port;
|
|
761
|
+
const host = config?.host ?? DEFAULT_GATEWAY_CONFIG.api.host;
|
|
762
|
+
apiSecret = config?.secret ?? '';
|
|
763
|
+
startTime = Date.now();
|
|
764
|
+
adapters = serviceAdapters;
|
|
765
|
+
|
|
766
|
+
server = Bun.serve({
|
|
767
|
+
port,
|
|
768
|
+
hostname: host,
|
|
769
|
+
fetch: handleRequest,
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
console.log(`Gateway API listening on http://${host}:${port}`);
|
|
773
|
+
return server;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Stop the API server
|
|
778
|
+
*/
|
|
779
|
+
export function stopApiServer(): void {
|
|
780
|
+
if (server) {
|
|
781
|
+
server.stop();
|
|
782
|
+
server = null;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Check if server is running
|
|
788
|
+
*/
|
|
789
|
+
export function isApiServerRunning(): boolean {
|
|
790
|
+
return server !== null;
|
|
791
|
+
}
|