@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,723 @@
|
|
|
1
|
+
import makeWASocket, {
|
|
2
|
+
DisconnectReason,
|
|
3
|
+
WASocket,
|
|
4
|
+
ConnectionState,
|
|
5
|
+
WAMessage,
|
|
6
|
+
MessageUpsertType,
|
|
7
|
+
jidNormalizedUser,
|
|
8
|
+
isJidGroup,
|
|
9
|
+
getContentType,
|
|
10
|
+
downloadMediaMessage,
|
|
11
|
+
} from '@whiskeysockets/baileys';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { mkdir } from 'fs/promises';
|
|
14
|
+
import { existsSync } from 'fs';
|
|
15
|
+
import type { ServiceName } from '../../types/config';
|
|
16
|
+
import type {
|
|
17
|
+
WhatsAppCredentials,
|
|
18
|
+
WhatsAppGroup,
|
|
19
|
+
WhatsAppGroupParticipant,
|
|
20
|
+
WhatsAppParticipantAction,
|
|
21
|
+
} from '../../types/whatsapp';
|
|
22
|
+
import { CONFIG_DIR } from '../../config/config-manager';
|
|
23
|
+
import { BaseAdapter, type AdapterInboundMessage, type AdapterOutboundMessage, type SendResult, type ConnectionState as AdapterConnectionState } from './types';
|
|
24
|
+
import { useSQLiteAuthState, hasAuthState } from './whatsapp-auth';
|
|
25
|
+
|
|
26
|
+
// Media storage directory
|
|
27
|
+
const MEDIA_DIR = join(CONFIG_DIR, 'media');
|
|
28
|
+
|
|
29
|
+
// Ensure media directory exists
|
|
30
|
+
async function ensureMediaDir(): Promise<void> {
|
|
31
|
+
if (!existsSync(MEDIA_DIR)) {
|
|
32
|
+
await mkdir(MEDIA_DIR, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Get file extension for media type
|
|
37
|
+
function getMediaExtension(mediaType: string, mimeType?: string): string {
|
|
38
|
+
if (mimeType) {
|
|
39
|
+
const ext = mimeType.split('/')[1]?.split(';')[0];
|
|
40
|
+
if (ext) return `.${ext}`;
|
|
41
|
+
}
|
|
42
|
+
switch (mediaType) {
|
|
43
|
+
case 'image': return '.jpg';
|
|
44
|
+
case 'video': return '.mp4';
|
|
45
|
+
case 'audio': return '.ogg';
|
|
46
|
+
case 'document': return '.bin';
|
|
47
|
+
default: return '.bin';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ProfileConnection {
|
|
52
|
+
socket: WASocket | null;
|
|
53
|
+
credentials: WhatsAppCredentials;
|
|
54
|
+
authState: Awaited<ReturnType<typeof useSQLiteAuthState>> | null;
|
|
55
|
+
qrCode: string | null;
|
|
56
|
+
shouldStop: boolean;
|
|
57
|
+
reconnectAttempts: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Extract phone number from JID
|
|
61
|
+
function jidToPhone(jid: string): string {
|
|
62
|
+
const normalized = jidNormalizedUser(jid);
|
|
63
|
+
return normalized.split('@')[0];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Convert phone number to JID
|
|
67
|
+
function phoneToJid(phone: string): string {
|
|
68
|
+
// Remove any non-digit characters except leading +
|
|
69
|
+
const cleaned = phone.replace(/[^\d]/g, '');
|
|
70
|
+
return `${cleaned}@s.whatsapp.net`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class WhatsAppAdapter extends BaseAdapter {
|
|
74
|
+
readonly service: ServiceName = 'whatsapp';
|
|
75
|
+
|
|
76
|
+
private profiles: Map<string, ProfileConnection> = new Map();
|
|
77
|
+
|
|
78
|
+
async connect(profile: string, credentials: unknown): Promise<void> {
|
|
79
|
+
const creds = credentials as WhatsAppCredentials;
|
|
80
|
+
|
|
81
|
+
// Stop existing connection if any
|
|
82
|
+
await this.disconnect(profile);
|
|
83
|
+
|
|
84
|
+
const connection: ProfileConnection = {
|
|
85
|
+
socket: null,
|
|
86
|
+
credentials: creds,
|
|
87
|
+
authState: null,
|
|
88
|
+
qrCode: null,
|
|
89
|
+
shouldStop: false,
|
|
90
|
+
reconnectAttempts: 0,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
this.profiles.set(profile, connection);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// Initialize auth state from SQLite
|
|
97
|
+
const authState = await useSQLiteAuthState(profile);
|
|
98
|
+
connection.authState = authState;
|
|
99
|
+
|
|
100
|
+
await this.createSocket(profile);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
const message = error instanceof Error ? error.message : 'Connection failed';
|
|
103
|
+
this.setConnected(profile, false, message);
|
|
104
|
+
this.profiles.delete(profile);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private async createSocket(profile: string): Promise<void> {
|
|
110
|
+
const connection = this.profiles.get(profile);
|
|
111
|
+
if (!connection || !connection.authState) return;
|
|
112
|
+
|
|
113
|
+
const { state, saveCreds } = connection.authState;
|
|
114
|
+
|
|
115
|
+
// Create a silent logger that satisfies Baileys' requirements
|
|
116
|
+
const silentLogger: any = {
|
|
117
|
+
level: 'silent',
|
|
118
|
+
trace: () => {},
|
|
119
|
+
debug: () => {},
|
|
120
|
+
info: () => {},
|
|
121
|
+
warn: console.warn,
|
|
122
|
+
error: console.error,
|
|
123
|
+
fatal: console.error,
|
|
124
|
+
};
|
|
125
|
+
// child() must return a logger with all methods
|
|
126
|
+
silentLogger.child = () => silentLogger;
|
|
127
|
+
|
|
128
|
+
// Create socket with auth state
|
|
129
|
+
const socket = makeWASocket({
|
|
130
|
+
auth: state,
|
|
131
|
+
printQRInTerminal: false, // We'll handle QR ourselves
|
|
132
|
+
browser: ['agentio', 'Chrome', '120.0.0'],
|
|
133
|
+
logger: silentLogger,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
connection.socket = socket;
|
|
137
|
+
|
|
138
|
+
// Handle connection updates
|
|
139
|
+
socket.ev.on('connection.update', async (update) => {
|
|
140
|
+
await this.handleConnectionUpdate(profile, update);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Handle credential updates
|
|
144
|
+
socket.ev.on('creds.update', saveCreds);
|
|
145
|
+
|
|
146
|
+
// Handle incoming messages
|
|
147
|
+
socket.ev.on('messages.upsert', (upsert) => {
|
|
148
|
+
this.handleMessagesUpsert(profile, upsert);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async handleConnectionUpdate(profile: string, update: Partial<ConnectionState>): Promise<void> {
|
|
153
|
+
const connection = this.profiles.get(profile);
|
|
154
|
+
if (!connection) return;
|
|
155
|
+
|
|
156
|
+
const { connection: connState, lastDisconnect, qr } = update;
|
|
157
|
+
|
|
158
|
+
// Handle QR code
|
|
159
|
+
if (qr) {
|
|
160
|
+
connection.qrCode = qr;
|
|
161
|
+
console.log(`[whatsapp:${profile}] Scan QR code to connect`);
|
|
162
|
+
// In a real implementation, you'd want to emit this to a pairing endpoint
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (connState === 'open') {
|
|
166
|
+
connection.qrCode = null;
|
|
167
|
+
connection.reconnectAttempts = 0;
|
|
168
|
+
this.setConnected(profile, true);
|
|
169
|
+
|
|
170
|
+
// Update credentials with phone number if available
|
|
171
|
+
const user = connection.socket?.user;
|
|
172
|
+
if (user) {
|
|
173
|
+
connection.credentials.phoneNumber = jidToPhone(user.id);
|
|
174
|
+
connection.credentials.displayName = user.name || user.verifiedName;
|
|
175
|
+
connection.credentials.paired = true;
|
|
176
|
+
connection.credentials.lastConnected = Date.now();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log(`[whatsapp:${profile}] Connected as ${connection.credentials.phoneNumber || 'unknown'}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (connState === 'close') {
|
|
183
|
+
const statusCode = (lastDisconnect?.error as any)?.output?.statusCode;
|
|
184
|
+
const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
|
|
185
|
+
|
|
186
|
+
if (statusCode === DisconnectReason.loggedOut) {
|
|
187
|
+
console.log(`[whatsapp:${profile}] Logged out, clearing session`);
|
|
188
|
+
connection.credentials.paired = false;
|
|
189
|
+
if (connection.authState) {
|
|
190
|
+
await connection.authState.clearState();
|
|
191
|
+
}
|
|
192
|
+
this.setConnected(profile, false, 'Logged out');
|
|
193
|
+
} else if (shouldReconnect && !connection.shouldStop) {
|
|
194
|
+
connection.reconnectAttempts++;
|
|
195
|
+
const delay = Math.min(connection.reconnectAttempts * 2000, 30000);
|
|
196
|
+
console.log(`[whatsapp:${profile}] Reconnecting in ${delay / 1000}s...`);
|
|
197
|
+
this.setConnected(profile, false, 'Reconnecting...');
|
|
198
|
+
|
|
199
|
+
setTimeout(() => {
|
|
200
|
+
if (!connection.shouldStop) {
|
|
201
|
+
this.createSocket(profile);
|
|
202
|
+
}
|
|
203
|
+
}, delay);
|
|
204
|
+
} else {
|
|
205
|
+
this.setConnected(profile, false, 'Disconnected');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private handleMessagesUpsert(profile: string, upsert: { messages: WAMessage[]; type: MessageUpsertType }): void {
|
|
211
|
+
const connection = this.profiles.get(profile);
|
|
212
|
+
if (!connection) return;
|
|
213
|
+
|
|
214
|
+
// Only process new messages (not history sync)
|
|
215
|
+
if (upsert.type !== 'notify') return;
|
|
216
|
+
|
|
217
|
+
for (const msg of upsert.messages) {
|
|
218
|
+
// Skip messages from self
|
|
219
|
+
if (msg.key.fromMe) continue;
|
|
220
|
+
|
|
221
|
+
// Skip status broadcasts
|
|
222
|
+
if (msg.key.remoteJid === 'status@broadcast') continue;
|
|
223
|
+
|
|
224
|
+
// Process message asynchronously (for media download)
|
|
225
|
+
this.processMessage(profile, msg, connection.socket).catch((error) => {
|
|
226
|
+
console.error(`[whatsapp:${profile}] Error processing message:`, error);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private async processMessage(profile: string, msg: WAMessage, socket: WASocket | null): Promise<void> {
|
|
232
|
+
const message = await this.parseMessage(profile, msg, socket);
|
|
233
|
+
if (message) {
|
|
234
|
+
this.emitMessage(profile, message);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private async parseMessage(profile: string, msg: WAMessage, socket: WASocket | null): Promise<AdapterInboundMessage | null> {
|
|
239
|
+
const remoteJid = msg.key.remoteJid;
|
|
240
|
+
if (!remoteJid) return null;
|
|
241
|
+
|
|
242
|
+
const messageContent = msg.message;
|
|
243
|
+
if (!messageContent) return null;
|
|
244
|
+
|
|
245
|
+
// Determine sender
|
|
246
|
+
let senderId = remoteJid;
|
|
247
|
+
let senderName: string | undefined;
|
|
248
|
+
|
|
249
|
+
if (isJidGroup(remoteJid)) {
|
|
250
|
+
// Group message - get actual sender
|
|
251
|
+
senderId = msg.key.participant || remoteJid;
|
|
252
|
+
senderName = msg.pushName ?? undefined;
|
|
253
|
+
} else {
|
|
254
|
+
// Private message
|
|
255
|
+
senderName = msg.pushName ?? undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Extract message content
|
|
259
|
+
let content: string | undefined;
|
|
260
|
+
let mediaType: AdapterInboundMessage['mediaType'];
|
|
261
|
+
let mediaPath: string | undefined;
|
|
262
|
+
let mimeType: string | undefined;
|
|
263
|
+
|
|
264
|
+
const contentType = getContentType(messageContent);
|
|
265
|
+
|
|
266
|
+
switch (contentType) {
|
|
267
|
+
case 'conversation':
|
|
268
|
+
content = messageContent.conversation || undefined;
|
|
269
|
+
break;
|
|
270
|
+
case 'extendedTextMessage':
|
|
271
|
+
content = messageContent.extendedTextMessage?.text || undefined;
|
|
272
|
+
break;
|
|
273
|
+
case 'imageMessage':
|
|
274
|
+
mediaType = 'image';
|
|
275
|
+
content = messageContent.imageMessage?.caption || undefined;
|
|
276
|
+
mimeType = messageContent.imageMessage?.mimetype || undefined;
|
|
277
|
+
break;
|
|
278
|
+
case 'videoMessage':
|
|
279
|
+
mediaType = 'video';
|
|
280
|
+
content = messageContent.videoMessage?.caption || undefined;
|
|
281
|
+
mimeType = messageContent.videoMessage?.mimetype || undefined;
|
|
282
|
+
break;
|
|
283
|
+
case 'audioMessage':
|
|
284
|
+
mediaType = 'audio';
|
|
285
|
+
mimeType = messageContent.audioMessage?.mimetype || undefined;
|
|
286
|
+
break;
|
|
287
|
+
case 'documentMessage':
|
|
288
|
+
mediaType = 'document';
|
|
289
|
+
content = messageContent.documentMessage?.caption || undefined;
|
|
290
|
+
mimeType = messageContent.documentMessage?.mimetype || undefined;
|
|
291
|
+
break;
|
|
292
|
+
default:
|
|
293
|
+
// Unsupported message type
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Download media if present
|
|
298
|
+
if (mediaType && socket) {
|
|
299
|
+
try {
|
|
300
|
+
await ensureMediaDir();
|
|
301
|
+
const buffer = await downloadMediaMessage(
|
|
302
|
+
msg,
|
|
303
|
+
'buffer',
|
|
304
|
+
{},
|
|
305
|
+
{
|
|
306
|
+
logger: { level: 'silent', child: () => ({ level: 'silent' }) } as any,
|
|
307
|
+
reuploadRequest: socket.updateMediaMessage,
|
|
308
|
+
}
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
if (buffer) {
|
|
312
|
+
const extension = getMediaExtension(mediaType, mimeType);
|
|
313
|
+
const filename = `${msg.key.id || Date.now()}${extension}`;
|
|
314
|
+
mediaPath = join(MEDIA_DIR, filename);
|
|
315
|
+
await Bun.write(mediaPath, buffer);
|
|
316
|
+
console.log(`[whatsapp:${profile}] Downloaded media: ${filename}`);
|
|
317
|
+
}
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.error(`[whatsapp:${profile}] Failed to download media:`, error);
|
|
320
|
+
// Continue without media - message still gets stored
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
conversationId: remoteJid,
|
|
326
|
+
platformId: msg.key.id || `${Date.now()}`,
|
|
327
|
+
senderId,
|
|
328
|
+
senderName,
|
|
329
|
+
senderHandle: jidToPhone(senderId),
|
|
330
|
+
content,
|
|
331
|
+
mediaType,
|
|
332
|
+
mediaUrl: mediaPath, // Store local path as mediaUrl
|
|
333
|
+
receivedAt: (msg.messageTimestamp as number) * 1000 || Date.now(),
|
|
334
|
+
replyToId: messageContent.extendedTextMessage?.contextInfo?.stanzaId ?? undefined,
|
|
335
|
+
metadata: {
|
|
336
|
+
isGroup: isJidGroup(remoteJid),
|
|
337
|
+
pushName: msg.pushName,
|
|
338
|
+
mimeType,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async disconnect(profile: string): Promise<void> {
|
|
344
|
+
const connection = this.profiles.get(profile);
|
|
345
|
+
if (!connection) return;
|
|
346
|
+
|
|
347
|
+
connection.shouldStop = true;
|
|
348
|
+
|
|
349
|
+
if (connection.socket) {
|
|
350
|
+
try {
|
|
351
|
+
connection.socket.end(undefined);
|
|
352
|
+
} catch {
|
|
353
|
+
// Ignore disconnect errors
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
this.profiles.delete(profile);
|
|
358
|
+
this.connections.delete(profile);
|
|
359
|
+
console.log(`[whatsapp:${profile}] Disconnected`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async disconnectAll(): Promise<void> {
|
|
363
|
+
const profiles = Array.from(this.profiles.keys());
|
|
364
|
+
await Promise.all(profiles.map((p) => this.disconnect(p)));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async send(profile: string, message: AdapterOutboundMessage): Promise<SendResult> {
|
|
368
|
+
const connection = this.profiles.get(profile);
|
|
369
|
+
if (!connection || !connection.socket) {
|
|
370
|
+
return { success: false, error: 'Profile not connected' };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
// Determine recipient JID
|
|
375
|
+
let jid = message.conversationId;
|
|
376
|
+
if (!jid.includes('@')) {
|
|
377
|
+
jid = phoneToJid(jid);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let result;
|
|
381
|
+
|
|
382
|
+
// Handle media attachments
|
|
383
|
+
if (message.mediaPath) {
|
|
384
|
+
const file = Bun.file(message.mediaPath);
|
|
385
|
+
if (!await file.exists()) {
|
|
386
|
+
return { success: false, error: `File not found: ${message.mediaPath}` };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
390
|
+
const mimeType = file.type || 'application/octet-stream';
|
|
391
|
+
const filename = message.mediaPath.split('/').pop() || 'file';
|
|
392
|
+
|
|
393
|
+
switch (message.mediaType) {
|
|
394
|
+
case 'image':
|
|
395
|
+
result = await connection.socket.sendMessage(jid, {
|
|
396
|
+
image: buffer,
|
|
397
|
+
caption: message.content,
|
|
398
|
+
mimetype: mimeType,
|
|
399
|
+
});
|
|
400
|
+
break;
|
|
401
|
+
case 'video':
|
|
402
|
+
result = await connection.socket.sendMessage(jid, {
|
|
403
|
+
video: buffer,
|
|
404
|
+
caption: message.content,
|
|
405
|
+
mimetype: mimeType,
|
|
406
|
+
});
|
|
407
|
+
break;
|
|
408
|
+
case 'audio':
|
|
409
|
+
result = await connection.socket.sendMessage(jid, {
|
|
410
|
+
audio: buffer,
|
|
411
|
+
mimetype: mimeType,
|
|
412
|
+
ptt: mimeType.includes('ogg'), // Voice note if ogg
|
|
413
|
+
});
|
|
414
|
+
break;
|
|
415
|
+
case 'document':
|
|
416
|
+
default:
|
|
417
|
+
result = await connection.socket.sendMessage(jid, {
|
|
418
|
+
document: buffer,
|
|
419
|
+
fileName: filename,
|
|
420
|
+
caption: message.content,
|
|
421
|
+
mimetype: mimeType,
|
|
422
|
+
});
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
} else if (message.content) {
|
|
426
|
+
// Text-only message
|
|
427
|
+
result = await connection.socket.sendMessage(jid, { text: message.content });
|
|
428
|
+
} else {
|
|
429
|
+
return { success: false, error: 'No content or media to send' };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
success: true,
|
|
434
|
+
platformId: result?.key?.id ?? undefined,
|
|
435
|
+
};
|
|
436
|
+
} catch (error) {
|
|
437
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
438
|
+
return { success: false, error: errorMessage };
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Get the current QR code for pairing (if available)
|
|
444
|
+
*/
|
|
445
|
+
getQRCode(profile: string): string | null {
|
|
446
|
+
return this.profiles.get(profile)?.qrCode ?? null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Check if a profile needs pairing (no existing session)
|
|
451
|
+
*/
|
|
452
|
+
needsPairing(profile: string): boolean {
|
|
453
|
+
return !hasAuthState(profile);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Get connection state with extra WhatsApp-specific info
|
|
458
|
+
*/
|
|
459
|
+
getWhatsAppState(profile: string): AdapterConnectionState & {
|
|
460
|
+
qrCode?: string;
|
|
461
|
+
phoneNumber?: string;
|
|
462
|
+
paired: boolean;
|
|
463
|
+
} {
|
|
464
|
+
const connection = this.profiles.get(profile);
|
|
465
|
+
const baseState = this.getConnectionState(profile);
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
...baseState,
|
|
469
|
+
qrCode: connection?.qrCode ?? undefined,
|
|
470
|
+
phoneNumber: connection?.credentials.phoneNumber,
|
|
471
|
+
paired: connection?.credentials.paired ?? false,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ============ GROUP OPERATIONS ============
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* List all groups the user is participating in
|
|
479
|
+
*/
|
|
480
|
+
async listGroups(profile: string): Promise<WhatsAppGroup[]> {
|
|
481
|
+
const connection = this.profiles.get(profile);
|
|
482
|
+
if (!connection?.socket) {
|
|
483
|
+
throw new Error('Profile not connected');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const groups = await connection.socket.groupFetchAllParticipating();
|
|
487
|
+
const myJid = connection.socket.user?.id;
|
|
488
|
+
|
|
489
|
+
return Object.values(groups).map((g) => {
|
|
490
|
+
const myParticipant = g.participants?.find((p) => p.id === myJid);
|
|
491
|
+
return {
|
|
492
|
+
id: g.id,
|
|
493
|
+
name: g.subject,
|
|
494
|
+
description: g.desc ?? undefined,
|
|
495
|
+
owner: g.owner ?? undefined,
|
|
496
|
+
creation: g.creation,
|
|
497
|
+
participantCount: g.participants?.length ?? 0,
|
|
498
|
+
isAdmin: myParticipant?.admin === 'admin' || myParticipant?.admin === 'superadmin',
|
|
499
|
+
isSuperAdmin: myParticipant?.admin === 'superadmin',
|
|
500
|
+
announce: g.announce ?? false,
|
|
501
|
+
restrict: g.restrict ?? false,
|
|
502
|
+
};
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Get detailed group information
|
|
508
|
+
*/
|
|
509
|
+
async getGroup(profile: string, groupId: string): Promise<WhatsAppGroup> {
|
|
510
|
+
const connection = this.profiles.get(profile);
|
|
511
|
+
if (!connection?.socket) {
|
|
512
|
+
throw new Error('Profile not connected');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const metadata = await connection.socket.groupMetadata(groupId);
|
|
516
|
+
const myJid = connection.socket.user?.id;
|
|
517
|
+
const myParticipant = metadata.participants?.find((p) => p.id === myJid);
|
|
518
|
+
|
|
519
|
+
const participants: WhatsAppGroupParticipant[] = metadata.participants.map((p) => ({
|
|
520
|
+
id: p.id,
|
|
521
|
+
phone: jidToPhone(p.id),
|
|
522
|
+
isAdmin: p.admin === 'admin' || p.admin === 'superadmin',
|
|
523
|
+
isSuperAdmin: p.admin === 'superadmin',
|
|
524
|
+
}));
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
id: metadata.id,
|
|
528
|
+
name: metadata.subject,
|
|
529
|
+
description: metadata.desc ?? undefined,
|
|
530
|
+
owner: metadata.owner ?? undefined,
|
|
531
|
+
creation: metadata.creation,
|
|
532
|
+
participantCount: metadata.participants.length,
|
|
533
|
+
participants,
|
|
534
|
+
isAdmin: myParticipant?.admin === 'admin' || myParticipant?.admin === 'superadmin',
|
|
535
|
+
isSuperAdmin: myParticipant?.admin === 'superadmin',
|
|
536
|
+
announce: metadata.announce ?? false,
|
|
537
|
+
restrict: metadata.restrict ?? false,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Create a new group
|
|
543
|
+
*/
|
|
544
|
+
async createGroup(profile: string, name: string, participants: string[], picturePath?: string): Promise<WhatsAppGroup> {
|
|
545
|
+
const connection = this.profiles.get(profile);
|
|
546
|
+
if (!connection?.socket) {
|
|
547
|
+
throw new Error('Profile not connected');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const jids = participants.map((p) => (p.includes('@') ? p : phoneToJid(p)));
|
|
551
|
+
const result = await connection.socket.groupCreate(name, jids);
|
|
552
|
+
|
|
553
|
+
// Set profile picture if provided
|
|
554
|
+
if (picturePath) {
|
|
555
|
+
await this.updateGroupPicture(profile, result.id, picturePath);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return this.getGroup(profile, result.id);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Update group profile picture
|
|
563
|
+
*/
|
|
564
|
+
async updateGroupPicture(profile: string, groupId: string, picturePath: string): Promise<void> {
|
|
565
|
+
const connection = this.profiles.get(profile);
|
|
566
|
+
if (!connection?.socket) {
|
|
567
|
+
throw new Error('Profile not connected');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const file = Bun.file(picturePath);
|
|
571
|
+
if (!await file.exists()) {
|
|
572
|
+
throw new Error(`File not found: ${picturePath}`);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
576
|
+
await connection.socket.updateProfilePicture(groupId, buffer);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Update group subject (name)
|
|
581
|
+
*/
|
|
582
|
+
async updateGroupSubject(profile: string, groupId: string, subject: string): Promise<void> {
|
|
583
|
+
const connection = this.profiles.get(profile);
|
|
584
|
+
if (!connection?.socket) {
|
|
585
|
+
throw new Error('Profile not connected');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
await connection.socket.groupUpdateSubject(groupId, subject);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Update group description
|
|
593
|
+
*/
|
|
594
|
+
async updateGroupDescription(profile: string, groupId: string, description: string): Promise<void> {
|
|
595
|
+
const connection = this.profiles.get(profile);
|
|
596
|
+
if (!connection?.socket) {
|
|
597
|
+
throw new Error('Profile not connected');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
await connection.socket.groupUpdateDescription(groupId, description);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Update group participants (add, remove, promote, demote)
|
|
605
|
+
*/
|
|
606
|
+
async updateParticipants(
|
|
607
|
+
profile: string,
|
|
608
|
+
groupId: string,
|
|
609
|
+
participants: string[],
|
|
610
|
+
action: WhatsAppParticipantAction
|
|
611
|
+
): Promise<{ participant: string; status: string }[]> {
|
|
612
|
+
const connection = this.profiles.get(profile);
|
|
613
|
+
if (!connection?.socket) {
|
|
614
|
+
throw new Error('Profile not connected');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const jids = participants.map((p) => (p.includes('@') ? p : phoneToJid(p)));
|
|
618
|
+
const results = await connection.socket.groupParticipantsUpdate(groupId, jids, action);
|
|
619
|
+
|
|
620
|
+
return results.map((r) => ({
|
|
621
|
+
participant: r.jid ?? 'unknown',
|
|
622
|
+
status: r.status,
|
|
623
|
+
}));
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Leave a group
|
|
628
|
+
*/
|
|
629
|
+
async leaveGroup(profile: string, groupId: string): Promise<void> {
|
|
630
|
+
const connection = this.profiles.get(profile);
|
|
631
|
+
if (!connection?.socket) {
|
|
632
|
+
throw new Error('Profile not connected');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
await connection.socket.groupLeave(groupId);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Get group invite code
|
|
640
|
+
*/
|
|
641
|
+
async getGroupInviteCode(profile: string, groupId: string): Promise<string> {
|
|
642
|
+
const connection = this.profiles.get(profile);
|
|
643
|
+
if (!connection?.socket) {
|
|
644
|
+
throw new Error('Profile not connected');
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const code = await connection.socket.groupInviteCode(groupId);
|
|
648
|
+
if (!code) {
|
|
649
|
+
throw new Error('Failed to get invite code');
|
|
650
|
+
}
|
|
651
|
+
return code;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Join a group via invite code
|
|
656
|
+
*/
|
|
657
|
+
async joinGroupViaInvite(profile: string, inviteCode: string): Promise<string> {
|
|
658
|
+
const connection = this.profiles.get(profile);
|
|
659
|
+
if (!connection?.socket) {
|
|
660
|
+
throw new Error('Profile not connected');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Extract code from full URL if provided
|
|
664
|
+
let code = inviteCode;
|
|
665
|
+
if (inviteCode.includes('chat.whatsapp.com/')) {
|
|
666
|
+
code = inviteCode.split('chat.whatsapp.com/').pop() || inviteCode;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const groupId = await connection.socket.groupAcceptInvite(code);
|
|
670
|
+
if (!groupId) {
|
|
671
|
+
throw new Error('Failed to join group');
|
|
672
|
+
}
|
|
673
|
+
return groupId;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Resolve group name to JID or JID to name
|
|
678
|
+
* Returns groupId if input looks like a JID, or searches for matching group name
|
|
679
|
+
*/
|
|
680
|
+
async resolveGroup(profile: string, nameOrId: string): Promise<{ groupId: string | null; groupName: string | null }> {
|
|
681
|
+
// If it looks like a group JID, get the name
|
|
682
|
+
if (nameOrId.includes('@g.us')) {
|
|
683
|
+
try {
|
|
684
|
+
const group = await this.getGroup(profile, nameOrId);
|
|
685
|
+
return { groupId: nameOrId, groupName: group.name };
|
|
686
|
+
} catch {
|
|
687
|
+
return { groupId: nameOrId, groupName: null };
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Otherwise, search for group by name
|
|
692
|
+
const groups = await this.listGroups(profile);
|
|
693
|
+
const lowerName = nameOrId.toLowerCase();
|
|
694
|
+
|
|
695
|
+
// Exact match first
|
|
696
|
+
const exactMatch = groups.find((g) => g.name.toLowerCase() === lowerName);
|
|
697
|
+
if (exactMatch) {
|
|
698
|
+
return { groupId: exactMatch.id, groupName: exactMatch.name };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Partial match
|
|
702
|
+
const partialMatch = groups.find((g) => g.name.toLowerCase().includes(lowerName));
|
|
703
|
+
if (partialMatch) {
|
|
704
|
+
return { groupId: partialMatch.id, groupName: partialMatch.name };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return { groupId: null, groupName: null };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Get cached group name for a JID (for display purposes)
|
|
712
|
+
* This is a quick lookup that doesn't make API calls
|
|
713
|
+
*/
|
|
714
|
+
async getGroupNameCached(profile: string, groupId: string): Promise<string | null> {
|
|
715
|
+
try {
|
|
716
|
+
const groups = await this.listGroups(profile);
|
|
717
|
+
const group = groups.find((g) => g.id === groupId);
|
|
718
|
+
return group?.name ?? null;
|
|
719
|
+
} catch {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|