@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,637 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
import { ensureConfigDir, CONFIG_DIR } from '../config/config-manager';
|
|
5
|
+
import type { ServiceName } from '../types/config';
|
|
6
|
+
import type {
|
|
7
|
+
InboundMessage,
|
|
8
|
+
OutboundMessage,
|
|
9
|
+
InboxStatus,
|
|
10
|
+
OutboxStatus,
|
|
11
|
+
MediaType,
|
|
12
|
+
} from './types';
|
|
13
|
+
|
|
14
|
+
const DATABASE_FILE = join(CONFIG_DIR, 'gateway.db');
|
|
15
|
+
|
|
16
|
+
let db: Database | null = null;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initialize the database and create tables if needed
|
|
20
|
+
*/
|
|
21
|
+
export async function initDatabase(): Promise<Database> {
|
|
22
|
+
if (db) return db;
|
|
23
|
+
|
|
24
|
+
await ensureConfigDir();
|
|
25
|
+
|
|
26
|
+
db = new Database(DATABASE_FILE);
|
|
27
|
+
|
|
28
|
+
// Enable WAL mode for better concurrent access
|
|
29
|
+
db.run('PRAGMA journal_mode = WAL');
|
|
30
|
+
|
|
31
|
+
// Create inbox table
|
|
32
|
+
db.run(`
|
|
33
|
+
CREATE TABLE IF NOT EXISTS inbox (
|
|
34
|
+
id TEXT PRIMARY KEY,
|
|
35
|
+
service TEXT NOT NULL,
|
|
36
|
+
profile TEXT NOT NULL,
|
|
37
|
+
conversation_id TEXT NOT NULL,
|
|
38
|
+
platform_id TEXT NOT NULL,
|
|
39
|
+
|
|
40
|
+
sender_id TEXT NOT NULL,
|
|
41
|
+
sender_name TEXT,
|
|
42
|
+
sender_handle TEXT,
|
|
43
|
+
|
|
44
|
+
content TEXT,
|
|
45
|
+
media_type TEXT,
|
|
46
|
+
media_path TEXT,
|
|
47
|
+
|
|
48
|
+
received_at INTEGER NOT NULL,
|
|
49
|
+
status TEXT DEFAULT 'pending',
|
|
50
|
+
done_at INTEGER,
|
|
51
|
+
|
|
52
|
+
reply_to_id TEXT,
|
|
53
|
+
metadata TEXT,
|
|
54
|
+
|
|
55
|
+
UNIQUE(service, profile, platform_id)
|
|
56
|
+
)
|
|
57
|
+
`);
|
|
58
|
+
|
|
59
|
+
// Create outbox table
|
|
60
|
+
db.run(`
|
|
61
|
+
CREATE TABLE IF NOT EXISTS outbox (
|
|
62
|
+
id TEXT PRIMARY KEY,
|
|
63
|
+
service TEXT NOT NULL,
|
|
64
|
+
profile TEXT NOT NULL,
|
|
65
|
+
conversation_id TEXT NOT NULL,
|
|
66
|
+
|
|
67
|
+
content TEXT,
|
|
68
|
+
media_path TEXT,
|
|
69
|
+
media_type TEXT,
|
|
70
|
+
|
|
71
|
+
reply_to_platform_id TEXT,
|
|
72
|
+
|
|
73
|
+
queued_at INTEGER NOT NULL,
|
|
74
|
+
status TEXT DEFAULT 'pending',
|
|
75
|
+
sent_at INTEGER,
|
|
76
|
+
error TEXT,
|
|
77
|
+
platform_id TEXT,
|
|
78
|
+
|
|
79
|
+
metadata TEXT
|
|
80
|
+
)
|
|
81
|
+
`);
|
|
82
|
+
|
|
83
|
+
// Create indexes
|
|
84
|
+
db.run('CREATE INDEX IF NOT EXISTS idx_inbox_status ON inbox(service, profile, status)');
|
|
85
|
+
db.run('CREATE INDEX IF NOT EXISTS idx_inbox_received ON inbox(received_at)');
|
|
86
|
+
db.run('CREATE INDEX IF NOT EXISTS idx_outbox_status ON outbox(service, profile, status)');
|
|
87
|
+
db.run('CREATE INDEX IF NOT EXISTS idx_outbox_queued ON outbox(queued_at)');
|
|
88
|
+
|
|
89
|
+
// WhatsApp auth state tables (for Baileys)
|
|
90
|
+
// Stores authentication credentials (keys, registration info)
|
|
91
|
+
db.run(`
|
|
92
|
+
CREATE TABLE IF NOT EXISTS whatsapp_auth_creds (
|
|
93
|
+
profile TEXT PRIMARY KEY,
|
|
94
|
+
data TEXT NOT NULL,
|
|
95
|
+
updated_at INTEGER NOT NULL
|
|
96
|
+
)
|
|
97
|
+
`);
|
|
98
|
+
|
|
99
|
+
// Stores session keys (pre-keys, sender keys, etc.)
|
|
100
|
+
db.run(`
|
|
101
|
+
CREATE TABLE IF NOT EXISTS whatsapp_auth_keys (
|
|
102
|
+
profile TEXT NOT NULL,
|
|
103
|
+
type TEXT NOT NULL,
|
|
104
|
+
key_id TEXT NOT NULL,
|
|
105
|
+
data TEXT NOT NULL,
|
|
106
|
+
PRIMARY KEY (profile, type, key_id)
|
|
107
|
+
)
|
|
108
|
+
`);
|
|
109
|
+
|
|
110
|
+
db.run('CREATE INDEX IF NOT EXISTS idx_whatsapp_keys ON whatsapp_auth_keys(profile, type)');
|
|
111
|
+
|
|
112
|
+
return db;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get the database instance (must call initDatabase first)
|
|
117
|
+
*/
|
|
118
|
+
export function getDatabase(): Database {
|
|
119
|
+
if (!db) {
|
|
120
|
+
throw new Error('Database not initialized. Call initDatabase() first.');
|
|
121
|
+
}
|
|
122
|
+
return db;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Close the database connection
|
|
127
|
+
*/
|
|
128
|
+
export function closeDatabase(): void {
|
|
129
|
+
if (db) {
|
|
130
|
+
db.close();
|
|
131
|
+
db = null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============ INBOX OPERATIONS ============
|
|
136
|
+
|
|
137
|
+
interface InboxRow {
|
|
138
|
+
id: string;
|
|
139
|
+
service: string;
|
|
140
|
+
profile: string;
|
|
141
|
+
conversation_id: string;
|
|
142
|
+
platform_id: string;
|
|
143
|
+
sender_id: string;
|
|
144
|
+
sender_name: string | null;
|
|
145
|
+
sender_handle: string | null;
|
|
146
|
+
content: string | null;
|
|
147
|
+
media_type: string | null;
|
|
148
|
+
media_path: string | null;
|
|
149
|
+
received_at: number;
|
|
150
|
+
status: string;
|
|
151
|
+
done_at: number | null;
|
|
152
|
+
reply_to_id: string | null;
|
|
153
|
+
metadata: string | null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function rowToInboundMessage(row: InboxRow): InboundMessage {
|
|
157
|
+
return {
|
|
158
|
+
id: row.id,
|
|
159
|
+
service: row.service as ServiceName,
|
|
160
|
+
profile: row.profile,
|
|
161
|
+
conversationId: row.conversation_id,
|
|
162
|
+
platformId: row.platform_id,
|
|
163
|
+
senderId: row.sender_id,
|
|
164
|
+
senderName: row.sender_name ?? undefined,
|
|
165
|
+
senderHandle: row.sender_handle ?? undefined,
|
|
166
|
+
content: row.content ?? undefined,
|
|
167
|
+
mediaType: row.media_type as MediaType | undefined,
|
|
168
|
+
mediaPath: row.media_path ?? undefined,
|
|
169
|
+
receivedAt: row.received_at,
|
|
170
|
+
status: row.status as InboxStatus,
|
|
171
|
+
doneAt: row.done_at ?? undefined,
|
|
172
|
+
replyToId: row.reply_to_id ?? undefined,
|
|
173
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Insert a new message into the inbox
|
|
179
|
+
*/
|
|
180
|
+
export function insertInboxMessage(message: Omit<InboundMessage, 'id' | 'status' | 'doneAt'>): InboundMessage {
|
|
181
|
+
const db = getDatabase();
|
|
182
|
+
const id = randomUUID();
|
|
183
|
+
|
|
184
|
+
db.run(
|
|
185
|
+
`INSERT INTO inbox (id, service, profile, conversation_id, platform_id, sender_id, sender_name, sender_handle, content, media_type, media_path, received_at, status, reply_to_id, metadata)
|
|
186
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)`,
|
|
187
|
+
[
|
|
188
|
+
id,
|
|
189
|
+
message.service,
|
|
190
|
+
message.profile,
|
|
191
|
+
message.conversationId,
|
|
192
|
+
message.platformId,
|
|
193
|
+
message.senderId,
|
|
194
|
+
message.senderName ?? null,
|
|
195
|
+
message.senderHandle ?? null,
|
|
196
|
+
message.content ?? null,
|
|
197
|
+
message.mediaType ?? null,
|
|
198
|
+
message.mediaPath ?? null,
|
|
199
|
+
message.receivedAt,
|
|
200
|
+
message.replyToId ?? null,
|
|
201
|
+
message.metadata ? JSON.stringify(message.metadata) : null,
|
|
202
|
+
]
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
...message,
|
|
207
|
+
id,
|
|
208
|
+
status: 'pending',
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Check if a message already exists (by platform ID)
|
|
214
|
+
*/
|
|
215
|
+
export function inboxMessageExists(service: ServiceName, profile: string, platformId: string): boolean {
|
|
216
|
+
const db = getDatabase();
|
|
217
|
+
const row = db.query(
|
|
218
|
+
'SELECT 1 FROM inbox WHERE service = ? AND profile = ? AND platform_id = ?'
|
|
219
|
+
).get(service, profile, platformId);
|
|
220
|
+
return !!row;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get messages from inbox
|
|
225
|
+
*/
|
|
226
|
+
export function getInboxMessages(options: {
|
|
227
|
+
service?: ServiceName;
|
|
228
|
+
profile?: string;
|
|
229
|
+
conversationId?: string;
|
|
230
|
+
status?: InboxStatus;
|
|
231
|
+
limit?: number;
|
|
232
|
+
}): InboundMessage[] {
|
|
233
|
+
const db = getDatabase();
|
|
234
|
+
const conditions: string[] = [];
|
|
235
|
+
const params: (string | number)[] = [];
|
|
236
|
+
|
|
237
|
+
if (options.service) {
|
|
238
|
+
conditions.push('service = ?');
|
|
239
|
+
params.push(options.service);
|
|
240
|
+
}
|
|
241
|
+
if (options.profile) {
|
|
242
|
+
conditions.push('profile = ?');
|
|
243
|
+
params.push(options.profile);
|
|
244
|
+
}
|
|
245
|
+
if (options.conversationId) {
|
|
246
|
+
conditions.push('conversation_id = ?');
|
|
247
|
+
params.push(options.conversationId);
|
|
248
|
+
}
|
|
249
|
+
if (options.status) {
|
|
250
|
+
conditions.push('status = ?');
|
|
251
|
+
params.push(options.status);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
255
|
+
const limit = options.limit ? `LIMIT ${options.limit}` : '';
|
|
256
|
+
|
|
257
|
+
const rows = db.query<InboxRow, (string | number)[]>(
|
|
258
|
+
`SELECT * FROM inbox ${where} ORDER BY received_at ASC ${limit}`
|
|
259
|
+
).all(...params);
|
|
260
|
+
|
|
261
|
+
return rows.map(rowToInboundMessage);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get a single inbox message by ID (supports partial ID prefix matching)
|
|
266
|
+
*/
|
|
267
|
+
export function getInboxMessage(id: string): InboundMessage | null {
|
|
268
|
+
const db = getDatabase();
|
|
269
|
+
|
|
270
|
+
// Full UUID is 36 characters, if shorter try prefix match
|
|
271
|
+
if (id.length < 36) {
|
|
272
|
+
const rows = db.query<InboxRow, [string]>(
|
|
273
|
+
'SELECT * FROM inbox WHERE id LIKE ? LIMIT 2'
|
|
274
|
+
).all(`${id}%`);
|
|
275
|
+
|
|
276
|
+
// Only return if exactly one match (avoid ambiguity)
|
|
277
|
+
if (rows.length === 1) {
|
|
278
|
+
return rowToInboundMessage(rows[0]);
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const row = db.query<InboxRow, [string]>(
|
|
284
|
+
'SELECT * FROM inbox WHERE id = ?'
|
|
285
|
+
).get(id);
|
|
286
|
+
|
|
287
|
+
return row ? rowToInboundMessage(row) : null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Mark an inbox message as done (supports partial ID prefix matching)
|
|
292
|
+
*/
|
|
293
|
+
export function ackInboxMessage(id: string): boolean {
|
|
294
|
+
const db = getDatabase();
|
|
295
|
+
|
|
296
|
+
// Full UUID is 36 characters, if shorter resolve full ID first
|
|
297
|
+
let fullId = id;
|
|
298
|
+
if (id.length < 36) {
|
|
299
|
+
const message = getInboxMessage(id);
|
|
300
|
+
if (!message) return false;
|
|
301
|
+
fullId = message.id;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const result = db.run(
|
|
305
|
+
'UPDATE inbox SET status = ?, done_at = ? WHERE id = ? AND status = ?',
|
|
306
|
+
['done', Date.now(), fullId, 'pending']
|
|
307
|
+
);
|
|
308
|
+
return result.changes > 0;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get inbox statistics
|
|
313
|
+
*/
|
|
314
|
+
export function getInboxStats(options: {
|
|
315
|
+
service?: ServiceName;
|
|
316
|
+
profile?: string;
|
|
317
|
+
}): { pending: number; done: number; total: number } {
|
|
318
|
+
const db = getDatabase();
|
|
319
|
+
const conditions: string[] = [];
|
|
320
|
+
const params: string[] = [];
|
|
321
|
+
|
|
322
|
+
if (options.service) {
|
|
323
|
+
conditions.push('service = ?');
|
|
324
|
+
params.push(options.service);
|
|
325
|
+
}
|
|
326
|
+
if (options.profile) {
|
|
327
|
+
conditions.push('profile = ?');
|
|
328
|
+
params.push(options.profile);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
332
|
+
|
|
333
|
+
const row = db.query<{ pending: number; done: number; total: number }, string[]>(
|
|
334
|
+
`SELECT
|
|
335
|
+
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
|
|
336
|
+
SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as done,
|
|
337
|
+
COUNT(*) as total
|
|
338
|
+
FROM inbox ${where}`
|
|
339
|
+
).get(...params);
|
|
340
|
+
|
|
341
|
+
return row ?? { pending: 0, done: 0, total: 0 };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Delete old processed messages (retention cleanup)
|
|
346
|
+
*/
|
|
347
|
+
export function cleanupInbox(daysOld: number): number {
|
|
348
|
+
if (daysOld <= 0) return 0;
|
|
349
|
+
const db = getDatabase();
|
|
350
|
+
const cutoff = Date.now() - (daysOld * 24 * 60 * 60 * 1000);
|
|
351
|
+
const result = db.run(
|
|
352
|
+
'DELETE FROM inbox WHERE status = ? AND done_at < ?',
|
|
353
|
+
['done', cutoff]
|
|
354
|
+
);
|
|
355
|
+
return result.changes;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ============ OUTBOX OPERATIONS ============
|
|
359
|
+
|
|
360
|
+
interface OutboxRow {
|
|
361
|
+
id: string;
|
|
362
|
+
service: string;
|
|
363
|
+
profile: string;
|
|
364
|
+
conversation_id: string;
|
|
365
|
+
content: string | null;
|
|
366
|
+
media_path: string | null;
|
|
367
|
+
media_type: string | null;
|
|
368
|
+
reply_to_platform_id: string | null;
|
|
369
|
+
queued_at: number;
|
|
370
|
+
status: string;
|
|
371
|
+
sent_at: number | null;
|
|
372
|
+
error: string | null;
|
|
373
|
+
platform_id: string | null;
|
|
374
|
+
metadata: string | null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function rowToOutboundMessage(row: OutboxRow): OutboundMessage {
|
|
378
|
+
return {
|
|
379
|
+
id: row.id,
|
|
380
|
+
service: row.service as ServiceName,
|
|
381
|
+
profile: row.profile,
|
|
382
|
+
conversationId: row.conversation_id,
|
|
383
|
+
content: row.content ?? undefined,
|
|
384
|
+
mediaPath: row.media_path ?? undefined,
|
|
385
|
+
mediaType: row.media_type as MediaType | undefined,
|
|
386
|
+
replyToPlatformId: row.reply_to_platform_id ?? undefined,
|
|
387
|
+
queuedAt: row.queued_at,
|
|
388
|
+
status: row.status as OutboxStatus,
|
|
389
|
+
sentAt: row.sent_at ?? undefined,
|
|
390
|
+
error: row.error ?? undefined,
|
|
391
|
+
platformId: row.platform_id ?? undefined,
|
|
392
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Queue a new message for sending
|
|
398
|
+
*/
|
|
399
|
+
export function queueOutboxMessage(message: Omit<OutboundMessage, 'id' | 'status' | 'queuedAt'>): OutboundMessage {
|
|
400
|
+
const db = getDatabase();
|
|
401
|
+
const id = randomUUID();
|
|
402
|
+
const queuedAt = Date.now();
|
|
403
|
+
|
|
404
|
+
db.run(
|
|
405
|
+
`INSERT INTO outbox (id, service, profile, conversation_id, content, media_path, media_type, reply_to_platform_id, queued_at, status, metadata)
|
|
406
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)`,
|
|
407
|
+
[
|
|
408
|
+
id,
|
|
409
|
+
message.service,
|
|
410
|
+
message.profile,
|
|
411
|
+
message.conversationId,
|
|
412
|
+
message.content ?? null,
|
|
413
|
+
message.mediaPath ?? null,
|
|
414
|
+
message.mediaType ?? null,
|
|
415
|
+
message.replyToPlatformId ?? null,
|
|
416
|
+
queuedAt,
|
|
417
|
+
message.metadata ? JSON.stringify(message.metadata) : null,
|
|
418
|
+
]
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
...message,
|
|
423
|
+
id,
|
|
424
|
+
status: 'pending',
|
|
425
|
+
queuedAt,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Get pending outbox messages for processing
|
|
431
|
+
*/
|
|
432
|
+
export function getPendingOutboxMessages(options?: {
|
|
433
|
+
service?: ServiceName;
|
|
434
|
+
profile?: string;
|
|
435
|
+
limit?: number;
|
|
436
|
+
}): OutboundMessage[] {
|
|
437
|
+
const db = getDatabase();
|
|
438
|
+
const conditions: string[] = ['status = ?'];
|
|
439
|
+
const params: (string | number)[] = ['pending'];
|
|
440
|
+
|
|
441
|
+
if (options?.service) {
|
|
442
|
+
conditions.push('service = ?');
|
|
443
|
+
params.push(options.service);
|
|
444
|
+
}
|
|
445
|
+
if (options?.profile) {
|
|
446
|
+
conditions.push('profile = ?');
|
|
447
|
+
params.push(options.profile);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const where = `WHERE ${conditions.join(' AND ')}`;
|
|
451
|
+
const limit = options?.limit ? `LIMIT ${options.limit}` : '';
|
|
452
|
+
|
|
453
|
+
const rows = db.query<OutboxRow, (string | number)[]>(
|
|
454
|
+
`SELECT * FROM outbox ${where} ORDER BY queued_at ASC ${limit}`
|
|
455
|
+
).all(...params);
|
|
456
|
+
|
|
457
|
+
return rows.map(rowToOutboundMessage);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Get outbox messages with filters
|
|
462
|
+
*/
|
|
463
|
+
export function getOutboxMessages(options: {
|
|
464
|
+
service?: ServiceName;
|
|
465
|
+
profile?: string;
|
|
466
|
+
status?: OutboxStatus;
|
|
467
|
+
limit?: number;
|
|
468
|
+
}): OutboundMessage[] {
|
|
469
|
+
const db = getDatabase();
|
|
470
|
+
const conditions: string[] = [];
|
|
471
|
+
const params: (string | number)[] = [];
|
|
472
|
+
|
|
473
|
+
if (options.service) {
|
|
474
|
+
conditions.push('service = ?');
|
|
475
|
+
params.push(options.service);
|
|
476
|
+
}
|
|
477
|
+
if (options.profile) {
|
|
478
|
+
conditions.push('profile = ?');
|
|
479
|
+
params.push(options.profile);
|
|
480
|
+
}
|
|
481
|
+
if (options.status) {
|
|
482
|
+
conditions.push('status = ?');
|
|
483
|
+
params.push(options.status);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
487
|
+
const limit = options.limit ? `LIMIT ${options.limit}` : '';
|
|
488
|
+
|
|
489
|
+
const rows = db.query<OutboxRow, (string | number)[]>(
|
|
490
|
+
`SELECT * FROM outbox ${where} ORDER BY queued_at DESC ${limit}`
|
|
491
|
+
).all(...params);
|
|
492
|
+
|
|
493
|
+
return rows.map(rowToOutboundMessage);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Get a single outbox message by ID (supports partial ID prefix matching)
|
|
498
|
+
*/
|
|
499
|
+
export function getOutboxMessage(id: string): OutboundMessage | null {
|
|
500
|
+
const db = getDatabase();
|
|
501
|
+
|
|
502
|
+
// Full UUID is 36 characters, if shorter try prefix match
|
|
503
|
+
if (id.length < 36) {
|
|
504
|
+
const rows = db.query<OutboxRow, [string]>(
|
|
505
|
+
'SELECT * FROM outbox WHERE id LIKE ? LIMIT 2'
|
|
506
|
+
).all(`${id}%`);
|
|
507
|
+
|
|
508
|
+
// Only return if exactly one match (avoid ambiguity)
|
|
509
|
+
if (rows.length === 1) {
|
|
510
|
+
return rowToOutboundMessage(rows[0]);
|
|
511
|
+
}
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const row = db.query<OutboxRow, [string]>(
|
|
516
|
+
'SELECT * FROM outbox WHERE id = ?'
|
|
517
|
+
).get(id);
|
|
518
|
+
|
|
519
|
+
return row ? rowToOutboundMessage(row) : null;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Update outbox message status (for processing)
|
|
524
|
+
*/
|
|
525
|
+
export function updateOutboxStatus(
|
|
526
|
+
id: string,
|
|
527
|
+
status: OutboxStatus,
|
|
528
|
+
options?: { error?: string; platformId?: string }
|
|
529
|
+
): boolean {
|
|
530
|
+
const db = getDatabase();
|
|
531
|
+
const updates: string[] = ['status = ?'];
|
|
532
|
+
const params: (string | number | null)[] = [status];
|
|
533
|
+
|
|
534
|
+
if (status === 'sent') {
|
|
535
|
+
updates.push('sent_at = ?');
|
|
536
|
+
params.push(Date.now());
|
|
537
|
+
}
|
|
538
|
+
if (options?.error !== undefined) {
|
|
539
|
+
updates.push('error = ?');
|
|
540
|
+
params.push(options.error);
|
|
541
|
+
}
|
|
542
|
+
if (options?.platformId !== undefined) {
|
|
543
|
+
updates.push('platform_id = ?');
|
|
544
|
+
params.push(options.platformId);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
params.push(id);
|
|
548
|
+
|
|
549
|
+
const result = db.run(
|
|
550
|
+
`UPDATE outbox SET ${updates.join(', ')} WHERE id = ?`,
|
|
551
|
+
params
|
|
552
|
+
);
|
|
553
|
+
return result.changes > 0;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Delete old sent messages (retention cleanup)
|
|
558
|
+
*/
|
|
559
|
+
export function cleanupOutbox(daysOld: number): number {
|
|
560
|
+
if (daysOld <= 0) return 0;
|
|
561
|
+
const db = getDatabase();
|
|
562
|
+
const cutoff = Date.now() - (daysOld * 24 * 60 * 60 * 1000);
|
|
563
|
+
const result = db.run(
|
|
564
|
+
'DELETE FROM outbox WHERE status = ? AND sent_at < ?',
|
|
565
|
+
['sent', cutoff]
|
|
566
|
+
);
|
|
567
|
+
return result.changes;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ============ WHATSAPP AUTH STATE EXPORT/IMPORT ============
|
|
571
|
+
|
|
572
|
+
export interface WhatsAppAuthExport {
|
|
573
|
+
profile: string;
|
|
574
|
+
creds: string | null; // JSON string
|
|
575
|
+
keys: { type: string; keyId: string; data: string }[];
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Export WhatsApp auth state for teleport
|
|
580
|
+
*/
|
|
581
|
+
export async function exportWhatsAppAuthState(profile: string): Promise<WhatsAppAuthExport | null> {
|
|
582
|
+
const db = getDatabase();
|
|
583
|
+
|
|
584
|
+
// Get credentials
|
|
585
|
+
const credsRow = db.query<{ data: string }, [string]>(
|
|
586
|
+
'SELECT data FROM whatsapp_auth_creds WHERE profile = ?'
|
|
587
|
+
).get(profile);
|
|
588
|
+
|
|
589
|
+
// Get all keys
|
|
590
|
+
const keysRows = db.query<{ type: string; key_id: string; data: string }, [string]>(
|
|
591
|
+
'SELECT type, key_id, data FROM whatsapp_auth_keys WHERE profile = ?'
|
|
592
|
+
).all(profile);
|
|
593
|
+
|
|
594
|
+
if (!credsRow && keysRows.length === 0) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
profile,
|
|
600
|
+
creds: credsRow?.data || null,
|
|
601
|
+
keys: keysRows.map(row => ({
|
|
602
|
+
type: row.type,
|
|
603
|
+
keyId: row.key_id,
|
|
604
|
+
data: row.data,
|
|
605
|
+
})),
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Import WhatsApp auth state from teleport
|
|
611
|
+
*/
|
|
612
|
+
export async function importWhatsAppAuthState(authExport: WhatsAppAuthExport): Promise<void> {
|
|
613
|
+
const db = getDatabase();
|
|
614
|
+
const { profile, creds, keys } = authExport;
|
|
615
|
+
|
|
616
|
+
// Clear existing auth state for this profile
|
|
617
|
+
db.run('DELETE FROM whatsapp_auth_creds WHERE profile = ?', [profile]);
|
|
618
|
+
db.run('DELETE FROM whatsapp_auth_keys WHERE profile = ?', [profile]);
|
|
619
|
+
|
|
620
|
+
// Import credentials
|
|
621
|
+
if (creds) {
|
|
622
|
+
db.run(
|
|
623
|
+
'INSERT INTO whatsapp_auth_creds (profile, data, updated_at) VALUES (?, ?, ?)',
|
|
624
|
+
[profile, creds, Date.now()]
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Import keys
|
|
629
|
+
for (const key of keys) {
|
|
630
|
+
db.run(
|
|
631
|
+
'INSERT INTO whatsapp_auth_keys (profile, type, key_id, data) VALUES (?, ?, ?, ?)',
|
|
632
|
+
[profile, key.type, key.keyId, key.data]
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export { DATABASE_FILE };
|