tide-commander 1.91.0 → 1.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{BossLogsModal-XsTxfWM8.js → BossLogsModal-BTTK3SDM.js} +1 -1
- package/dist/assets/{BossSpawnModal-DqQMPxHu.js → BossSpawnModal-BNbFVewB.js} +1 -1
- package/dist/assets/{ControlsModal-5mzDDdS5.js → ControlsModal-DnKdyYiv.js} +1 -1
- package/dist/assets/{DockerLogsModal-2eHlxyKa.js → DockerLogsModal-BRFhRQka.js} +1 -1
- package/dist/assets/{EmbeddedEditor-Bi9Ysd99.js → EmbeddedEditor-BFnSppyy.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-5u85N8Br.js → GmailOAuthSetup-9uMrVRsz.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-OxT_QwZL.js → GoogleOAuthSetup-CLAy54pj.js} +1 -1
- package/dist/assets/{IframeModal-Bn1kdP1S.js → IframeModal-Cy5KHwqs.js} +1 -1
- package/dist/assets/{IntegrationsPanel-BehHkKJu.js → IntegrationsPanel-C_a9pmU1.js} +2 -2
- package/dist/assets/{LogViewerModal-JuUpWFPL.js → LogViewerModal-D68ppO94.js} +1 -1
- package/dist/assets/{MonitoringModal-CLk3uqDa.js → MonitoringModal-CoZ0uDPq.js} +1 -1
- package/dist/assets/{PM2LogsModal-C_NpOsos.js → PM2LogsModal-Bbwdouj-.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-Cbcg2Fm8.js → RestoreArchivedAreaModal-Dqi_ofXl.js} +1 -1
- package/dist/assets/{Scene2DCanvas-4C-jHERv.js → Scene2DCanvas-CnqwS4QZ.js} +1 -1
- package/dist/assets/{SceneManager-BoRV8xt3.js → SceneManager-W7pJrfEW.js} +1 -1
- package/dist/assets/{SkillsPanel-Bwk3UEY_.js → SkillsPanel-Cw-tVu6j.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-t-g3hdbr.js → SlackMultiInstanceSetup-DddkR8Zr.js} +1 -1
- package/dist/assets/{SpawnModal-BOXkPtaJ.js → SpawnModal-DZODgZFD.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-CLHq5a9b.js → SubordinateAssignmentModal-BkhktU8i.js} +1 -1
- package/dist/assets/TriggerManagerPanel-YcbVJ2Rc.js +9 -0
- package/dist/assets/{WorkflowEditorPanel-Bevs1fpc.js → WorkflowEditorPanel-ZadWONKw.js} +1 -1
- package/dist/assets/{index-CiXA-Zp-.js → index-Bi8u2UlO.js} +1 -1
- package/dist/assets/{index-DxHwQ6CI.js → index-CF1hOKKa.js} +3 -3
- package/dist/assets/{index-CdKOXIM2.js → index-CN-XkbYd.js} +1 -1
- package/dist/assets/{index-CJuTMFz9.js → index-Ckn27_lZ.js} +1 -1
- package/dist/assets/{index-DBt10C9K.js → index-CtEyTmv-.js} +1 -1
- package/dist/assets/{index-H8kj1tuO.js → index-CyPV_nq1.js} +1 -1
- package/dist/assets/{index-Dd063aRs.js → index-DRsSsabz.js} +1 -1
- package/dist/assets/{index-B4JdUiAe.js → index-cK048gKM.js} +2 -2
- package/dist/assets/{index-vFrHpR5s.js → index-pqOlU6XT.js} +1 -1
- package/dist/assets/{main-5eyR3isL.js → main-jN7LzP4f.js} +95 -96
- package/dist/assets/{web-Cx_ySRHK.js → web-CfwxlY8x.js} +1 -1
- package/dist/assets/{web-DMjkVCWy.js → web-DZfyqisM.js} +1 -1
- package/dist/assets/{web-DGO1VHbi.js → web-DbhP1mtF.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/src/packages/server/index.js +4 -0
- package/dist/src/packages/server/integrations/slack/slack-instance.js +89 -0
- package/dist/src/packages/server/integrations/slack/slack-polling-client.js +150 -42
- package/dist/src/packages/server/integrations/slack/slack-skill.js +16 -0
- package/dist/src/packages/server/integrations/slack/slack-trigger-handler.js +19 -0
- package/dist/src/packages/server/integrations/whatsapp/whatsapp-skill.js +12 -0
- package/dist/src/packages/server/integrations/whatsapp/whatsapp-trigger-handler.js +226 -16
- package/dist/src/packages/server/routes/files.js +4 -2
- package/dist/src/packages/server/services/attachment-downloader.js +317 -0
- package/dist/src/packages/server/services/attachment-janitor.js +110 -0
- package/package.json +1 -1
- package/dist/assets/TriggerManagerPanel-DuWagsLi.js +0 -3
|
@@ -15,6 +15,7 @@ import { ContactNameCache } from './contact-name-cache.js';
|
|
|
15
15
|
import { GroupNameCache } from './group-name-cache.js';
|
|
16
16
|
import { WhatsAppClient } from './whatsapp-client.js';
|
|
17
17
|
import { createLogger } from '../../utils/logger.js';
|
|
18
|
+
import { downloadAttachment, formatAttachmentLine, MAX_ATTACHMENT_BYTES } from '../../services/attachment-downloader.js';
|
|
18
19
|
import * as crypto from 'crypto';
|
|
19
20
|
const log = createLogger('WhatsAppTrigger');
|
|
20
21
|
// Module-level subscriber set fed by the in-process bridge whenever a normalized
|
|
@@ -164,7 +165,26 @@ export function createWhatsAppTriggerHandler(ctx) {
|
|
|
164
165
|
payload.isGroup &&
|
|
165
166
|
!payload.groupName &&
|
|
166
167
|
!!payload.chatId;
|
|
167
|
-
|
|
168
|
+
// Media → download bytes via the upstream proxy (NOT the raw Baileys
|
|
169
|
+
// `mediaUrl`, which points at an encrypted Meta CDN). The upstream
|
|
170
|
+
// exposes `/api/sessions/<sid>/messages/<mid>/media?download=true`.
|
|
171
|
+
//
|
|
172
|
+
// We download for BOTH directions:
|
|
173
|
+
// - inbound (`fromMe=false`): a contact sent media into our number.
|
|
174
|
+
// - outbound (`fromMe=true`): the WhatsApp account itself originated
|
|
175
|
+
// the media — this is the real-world case where David sends an
|
|
176
|
+
// attachment from his own WA so Bolba can process it. The fact that
|
|
177
|
+
// Baileys reports `fromMe=true` on messages we ourselves typed in
|
|
178
|
+
// the WA app is exactly why filtering on direction was wrong.
|
|
179
|
+
//
|
|
180
|
+
// The only case where this is wasteful is Tide Commander itself sending
|
|
181
|
+
// media via `/api/whatsapp/send-media-url` (upstream echoes back the
|
|
182
|
+
// `message_create` with `fromMe=true` + mediaType). That's a localhost
|
|
183
|
+
// self-fetch that costs almost nothing, so we don't bother
|
|
184
|
+
// distinguishing it from the user-typing case.
|
|
185
|
+
const needsMediaDownload = !!payload.mediaType &&
|
|
186
|
+
!!payload.messageId;
|
|
187
|
+
if (!needsContactEnrich && !needsGroupEnrich && !needsMediaDownload) {
|
|
168
188
|
ctx.broadcast({ type: 'whatsapp_message', payload });
|
|
169
189
|
notifyTriggerSubscribers(payload);
|
|
170
190
|
return;
|
|
@@ -189,7 +209,65 @@ export function createWhatsAppTriggerHandler(ctx) {
|
|
|
189
209
|
ctx.log.warn(`WhatsApp group lookup failed: ${err}`);
|
|
190
210
|
})
|
|
191
211
|
: Promise.resolve();
|
|
192
|
-
|
|
212
|
+
const mediaPromise = needsMediaDownload
|
|
213
|
+
? (async () => {
|
|
214
|
+
const apiKey = ctx.secrets.get(WHATSAPP_API_KEY_SECRET);
|
|
215
|
+
if (!apiKey) {
|
|
216
|
+
payload.skippedAttachment = {
|
|
217
|
+
reason: 'fetch-failed',
|
|
218
|
+
filename: payload.mediaFilename,
|
|
219
|
+
size: payload.mediaSize,
|
|
220
|
+
detail: 'whatsapp.apiKey secret missing',
|
|
221
|
+
};
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const base = config.baseUrl.replace(/\/+$/, '');
|
|
225
|
+
const downloadUrl = `${base}/api/sessions/${encodeURIComponent(payload.sessionId)}/messages/${encodeURIComponent(payload.messageId)}/media?download=true`;
|
|
226
|
+
if (typeof payload.mediaSize === 'number' && payload.mediaSize > MAX_ATTACHMENT_BYTES) {
|
|
227
|
+
payload.skippedAttachment = {
|
|
228
|
+
reason: 'too-large',
|
|
229
|
+
filename: payload.mediaFilename,
|
|
230
|
+
size: payload.mediaSize,
|
|
231
|
+
sourceUrl: downloadUrl,
|
|
232
|
+
};
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const result = await downloadAttachment({
|
|
237
|
+
source: 'whatsapp',
|
|
238
|
+
messageId: payload.messageId,
|
|
239
|
+
url: downloadUrl,
|
|
240
|
+
headers: { 'X-API-Key': apiKey },
|
|
241
|
+
suggestedFilename: payload.mediaFilename,
|
|
242
|
+
mimetype: payload.mediaMimetype,
|
|
243
|
+
sizeHintBytes: payload.mediaSize,
|
|
244
|
+
});
|
|
245
|
+
if (result) {
|
|
246
|
+
payload.mediaPath = result.path;
|
|
247
|
+
payload.attachment = result;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
payload.skippedAttachment = {
|
|
251
|
+
reason: 'fetch-failed',
|
|
252
|
+
filename: payload.mediaFilename,
|
|
253
|
+
size: payload.mediaSize,
|
|
254
|
+
sourceUrl: downloadUrl,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
ctx.log.warn(`WhatsApp media download threw: ${err}`);
|
|
260
|
+
payload.skippedAttachment = {
|
|
261
|
+
reason: 'fetch-failed',
|
|
262
|
+
filename: payload.mediaFilename,
|
|
263
|
+
size: payload.mediaSize,
|
|
264
|
+
sourceUrl: downloadUrl,
|
|
265
|
+
detail: String(err),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
})()
|
|
269
|
+
: Promise.resolve();
|
|
270
|
+
void Promise.all([contactPromise, groupPromise, mediaPromise]).then(() => {
|
|
193
271
|
ctx.broadcast({ type: 'whatsapp_message', payload });
|
|
194
272
|
notifyTriggerSubscribers(payload);
|
|
195
273
|
});
|
|
@@ -272,9 +350,12 @@ function normalizeBaileysMessage(sessionId, eventName, data, ts, baseUrl) {
|
|
|
272
350
|
? (pickString(d.groupName) ?? pickString(d.subject) ?? pickString(d.chatName))
|
|
273
351
|
: undefined;
|
|
274
352
|
// Body / media extraction
|
|
275
|
-
const { body, mediaType, mediaUrl: rawMediaUrl } = extractContent(d, message);
|
|
353
|
+
const { body, mediaType, mediaUrl: rawMediaUrl, mediaMimetype, mediaSize, mediaFilename } = extractContent(d, message);
|
|
276
354
|
// Resolve relative mediaUrl against baseUrl
|
|
277
355
|
const mediaUrl = rawMediaUrl ? resolveMediaUrl(rawMediaUrl, baseUrl) : undefined;
|
|
356
|
+
// Stable Baileys message id — needed to ask the upstream for the decrypted bytes.
|
|
357
|
+
const messageId = pickString(d.id) ??
|
|
358
|
+
(key ? pickString(key.id) : undefined);
|
|
278
359
|
// Timestamp resolution. Baileys messageTimestamp is in seconds; envelope `ts`
|
|
279
360
|
// might already be ms. Heuristic: anything < 10^12 is treated as seconds.
|
|
280
361
|
const rawTs = pickNumber(d.timestamp) ??
|
|
@@ -294,6 +375,10 @@ function normalizeBaileysMessage(sessionId, eventName, data, ts, baseUrl) {
|
|
|
294
375
|
mediaUrl,
|
|
295
376
|
direction,
|
|
296
377
|
chatId: remoteJid ?? from,
|
|
378
|
+
messageId,
|
|
379
|
+
mediaMimetype,
|
|
380
|
+
mediaSize,
|
|
381
|
+
mediaFilename,
|
|
297
382
|
};
|
|
298
383
|
}
|
|
299
384
|
function extractContent(envelope, message) {
|
|
@@ -302,16 +387,61 @@ function extractContent(envelope, message) {
|
|
|
302
387
|
const directMediaType = normalizeMediaType(pickString(envelope.mediaType) ?? pickString(envelope.type));
|
|
303
388
|
const directMediaUrl = pickString(envelope.mediaUrl) ??
|
|
304
389
|
pickString(envelope.media?.url);
|
|
305
|
-
|
|
390
|
+
// 1a. The Baileys upstream we ship today emits a *flat* event shape where
|
|
391
|
+
// the media signal lives at `data.type` (e.g. "imageMessage") with no
|
|
392
|
+
// nested `message.imageMessage` proto, no `mediaUrl`, and `media: null`.
|
|
393
|
+
// Detect that case and surface mediaType so the downloader's
|
|
394
|
+
// `${baseUrl}/api/sessions/<sid>/messages/<mid>/media?download=true` URL
|
|
395
|
+
// gets constructed from sessionId+messageId. mimetype/filename/size are
|
|
396
|
+
// intentionally left undefined here — the downloader fills them from the
|
|
397
|
+
// proxy's response headers (Content-Type, Content-Disposition).
|
|
398
|
+
//
|
|
399
|
+
// Trigger conditions, in priority order:
|
|
400
|
+
// 1. `data.type` matches `*Message` (image/video/audio/document/sticker
|
|
401
|
+
// and all known variants like pttMessage, documentWithCaptionMessage,
|
|
402
|
+
// videoNoteMessage, etc.)
|
|
403
|
+
// 2. `data.hasMedia === true` regardless of how `data.type` is spelled.
|
|
404
|
+
// We default unknown variants to `document` (the catch-all "it's a file"
|
|
405
|
+
// category) so the download proceeds; the UI chip picks an icon from the
|
|
406
|
+
// mimetype the downloader fills in.
|
|
407
|
+
const flatType = pickString(envelope.type);
|
|
408
|
+
const flatHasMedia = pickBoolean(envelope.hasMedia) === true;
|
|
409
|
+
const flatTypeIsMessage = !!flatType && /Message$/i.test(flatType);
|
|
410
|
+
if (flatTypeIsMessage || (flatHasMedia && !directMediaType && !directMediaUrl)) {
|
|
411
|
+
return {
|
|
412
|
+
body: directBody,
|
|
413
|
+
mediaType: classifyFlatType(flatType),
|
|
414
|
+
mediaUrl: undefined,
|
|
415
|
+
mediaMimetype: undefined,
|
|
416
|
+
mediaFilename: undefined,
|
|
417
|
+
mediaSize: undefined,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
// Short-circuit ONLY when the upstream pre-normalized actual media metadata
|
|
421
|
+
// at the top level. Earlier this branch also fired on `directBody` alone,
|
|
422
|
+
// which meant a Baileys envelope carrying `body='caption text'` + a real
|
|
423
|
+
// `message.imageMessage` proto was returned as text-only — the media
|
|
424
|
+
// extraction below was skipped and downstream `payload.mediaType` came back
|
|
425
|
+
// undefined. Fall through when only the body is set so the Baileys proto
|
|
426
|
+
// gets a chance to provide the media info.
|
|
427
|
+
if (directMediaType || directMediaUrl) {
|
|
428
|
+
const mediaObj = envelope.media;
|
|
306
429
|
return {
|
|
307
430
|
body: directBody,
|
|
308
431
|
mediaType: directMediaType,
|
|
309
432
|
mediaUrl: directMediaUrl,
|
|
433
|
+
mediaMimetype: pickString(envelope.mediaMimetype) ?? pickString(envelope.mimetype) ?? pickString(mediaObj?.mimetype),
|
|
434
|
+
mediaSize: pickNumber(envelope.mediaSize) ?? pickNumber(envelope.fileLength) ?? pickNumber(mediaObj?.size),
|
|
435
|
+
mediaFilename: pickString(envelope.mediaFilename) ?? pickString(envelope.fileName) ?? pickString(mediaObj?.fileName),
|
|
310
436
|
};
|
|
311
437
|
}
|
|
312
438
|
// 2. Baileys `message` proto shape.
|
|
313
|
-
if (!message)
|
|
314
|
-
|
|
439
|
+
if (!message) {
|
|
440
|
+
// No proto to inspect but we may still have a plain text body at the top
|
|
441
|
+
// level. Return it so the trigger gets *something*; mediaType remains
|
|
442
|
+
// unset which is correct.
|
|
443
|
+
return directBody !== undefined ? { body: directBody } : {};
|
|
444
|
+
}
|
|
315
445
|
const conversation = pickString(message.conversation);
|
|
316
446
|
if (conversation)
|
|
317
447
|
return { body: conversation };
|
|
@@ -324,34 +454,53 @@ function extractContent(envelope, message) {
|
|
|
324
454
|
const image = message.imageMessage;
|
|
325
455
|
if (image) {
|
|
326
456
|
return {
|
|
327
|
-
body: pickString(image.caption) ?? '',
|
|
457
|
+
body: pickString(image.caption) ?? directBody ?? '',
|
|
328
458
|
mediaType: 'image',
|
|
329
459
|
mediaUrl: pickString(image.url),
|
|
460
|
+
mediaMimetype: pickString(image.mimetype),
|
|
461
|
+
mediaSize: pickNumber(image.fileLength),
|
|
330
462
|
};
|
|
331
463
|
}
|
|
332
464
|
const video = message.videoMessage;
|
|
333
465
|
if (video) {
|
|
334
466
|
return {
|
|
335
|
-
body: pickString(video.caption) ?? '',
|
|
467
|
+
body: pickString(video.caption) ?? directBody ?? '',
|
|
336
468
|
mediaType: 'video',
|
|
337
469
|
mediaUrl: pickString(video.url),
|
|
470
|
+
mediaMimetype: pickString(video.mimetype),
|
|
471
|
+
mediaSize: pickNumber(video.fileLength),
|
|
338
472
|
};
|
|
339
473
|
}
|
|
340
474
|
const audio = message.audioMessage;
|
|
341
475
|
if (audio) {
|
|
342
|
-
return {
|
|
476
|
+
return {
|
|
477
|
+
body: directBody ?? '',
|
|
478
|
+
mediaType: 'audio',
|
|
479
|
+
mediaUrl: pickString(audio.url),
|
|
480
|
+
mediaMimetype: pickString(audio.mimetype),
|
|
481
|
+
mediaSize: pickNumber(audio.fileLength),
|
|
482
|
+
};
|
|
343
483
|
}
|
|
344
484
|
const doc = message.documentMessage;
|
|
345
485
|
if (doc) {
|
|
346
486
|
return {
|
|
347
|
-
body: pickString(doc.fileName) ?? pickString(doc.caption) ?? '',
|
|
487
|
+
body: pickString(doc.fileName) ?? pickString(doc.caption) ?? directBody ?? '',
|
|
348
488
|
mediaType: 'document',
|
|
349
489
|
mediaUrl: pickString(doc.url),
|
|
490
|
+
mediaMimetype: pickString(doc.mimetype),
|
|
491
|
+
mediaSize: pickNumber(doc.fileLength),
|
|
492
|
+
mediaFilename: pickString(doc.fileName) ?? pickString(doc.title),
|
|
350
493
|
};
|
|
351
494
|
}
|
|
352
495
|
const sticker = message.stickerMessage;
|
|
353
496
|
if (sticker) {
|
|
354
|
-
return {
|
|
497
|
+
return {
|
|
498
|
+
body: directBody ?? '',
|
|
499
|
+
mediaType: 'sticker',
|
|
500
|
+
mediaUrl: pickString(sticker.url),
|
|
501
|
+
mediaMimetype: pickString(sticker.mimetype),
|
|
502
|
+
mediaSize: pickNumber(sticker.fileLength),
|
|
503
|
+
};
|
|
355
504
|
}
|
|
356
505
|
// Some Baileys variants nest the actual message under ephemeralMessage / viewOnceMessage.
|
|
357
506
|
const ephemeral = message.ephemeralMessage?.message;
|
|
@@ -360,7 +509,9 @@ function extractContent(envelope, message) {
|
|
|
360
509
|
const viewOnce = message.viewOnceMessage?.message;
|
|
361
510
|
if (viewOnce)
|
|
362
511
|
return extractContent(envelope, viewOnce);
|
|
363
|
-
|
|
512
|
+
// Unknown proto shape (reaction, protocol, etc.) — preserve the top-level
|
|
513
|
+
// body so the trigger still receives the text.
|
|
514
|
+
return directBody !== undefined ? { body: directBody } : {};
|
|
364
515
|
}
|
|
365
516
|
function resolveMediaUrl(url, baseUrl) {
|
|
366
517
|
if (/^[a-z][a-z0-9+\-.]*:\/\//i.test(url))
|
|
@@ -382,6 +533,34 @@ function normalizeMediaType(value) {
|
|
|
382
533
|
return v;
|
|
383
534
|
return undefined;
|
|
384
535
|
}
|
|
536
|
+
/**
|
|
537
|
+
* Map an upstream `data.type` (Baileys-style, suffixed `Message`) to one of
|
|
538
|
+
* the supported MediaType buckets. Known variants:
|
|
539
|
+
* - imageMessage → image
|
|
540
|
+
* - videoMessage, videoNoteMessage → video
|
|
541
|
+
* - audioMessage, pttMessage → audio
|
|
542
|
+
* - documentMessage, documentWithCaptionMessage → document
|
|
543
|
+
* - stickerMessage → sticker
|
|
544
|
+
* - any other `*Message` or unknown signal → document (catch-all "it's a file")
|
|
545
|
+
* Returns `'document'` as a safe default rather than `undefined` so the
|
|
546
|
+
* download predicate (`!!payload.mediaType && !!payload.messageId`) stays
|
|
547
|
+
* true and the proxy is given a chance to deliver the bytes.
|
|
548
|
+
*/
|
|
549
|
+
function classifyFlatType(type) {
|
|
550
|
+
const t = (type ?? '').toLowerCase();
|
|
551
|
+
if (t.startsWith('image'))
|
|
552
|
+
return 'image';
|
|
553
|
+
if (t.startsWith('video'))
|
|
554
|
+
return 'video';
|
|
555
|
+
if (t.startsWith('audio') || t.startsWith('ptt') || t.startsWith('voice'))
|
|
556
|
+
return 'audio';
|
|
557
|
+
if (t.startsWith('sticker'))
|
|
558
|
+
return 'sticker';
|
|
559
|
+
// documentMessage, documentWithCaptionMessage, and ANY other `*Message`
|
|
560
|
+
// variant we don't explicitly know about. Better to attempt the download
|
|
561
|
+
// and let the proxy decide than to silently skip an unknown media kind.
|
|
562
|
+
return 'document';
|
|
563
|
+
}
|
|
385
564
|
function pickString(value) {
|
|
386
565
|
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
387
566
|
}
|
|
@@ -573,6 +752,18 @@ export const whatsappTriggerHandler = {
|
|
|
573
752
|
const groupName = msg.isGroup
|
|
574
753
|
? (msg.groupName?.trim() || humanizeGroupJid(msg.chatId))
|
|
575
754
|
: '';
|
|
755
|
+
// Build the attachments block (single line — WA messages carry at most one
|
|
756
|
+
// media payload). Use the downloaded local path when present; otherwise
|
|
757
|
+
// emit a `[attachment-skipped: …]` marker if we tried and failed.
|
|
758
|
+
let attachmentsBlock = '';
|
|
759
|
+
if (msg.attachment) {
|
|
760
|
+
attachmentsBlock = formatAttachmentLine(msg.attachment);
|
|
761
|
+
}
|
|
762
|
+
else if (msg.skippedAttachment) {
|
|
763
|
+
const sk = msg.skippedAttachment;
|
|
764
|
+
const sizeMb = typeof sk.size === 'number' ? `${Math.round(sk.size / (1024 * 1024))}MB` : 'unknown';
|
|
765
|
+
attachmentsBlock = `[attachment-skipped: ${sk.reason} name=${sk.filename ?? 'unknown'} size=${sizeMb}${sk.detail ? ` detail=${sk.detail}` : ''}]`;
|
|
766
|
+
}
|
|
576
767
|
return {
|
|
577
768
|
'whatsapp.from': msg.from,
|
|
578
769
|
'whatsapp.fromName': fromName,
|
|
@@ -584,6 +775,13 @@ export const whatsappTriggerHandler = {
|
|
|
584
775
|
'whatsapp.direction': msg.direction,
|
|
585
776
|
'whatsapp.mediaType': msg.mediaType ?? '',
|
|
586
777
|
'whatsapp.mediaUrl': msg.mediaUrl ?? '',
|
|
778
|
+
// New: local path + metadata for inbound media, plus a ready-to-paste
|
|
779
|
+
// attachments block. Additive — old templates ignore these.
|
|
780
|
+
'whatsapp.attachmentPath': msg.attachment?.path ?? '',
|
|
781
|
+
'whatsapp.attachmentMime': msg.attachment?.mimetype ?? msg.mediaMimetype ?? '',
|
|
782
|
+
'whatsapp.attachmentName': msg.attachment?.filename ?? msg.mediaFilename ?? '',
|
|
783
|
+
'whatsapp.attachmentSize': msg.attachment ? String(msg.attachment.size) : (msg.mediaSize !== undefined ? String(msg.mediaSize) : ''),
|
|
784
|
+
'whatsapp.attachmentsBlock': attachmentsBlock,
|
|
587
785
|
'whatsapp.timestamp': new Date(msg.timestamp).toISOString(),
|
|
588
786
|
};
|
|
589
787
|
},
|
|
@@ -592,9 +790,21 @@ export const whatsappTriggerHandler = {
|
|
|
592
790
|
const sender = msg.fromName ? `${msg.fromName} (${msg.from})` : msg.from;
|
|
593
791
|
const channel = msg.isGroup ? `group "${msg.groupName ?? msg.from}"` : 'DM';
|
|
594
792
|
const verb = msg.direction === 'outbound' ? 'sent' : 'received';
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
793
|
+
// If we downloaded the media, surface the local path as the agent-facing
|
|
794
|
+
// attachment marker (it supersedes the raw mediaUrl). If we tried but
|
|
795
|
+
// failed, surface that explicitly so the agent doesn't silently miss it.
|
|
796
|
+
let mediaSuffix = '';
|
|
797
|
+
if (msg.attachment) {
|
|
798
|
+
mediaSuffix = `\n\n${formatAttachmentLine(msg.attachment)}`;
|
|
799
|
+
}
|
|
800
|
+
else if (msg.skippedAttachment) {
|
|
801
|
+
const sk = msg.skippedAttachment;
|
|
802
|
+
const sizeMb = typeof sk.size === 'number' ? `${Math.round(sk.size / (1024 * 1024))}MB` : 'unknown';
|
|
803
|
+
mediaSuffix = `\n\n[attachment-skipped: ${sk.reason} name=${sk.filename ?? 'unknown'} size=${sizeMb}${sk.detail ? ` detail=${sk.detail}` : ''}]`;
|
|
804
|
+
}
|
|
805
|
+
else if (msg.mediaType) {
|
|
806
|
+
mediaSuffix = `\n[${msg.mediaType} attachment${msg.mediaUrl ? ` ${msg.mediaUrl}` : ''}]`;
|
|
807
|
+
}
|
|
808
|
+
return `WhatsApp message ${verb} in ${channel}\nFrom: ${sender}\nSession: ${msg.sessionId}\nTime: ${new Date(msg.timestamp).toISOString()}\n\n${msg.body}${mediaSuffix}`;
|
|
599
809
|
},
|
|
600
810
|
};
|
|
@@ -10,8 +10,10 @@ import * as os from 'os';
|
|
|
10
10
|
import { logger } from '../utils/logger.js';
|
|
11
11
|
import { loadAreas } from '../data/index.js';
|
|
12
12
|
const log = logger.files;
|
|
13
|
-
// Get or create temp directory for tide-commander uploads
|
|
14
|
-
|
|
13
|
+
// Get or create temp directory for tide-commander uploads. Exported so other
|
|
14
|
+
// modules (attachment-downloader, attachment-janitor) put their files under the
|
|
15
|
+
// same root that Express already serves statically at `/uploads/`.
|
|
16
|
+
export const TEMP_DIR = path.join(os.tmpdir(), 'tide-commander-uploads');
|
|
15
17
|
if (!fs.existsSync(TEMP_DIR)) {
|
|
16
18
|
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
17
19
|
}
|