better-zap 25.0.2 → 25.0.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 +1 -0
- package/dist/{client-DNlBPLBd.d.cts → client-B49AT0HF.d.mts} +75 -13
- package/dist/{client-DiHHMG8t.d.mts → client-B6XKmfej.d.cts} +75 -13
- package/dist/client.d.cts +1 -1
- package/dist/client.d.mts +1 -1
- package/dist/index.cjs +572 -49
- package/dist/index.d.cts +139 -3
- package/dist/index.d.mts +139 -3
- package/dist/index.mjs +553 -46
- package/dist/s3.cjs +140 -0
- package/dist/s3.d.cts +49 -0
- package/dist/s3.d.mts +49 -0
- package/dist/s3.mjs +138 -0
- package/dist/types-B9sxujNH.d.mts +121 -0
- package/dist/types-C3niCOVI.d.cts +121 -0
- package/dist/types-CITvQMYt.cjs +216 -0
- package/dist/types-ClrLWuyK.mjs +127 -0
- package/package.json +15 -2
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_client = require("./client.cjs");
|
|
3
|
+
const require_types = require("./types-CITvQMYt.cjs");
|
|
3
4
|
//#region src/freeform-message-window.ts
|
|
4
5
|
const FREEFORM_MESSAGE_WINDOW_MS = 1440 * 60 * 1e3;
|
|
5
6
|
function toTimestamp(value) {
|
|
@@ -122,13 +123,184 @@ function formatPhone(phone) {
|
|
|
122
123
|
throw new Error(`[formatPhone] Cannot normalize phone: "${phone}" (${digits.length} digits). Expected 10–13 digit Brazilian number.`);
|
|
123
124
|
}
|
|
124
125
|
//#endregion
|
|
125
|
-
//#region src/utils/
|
|
126
|
+
//#region src/utils/record.ts
|
|
127
|
+
/** True for a plain key-value object: not null, not an array. */
|
|
128
|
+
function isRecord(value) {
|
|
129
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
130
|
+
}
|
|
131
|
+
/** The value when it is a plain object, otherwise an empty object. */
|
|
132
|
+
function asRecord(value) {
|
|
133
|
+
return isRecord(value) ? value : {};
|
|
134
|
+
}
|
|
135
|
+
/** Copies `patch` onto `base`, skipping `undefined` values so they never erase stored fields. */
|
|
136
|
+
function mergeDefined(base, patch) {
|
|
137
|
+
const merged = { ...base };
|
|
138
|
+
for (const [key, value] of Object.entries(patch ?? {})) {
|
|
139
|
+
if (value === void 0) continue;
|
|
140
|
+
merged[key] = value;
|
|
141
|
+
}
|
|
142
|
+
return merged;
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/media/download.ts
|
|
146
|
+
/** Returned by one download pass when the 5-minute URL expired before use. */
|
|
147
|
+
const URL_EXPIRED = Symbol("url_expired");
|
|
148
|
+
var TooLargeError = class extends Error {
|
|
149
|
+
constructor(size) {
|
|
150
|
+
super("too_large");
|
|
151
|
+
this.size = size;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
126
154
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
155
|
+
* Downloads a WhatsApp media file:
|
|
156
|
+
* 1. `GET /{media-id}` returns a download URL that expires after 5 minutes.
|
|
157
|
+
* 2. The URL is fetched with the same bearer token. On 404 a new URL is
|
|
158
|
+
* requested once.
|
|
159
|
+
*
|
|
160
|
+
* Never throws. Files over `maxBytes` are not read.
|
|
129
161
|
*/
|
|
130
|
-
function
|
|
131
|
-
|
|
162
|
+
async function downloadMetaMedia(input) {
|
|
163
|
+
const [result, error] = await require_types.tryCatch(() => download(input));
|
|
164
|
+
if (error) return failed$1(`Media download failed: ${error.message}`);
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
async function download(input) {
|
|
168
|
+
const token = input.options?.accessToken ?? input.accessToken;
|
|
169
|
+
const context = {
|
|
170
|
+
lookupUrl: `${input.graphUrl}/${encodeURIComponent(input.mediaId)}`,
|
|
171
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
172
|
+
maxBytes: input.options?.maxBytes ?? 26214400,
|
|
173
|
+
retries: input.options?.retries ?? 2,
|
|
174
|
+
expectedSha256: input.options?.expectedSha256,
|
|
175
|
+
signal: input.options?.signal,
|
|
176
|
+
fetch: input.fetch ?? fetch
|
|
177
|
+
};
|
|
178
|
+
const first = await downloadOnce(context);
|
|
179
|
+
if (first !== URL_EXPIRED) return first;
|
|
180
|
+
const second = await downloadOnce(context);
|
|
181
|
+
if (second !== URL_EXPIRED) return second;
|
|
182
|
+
return failed$1("Media download failed: HTTP 404");
|
|
183
|
+
}
|
|
184
|
+
async function downloadOnce(context) {
|
|
185
|
+
const { headers, maxBytes, retries, signal } = context;
|
|
186
|
+
const [lookup, lookupError] = await require_types.fetchWithRetry(() => context.fetch(context.lookupUrl, {
|
|
187
|
+
method: "GET",
|
|
188
|
+
headers,
|
|
189
|
+
signal
|
|
190
|
+
}), {
|
|
191
|
+
retries,
|
|
192
|
+
signal
|
|
193
|
+
});
|
|
194
|
+
if (lookupError) return failed$1(`Media lookup failed: ${lookupError.message}`);
|
|
195
|
+
if (!lookup.ok) return failed$1(`Media lookup failed: ${await describeError(lookup)}`);
|
|
196
|
+
const [json, jsonError] = await require_types.tryCatch(() => lookup.json());
|
|
197
|
+
if (jsonError) return failed$1(`Media lookup returned invalid JSON: ${jsonError.message}`);
|
|
198
|
+
const info = asRecord(json);
|
|
199
|
+
const fileUrl = info.url;
|
|
200
|
+
if (typeof fileUrl !== "string" || !fileUrl) return failed$1("Media lookup returned no url");
|
|
201
|
+
const reportedSize = require_types.toNumber(info.file_size);
|
|
202
|
+
if (reportedSize !== void 0 && reportedSize > maxBytes) return {
|
|
203
|
+
status: "too_large",
|
|
204
|
+
size: reportedSize,
|
|
205
|
+
maxBytes
|
|
206
|
+
};
|
|
207
|
+
const [response, downloadError] = await require_types.fetchWithRetry(() => context.fetch(fileUrl, {
|
|
208
|
+
method: "GET",
|
|
209
|
+
headers,
|
|
210
|
+
signal
|
|
211
|
+
}), {
|
|
212
|
+
retries,
|
|
213
|
+
signal
|
|
214
|
+
});
|
|
215
|
+
if (downloadError) return failed$1(`Media download failed: ${downloadError.message}`);
|
|
216
|
+
if (response.status === 404) {
|
|
217
|
+
await require_types.discardBody(response);
|
|
218
|
+
return URL_EXPIRED;
|
|
219
|
+
}
|
|
220
|
+
if (!response.ok) return failed$1(`Media download failed: ${await describeError(response)}`);
|
|
221
|
+
const [body, readError] = await readLimited(response, maxBytes);
|
|
222
|
+
if (readError instanceof TooLargeError) return {
|
|
223
|
+
status: "too_large",
|
|
224
|
+
size: readError.size ?? reportedSize,
|
|
225
|
+
maxBytes
|
|
226
|
+
};
|
|
227
|
+
if (readError) return failed$1(`Media download failed: ${readError.message}`);
|
|
228
|
+
const sha256 = await require_types.sha256Hex(body);
|
|
229
|
+
const expected = stringOrUndefined(info.sha256) ?? context.expectedSha256;
|
|
230
|
+
if (expected && !sha256Matches(sha256, expected)) return failed$1(`SHA-256 mismatch: expected ${expected}, got ${sha256}`);
|
|
231
|
+
return {
|
|
232
|
+
status: "downloaded",
|
|
233
|
+
body,
|
|
234
|
+
contentType: stringOrUndefined(info.mime_type) || response.headers.get("content-type") || "application/octet-stream",
|
|
235
|
+
size: body.byteLength,
|
|
236
|
+
sha256
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function failed$1(error) {
|
|
240
|
+
return {
|
|
241
|
+
status: "failed",
|
|
242
|
+
error
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function stringOrUndefined(value) {
|
|
246
|
+
return typeof value === "string" ? value : void 0;
|
|
247
|
+
}
|
|
248
|
+
async function describeError(response) {
|
|
249
|
+
const [data] = await require_types.tryCatch(() => response.json());
|
|
250
|
+
const message = data?.error?.message;
|
|
251
|
+
if (!message) return `HTTP ${response.status}`;
|
|
252
|
+
return `HTTP ${response.status}: ${message}`;
|
|
253
|
+
}
|
|
254
|
+
/** Reads the body, stopping with a `TooLargeError` once it passes `maxBytes`. */
|
|
255
|
+
async function readLimited(response, maxBytes) {
|
|
256
|
+
const declared = require_types.toNumber(response.headers.get("content-length"));
|
|
257
|
+
if (declared !== void 0 && declared > maxBytes) {
|
|
258
|
+
await require_types.discardBody(response);
|
|
259
|
+
return [null, new TooLargeError(declared)];
|
|
260
|
+
}
|
|
261
|
+
if (!response.body) return [/* @__PURE__ */ new ArrayBuffer(0), null];
|
|
262
|
+
const reader = response.body.getReader();
|
|
263
|
+
const chunks = [];
|
|
264
|
+
let total = 0;
|
|
265
|
+
for (;;) {
|
|
266
|
+
const [chunk, error] = await require_types.tryCatch(() => reader.read());
|
|
267
|
+
if (error) return [null, error];
|
|
268
|
+
if (chunk.done) break;
|
|
269
|
+
total += chunk.value.byteLength;
|
|
270
|
+
if (total > maxBytes) {
|
|
271
|
+
await require_types.tryCatch(() => reader.cancel());
|
|
272
|
+
return [null, new TooLargeError(declared)];
|
|
273
|
+
}
|
|
274
|
+
chunks.push(chunk.value);
|
|
275
|
+
}
|
|
276
|
+
const out = new Uint8Array(total);
|
|
277
|
+
let offset = 0;
|
|
278
|
+
for (const chunk of chunks) {
|
|
279
|
+
out.set(chunk, offset);
|
|
280
|
+
offset += chunk.byteLength;
|
|
281
|
+
}
|
|
282
|
+
return [out.buffer, null];
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Meta's docs don't say whether the hash is hex or base64, so both are
|
|
286
|
+
* accepted. Values that are neither (e.g. test fixtures) are ignored rather
|
|
287
|
+
* than failing the download.
|
|
288
|
+
*/
|
|
289
|
+
function sha256Matches(actualHex, expected) {
|
|
290
|
+
const trimmed = expected.trim();
|
|
291
|
+
if (/^[0-9a-f]{64}$/i.test(trimmed)) return trimmed.toLowerCase() === actualHex;
|
|
292
|
+
const bytes = decodeBase64(trimmed);
|
|
293
|
+
if (bytes?.length !== 32) return true;
|
|
294
|
+
return require_types.bytesToHex(bytes) === actualHex;
|
|
295
|
+
}
|
|
296
|
+
/** Decodes standard or URL-safe base64. Returns `undefined` for anything else. */
|
|
297
|
+
function decodeBase64(value) {
|
|
298
|
+
if (!/^[A-Za-z0-9+/_-]+={0,2}$/.test(value)) return void 0;
|
|
299
|
+
const standard = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
300
|
+
if (standard.includes("=") && standard.length % 4 !== 0) return void 0;
|
|
301
|
+
if (standard.replace(/=+$/, "").length % 4 === 1) return void 0;
|
|
302
|
+
const binary = atob(standard);
|
|
303
|
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
132
304
|
}
|
|
133
305
|
//#endregion
|
|
134
306
|
//#region src/services/whatsapp.service.ts
|
|
@@ -371,6 +543,24 @@ var WhatsAppService = class {
|
|
|
371
543
|
};
|
|
372
544
|
return this.send(payload);
|
|
373
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* Download a media file received in a webhook (image, audio, video,
|
|
548
|
+
* document or sticker), using the configured access token.
|
|
549
|
+
*
|
|
550
|
+
* Meta deletes webhook media after 7 days. Never throws: failures and files
|
|
551
|
+
* over `maxBytes` are returned as results. Runs in development mode too,
|
|
552
|
+
* since it sends nothing.
|
|
553
|
+
*
|
|
554
|
+
* @see https://developers.facebook.com/docs/whatsapp/cloud-api/reference/media
|
|
555
|
+
*/
|
|
556
|
+
async downloadMedia(mediaId, options) {
|
|
557
|
+
return downloadMetaMedia({
|
|
558
|
+
mediaId,
|
|
559
|
+
accessToken: this.token,
|
|
560
|
+
graphUrl: `${META_BASE_URL$1}/${META_API_VERSION$1}`,
|
|
561
|
+
options
|
|
562
|
+
});
|
|
563
|
+
}
|
|
374
564
|
/** Core send method with retry logic (2 retries, exponential backoff). */
|
|
375
565
|
async send(payload, logging, retries = 2) {
|
|
376
566
|
if (this.isDev) {
|
|
@@ -422,11 +612,11 @@ var WhatsAppService = class {
|
|
|
422
612
|
errorData = null;
|
|
423
613
|
}
|
|
424
614
|
if (response.status === 429 && attempt < retries) {
|
|
425
|
-
await delay(1e3 * (attempt + 1));
|
|
615
|
+
await require_types.delay(1e3 * (attempt + 1));
|
|
426
616
|
continue;
|
|
427
617
|
}
|
|
428
618
|
if (response.status >= 500 && attempt < retries) {
|
|
429
|
-
await delay(500 * (attempt + 1));
|
|
619
|
+
await require_types.delay(500 * (attempt + 1));
|
|
430
620
|
continue;
|
|
431
621
|
}
|
|
432
622
|
return {
|
|
@@ -442,7 +632,7 @@ var WhatsAppService = class {
|
|
|
442
632
|
};
|
|
443
633
|
} catch (error) {
|
|
444
634
|
if (attempt < retries) {
|
|
445
|
-
await delay(500 * (attempt + 1));
|
|
635
|
+
await require_types.delay(500 * (attempt + 1));
|
|
446
636
|
continue;
|
|
447
637
|
}
|
|
448
638
|
return {
|
|
@@ -744,9 +934,9 @@ const SESSION_IDENTIFIER_FIELDS = [
|
|
|
744
934
|
];
|
|
745
935
|
function parseEmbeddedSignupMessage(data) {
|
|
746
936
|
const payload = typeof data === "string" ? parseJson(data) : data;
|
|
747
|
-
if (!
|
|
937
|
+
if (!isRecord(payload)) return null;
|
|
748
938
|
if (payload.type !== "WA_EMBEDDED_SIGNUP" || !SUPPORTED_SESSION_VERSIONS.has(payload.version) || typeof payload.event !== "string" || payload.event.length === 0) return null;
|
|
749
|
-
if (payload.data !== void 0 && !
|
|
939
|
+
if (payload.data !== void 0 && !isRecord(payload.data)) return null;
|
|
750
940
|
const sessionData = payload.data;
|
|
751
941
|
if (SESSION_IDENTIFIER_FIELDS.some((field) => sessionData?.[field] !== void 0 && typeof sessionData[field] !== "string")) return null;
|
|
752
942
|
if (normalizeCoexistenceSessionEvent(payload.event) === "FINISH" && typeof sessionData?.waba_id !== "string") return null;
|
|
@@ -755,9 +945,6 @@ function parseEmbeddedSignupMessage(data) {
|
|
|
755
945
|
data: sessionData
|
|
756
946
|
};
|
|
757
947
|
}
|
|
758
|
-
function isPlainObject(value) {
|
|
759
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
760
|
-
}
|
|
761
948
|
function parseJson(value) {
|
|
762
949
|
try {
|
|
763
950
|
return JSON.parse(value);
|
|
@@ -973,6 +1160,82 @@ var InMemoryCoexistenceStore = class {
|
|
|
973
1160
|
}
|
|
974
1161
|
};
|
|
975
1162
|
//#endregion
|
|
1163
|
+
//#region src/utils/media-placeholder.ts
|
|
1164
|
+
/**
|
|
1165
|
+
* Coexistence history sync sends older media as `type: "media_placeholder"`
|
|
1166
|
+
* with no media object. For media sent within 14 days of onboarding, Meta later
|
|
1167
|
+
* sends another history webhook with the same message id and the real media.
|
|
1168
|
+
*/
|
|
1169
|
+
const MEDIA_PLACEHOLDER_TYPE = "media_placeholder";
|
|
1170
|
+
/** True when a webhook message is a history media placeholder. */
|
|
1171
|
+
function isMediaPlaceholder(message) {
|
|
1172
|
+
return message?.type === MEDIA_PLACEHOLDER_TYPE;
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* True when a stored log record still holds a media placeholder, so UIs can
|
|
1176
|
+
* show "media unavailable" instead of the text label.
|
|
1177
|
+
*
|
|
1178
|
+
* Checks the `mediaPending` flag set by the SDK, and falls back to the raw
|
|
1179
|
+
* message type for records stored before the flag existed.
|
|
1180
|
+
*/
|
|
1181
|
+
function hasPendingMedia(record) {
|
|
1182
|
+
const metadata = asRecord(record?.metadata);
|
|
1183
|
+
if (metadata.mediaPending === true) return true;
|
|
1184
|
+
return isMediaPlaceholder(asRecord(metadata.raw));
|
|
1185
|
+
}
|
|
1186
|
+
//#endregion
|
|
1187
|
+
//#region src/media/message-media.ts
|
|
1188
|
+
/**
|
|
1189
|
+
* Returns the media object of an image, audio, video, document or sticker
|
|
1190
|
+
* message. Returns `undefined` for other types and for history media
|
|
1191
|
+
* placeholders, which carry no media.
|
|
1192
|
+
*/
|
|
1193
|
+
function getMessageMediaPayload(message) {
|
|
1194
|
+
if (!message || isMediaPlaceholder(message)) return void 0;
|
|
1195
|
+
if (!isStorableMediaType(message.type)) return void 0;
|
|
1196
|
+
const type = message.type;
|
|
1197
|
+
const media = asRecord(message[type]);
|
|
1198
|
+
if (typeof media.id !== "string" || media.id.length === 0) return void 0;
|
|
1199
|
+
return {
|
|
1200
|
+
type,
|
|
1201
|
+
id: media.id,
|
|
1202
|
+
mimeType: typeof media.mime_type === "string" ? media.mime_type : void 0,
|
|
1203
|
+
sha256: typeof media.sha256 === "string" ? media.sha256 : void 0,
|
|
1204
|
+
filename: typeof media.filename === "string" ? media.filename : void 0
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Stable storage key for a message's media:
|
|
1209
|
+
* `whatsapp/{phoneNumberId}/{waMessageId}`. Each segment is URI-encoded, so
|
|
1210
|
+
* ids with `/` or `=` stay one path segment.
|
|
1211
|
+
*/
|
|
1212
|
+
function getMediaStorageKey(input) {
|
|
1213
|
+
return [
|
|
1214
|
+
"whatsapp",
|
|
1215
|
+
encodeURIComponent(input.phoneNumberId || "unknown"),
|
|
1216
|
+
encodeURIComponent(input.waMessageId)
|
|
1217
|
+
].join("/");
|
|
1218
|
+
}
|
|
1219
|
+
/** Reads `metadata.media` from a stored log record. */
|
|
1220
|
+
function getMessageMedia(record) {
|
|
1221
|
+
const media = asRecord(asRecord(record?.metadata).media);
|
|
1222
|
+
if (!MEDIA_STATUSES.includes(media.status)) return void 0;
|
|
1223
|
+
return media;
|
|
1224
|
+
}
|
|
1225
|
+
/** Returns `metadata.media` only when the file is in the `MediaStore`. */
|
|
1226
|
+
function getStoredMessageMedia(record) {
|
|
1227
|
+
const media = getMessageMedia(record);
|
|
1228
|
+
return media?.status === "stored" && typeof media.key === "string" ? media : void 0;
|
|
1229
|
+
}
|
|
1230
|
+
const MEDIA_STATUSES = [
|
|
1231
|
+
"stored",
|
|
1232
|
+
"failed",
|
|
1233
|
+
"skipped"
|
|
1234
|
+
];
|
|
1235
|
+
function isStorableMediaType(type) {
|
|
1236
|
+
return require_types.STORABLE_MEDIA_TYPES.includes(type);
|
|
1237
|
+
}
|
|
1238
|
+
//#endregion
|
|
976
1239
|
//#region src/services/message-logger.service.ts
|
|
977
1240
|
const WHATSAPP_MESSAGE_TYPES = [
|
|
978
1241
|
"queue_position",
|
|
@@ -992,12 +1255,19 @@ var MessageLoggerService = class {
|
|
|
992
1255
|
this.log = log;
|
|
993
1256
|
}
|
|
994
1257
|
async notify(event) {
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1258
|
+
const notifier = this.notifier;
|
|
1259
|
+
if (!notifier) return;
|
|
1260
|
+
const [, error] = await require_types.tryCatch(() => notifier.notify(event));
|
|
1261
|
+
if (error) this.log.error("message_logger.sync_notify_failed", serializeError(error));
|
|
1262
|
+
}
|
|
1263
|
+
async notifyNewMessage(message) {
|
|
1264
|
+
const conversation = await this.getConversationById(message.conversationId);
|
|
1265
|
+
if (!conversation) return;
|
|
1266
|
+
await this.notify({
|
|
1267
|
+
type: "NEW_MESSAGE",
|
|
1268
|
+
message,
|
|
1269
|
+
conversation
|
|
1270
|
+
});
|
|
1001
1271
|
}
|
|
1002
1272
|
async getConversationById(conversationId) {
|
|
1003
1273
|
const conversation = await this.store.getConversationById(conversationId);
|
|
@@ -1040,12 +1310,7 @@ var MessageLoggerService = class {
|
|
|
1040
1310
|
sentAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1041
1311
|
metadata: params.metadata
|
|
1042
1312
|
});
|
|
1043
|
-
|
|
1044
|
-
if (conversation) await this.notify({
|
|
1045
|
-
type: "NEW_MESSAGE",
|
|
1046
|
-
message: inserted,
|
|
1047
|
-
conversation
|
|
1048
|
-
});
|
|
1313
|
+
await this.notifyNewMessage(inserted);
|
|
1049
1314
|
return inserted.id;
|
|
1050
1315
|
}
|
|
1051
1316
|
/**
|
|
@@ -1095,21 +1360,34 @@ var MessageLoggerService = class {
|
|
|
1095
1360
|
sentAt: params.sentAt
|
|
1096
1361
|
});
|
|
1097
1362
|
if (!created) return false;
|
|
1098
|
-
|
|
1099
|
-
if (conversation) await this.notify({
|
|
1100
|
-
type: "NEW_MESSAGE",
|
|
1101
|
-
message: inserted,
|
|
1102
|
-
conversation
|
|
1103
|
-
});
|
|
1363
|
+
await this.notifyNewMessage(inserted);
|
|
1104
1364
|
return true;
|
|
1105
1365
|
}
|
|
1106
1366
|
/**
|
|
1107
1367
|
* Log an imported WhatsApp message with an explicit direction and timestamp.
|
|
1108
1368
|
* Used by coexistence history imports and app echo webhooks where the message
|
|
1109
1369
|
* did not originate from the local send API call.
|
|
1370
|
+
*
|
|
1371
|
+
* Returns true only when a new row was created. See {@link importMessage}
|
|
1372
|
+
* for the detailed result, including placeholder upgrades.
|
|
1110
1373
|
*/
|
|
1111
1374
|
async logImportedMessage(params) {
|
|
1112
|
-
|
|
1375
|
+
return await this.importMessage(params) === "created";
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Import a WhatsApp message, deduplicated by `waMessageId`.
|
|
1379
|
+
*
|
|
1380
|
+
* When a row with the same id already exists and still holds a history media
|
|
1381
|
+
* placeholder (see {@link hasPendingMedia}), and the new message has real
|
|
1382
|
+
* content, the stored content and metadata are replaced and a
|
|
1383
|
+
* `MESSAGE_UPDATE` sync event is sent. A placeholder never overwrites real
|
|
1384
|
+
* content.
|
|
1385
|
+
*
|
|
1386
|
+
* Relies on `createWhatsAppLog` returning the existing row with
|
|
1387
|
+
* `created: false` for a known `waMessageId`.
|
|
1388
|
+
*/
|
|
1389
|
+
async importMessage(params) {
|
|
1390
|
+
const { record, created } = await this.store.createWhatsAppLog({
|
|
1113
1391
|
phone: params.phone,
|
|
1114
1392
|
contactName: params.senderName,
|
|
1115
1393
|
waMessageId: params.waMessageId,
|
|
@@ -1120,12 +1398,50 @@ var MessageLoggerService = class {
|
|
|
1120
1398
|
metadata: params.metadata,
|
|
1121
1399
|
sentAt: params.sentAt
|
|
1122
1400
|
});
|
|
1123
|
-
if (!created) return
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1401
|
+
if (!created) return await this.replaceMediaPlaceholder(record, params) ? "upgraded" : "duplicate";
|
|
1402
|
+
await this.notifyNewMessage(record);
|
|
1403
|
+
return "created";
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Save the result of a media download as `metadata.media` on a stored
|
|
1407
|
+
* message and send a `MESSAGE_UPDATE` sync event.
|
|
1408
|
+
*
|
|
1409
|
+
* Only the `media` field is written; other metadata fields are kept. A
|
|
1410
|
+
* stored file is never replaced by a failed or skipped result.
|
|
1411
|
+
*
|
|
1412
|
+
* Returns the updated record, or `null` when the message isn't stored or
|
|
1413
|
+
* the update was skipped.
|
|
1414
|
+
*/
|
|
1415
|
+
async setMessageMedia(waMessageId, media) {
|
|
1416
|
+
const existing = await this.store.getMessageByWaId(waMessageId);
|
|
1417
|
+
if (!existing) return null;
|
|
1418
|
+
if (getMessageMedia(existing)?.status === "stored" && media.status !== "stored") return null;
|
|
1419
|
+
return this.updateMessage(waMessageId, existing, { metadata: {
|
|
1420
|
+
...asRecord(existing.metadata),
|
|
1421
|
+
media
|
|
1422
|
+
} });
|
|
1423
|
+
}
|
|
1424
|
+
/** Writes `updates` to a stored message and sends `MESSAGE_UPDATE`. */
|
|
1425
|
+
async updateMessage(waMessageId, existing, updates) {
|
|
1426
|
+
await this.store.updateWhatsAppLogByWaMessageId(waMessageId, updates);
|
|
1427
|
+
const message = {
|
|
1428
|
+
...existing,
|
|
1429
|
+
...updates
|
|
1430
|
+
};
|
|
1431
|
+
await this.notify({
|
|
1432
|
+
type: "MESSAGE_UPDATE",
|
|
1433
|
+
message
|
|
1434
|
+
});
|
|
1435
|
+
return message;
|
|
1436
|
+
}
|
|
1437
|
+
async replaceMediaPlaceholder(existing, params) {
|
|
1438
|
+
if (!existing || !hasPendingMedia(existing)) return false;
|
|
1439
|
+
if (hasPendingMedia({ metadata: params.metadata })) return false;
|
|
1440
|
+
const metadata = mergeDefined(asRecord(existing.metadata), params.metadata);
|
|
1441
|
+
delete metadata.mediaPending;
|
|
1442
|
+
await this.updateMessage(params.waMessageId, existing, {
|
|
1443
|
+
content: params.content,
|
|
1444
|
+
metadata
|
|
1129
1445
|
});
|
|
1130
1446
|
return true;
|
|
1131
1447
|
}
|
|
@@ -1341,6 +1657,116 @@ function decodeEntities(value) {
|
|
|
1341
1657
|
});
|
|
1342
1658
|
}
|
|
1343
1659
|
//#endregion
|
|
1660
|
+
//#region src/media/store-message-media.ts
|
|
1661
|
+
/**
|
|
1662
|
+
* Downloads a message's media, stores it, and records the result in
|
|
1663
|
+
* `metadata.media`. Does nothing for messages without media and for history
|
|
1664
|
+
* media placeholders. Never throws.
|
|
1665
|
+
*
|
|
1666
|
+
* The download and upload together must finish within `media.timeoutMs`.
|
|
1667
|
+
* The webhook processor runs this in the background through a media queue,
|
|
1668
|
+
* after `onMessage`.
|
|
1669
|
+
*/
|
|
1670
|
+
async function storeMessageMedia(input) {
|
|
1671
|
+
if (!input.media) return void 0;
|
|
1672
|
+
const payload = getMessageMediaPayload(input.message);
|
|
1673
|
+
if (!payload) return void 0;
|
|
1674
|
+
const timeoutMs = input.media.timeoutMs ?? 3e4;
|
|
1675
|
+
const timeout = require_types.timeoutSignal(timeoutMs, `Media transfer timed out after ${timeoutMs}ms`);
|
|
1676
|
+
const result = await downloadAndStore(input.media, input, payload, timeout.signal);
|
|
1677
|
+
timeout.clear();
|
|
1678
|
+
const [, updateError] = await require_types.tryCatch(() => input.logger.setMessageMedia(input.message.id, result));
|
|
1679
|
+
if (updateError) input.log.error("webhook.media_metadata_update_failed", {
|
|
1680
|
+
waMessageId: input.message.id,
|
|
1681
|
+
...serializeError(updateError)
|
|
1682
|
+
});
|
|
1683
|
+
return result;
|
|
1684
|
+
}
|
|
1685
|
+
async function downloadAndStore(media, input, payload, signal) {
|
|
1686
|
+
const { message, log } = input;
|
|
1687
|
+
const phoneNumberId = input.phoneNumberId ?? media.phoneNumberId;
|
|
1688
|
+
const logContext = {
|
|
1689
|
+
waMessageId: message.id,
|
|
1690
|
+
mediaId: payload.id,
|
|
1691
|
+
mediaType: payload.type
|
|
1692
|
+
};
|
|
1693
|
+
const [download, downloadError] = await require_types.tryCatch(() => media.download(payload.id, {
|
|
1694
|
+
maxBytes: media.maxBytes ?? 26214400,
|
|
1695
|
+
expectedSha256: payload.sha256,
|
|
1696
|
+
phoneNumberId,
|
|
1697
|
+
signal
|
|
1698
|
+
}), signal);
|
|
1699
|
+
if (downloadError) {
|
|
1700
|
+
log.error("webhook.media_download_failed", {
|
|
1701
|
+
...logContext,
|
|
1702
|
+
...serializeError(downloadError)
|
|
1703
|
+
});
|
|
1704
|
+
return failed(payload.id, downloadError.message);
|
|
1705
|
+
}
|
|
1706
|
+
if (download.status === "failed") {
|
|
1707
|
+
log.error("webhook.media_download_failed", {
|
|
1708
|
+
...logContext,
|
|
1709
|
+
error: download.error
|
|
1710
|
+
});
|
|
1711
|
+
return failed(payload.id, download.error);
|
|
1712
|
+
}
|
|
1713
|
+
if (download.status === "too_large") {
|
|
1714
|
+
log.warn("webhook.media_skipped_too_large", {
|
|
1715
|
+
...logContext,
|
|
1716
|
+
size: download.size,
|
|
1717
|
+
maxBytes: download.maxBytes
|
|
1718
|
+
});
|
|
1719
|
+
return {
|
|
1720
|
+
status: "skipped",
|
|
1721
|
+
reason: "too_large",
|
|
1722
|
+
...download.size !== void 0 ? { size: download.size } : {},
|
|
1723
|
+
maxBytes: download.maxBytes,
|
|
1724
|
+
mediaId: payload.id,
|
|
1725
|
+
skippedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
const key = getMediaStorageKey({
|
|
1729
|
+
phoneNumberId,
|
|
1730
|
+
waMessageId: message.id
|
|
1731
|
+
});
|
|
1732
|
+
const [, putError] = await require_types.tryCatch(() => media.store.put(key, download.body, {
|
|
1733
|
+
contentType: download.contentType,
|
|
1734
|
+
size: download.size,
|
|
1735
|
+
sha256: download.sha256,
|
|
1736
|
+
signal
|
|
1737
|
+
}), signal);
|
|
1738
|
+
if (putError) {
|
|
1739
|
+
log.error("webhook.media_store_failed", {
|
|
1740
|
+
...logContext,
|
|
1741
|
+
...serializeError(putError)
|
|
1742
|
+
});
|
|
1743
|
+
return failed(payload.id, putError.message);
|
|
1744
|
+
}
|
|
1745
|
+
log.info("webhook.media_stored", {
|
|
1746
|
+
...logContext,
|
|
1747
|
+
key,
|
|
1748
|
+
size: download.size
|
|
1749
|
+
});
|
|
1750
|
+
return {
|
|
1751
|
+
status: "stored",
|
|
1752
|
+
key,
|
|
1753
|
+
contentType: download.contentType,
|
|
1754
|
+
size: download.size,
|
|
1755
|
+
sha256: download.sha256,
|
|
1756
|
+
...payload.filename ? { filename: payload.filename } : {},
|
|
1757
|
+
mediaId: payload.id,
|
|
1758
|
+
storedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
function failed(mediaId, error) {
|
|
1762
|
+
return {
|
|
1763
|
+
status: "failed",
|
|
1764
|
+
error,
|
|
1765
|
+
mediaId,
|
|
1766
|
+
failedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
//#endregion
|
|
1344
1770
|
//#region src/webhook/message-content.ts
|
|
1345
1771
|
/**
|
|
1346
1772
|
* Extract human-readable content from incoming messages for audit logs.
|
|
@@ -1360,17 +1786,85 @@ function getMessageContent(message) {
|
|
|
1360
1786
|
return "[interativo]";
|
|
1361
1787
|
case "sticker": return "[figurinha]";
|
|
1362
1788
|
case "reaction": return "[reação]";
|
|
1789
|
+
case "media_placeholder": return "[mídia]";
|
|
1363
1790
|
default: return `[${message.type}]`;
|
|
1364
1791
|
}
|
|
1365
1792
|
}
|
|
1366
1793
|
//#endregion
|
|
1794
|
+
//#region src/utils/limit.ts
|
|
1795
|
+
/**
|
|
1796
|
+
* Returns a runner that runs at most `concurrency` tasks at a time. Extra
|
|
1797
|
+
* tasks wait their turn in order. Task errors come back as a {@link Result}.
|
|
1798
|
+
*/
|
|
1799
|
+
function createLimiter(concurrency) {
|
|
1800
|
+
const max = Number.isNaN(concurrency) ? 1 : Math.max(1, Math.floor(concurrency));
|
|
1801
|
+
const waiting = [];
|
|
1802
|
+
let active = 0;
|
|
1803
|
+
const acquire = () => {
|
|
1804
|
+
if (active < max) {
|
|
1805
|
+
active++;
|
|
1806
|
+
return Promise.resolve();
|
|
1807
|
+
}
|
|
1808
|
+
return new Promise((resolve) => waiting.push(resolve));
|
|
1809
|
+
};
|
|
1810
|
+
const release = () => {
|
|
1811
|
+
const next = waiting.shift();
|
|
1812
|
+
if (next) return next();
|
|
1813
|
+
active--;
|
|
1814
|
+
};
|
|
1815
|
+
return async (task) => {
|
|
1816
|
+
await acquire();
|
|
1817
|
+
const result = await require_types.tryCatch(task);
|
|
1818
|
+
release();
|
|
1819
|
+
return result;
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
//#endregion
|
|
1823
|
+
//#region src/media/media-queue.ts
|
|
1824
|
+
/** Runs media downloads in the background so they never delay webhook hooks. */
|
|
1825
|
+
function createMediaQueue(input) {
|
|
1826
|
+
const run = createLimiter(input.media.concurrency ?? 3);
|
|
1827
|
+
const queued = /* @__PURE__ */ new Map();
|
|
1828
|
+
return { add(message, phoneNumberId) {
|
|
1829
|
+
if (!getMessageMediaPayload(message)) return Promise.resolve();
|
|
1830
|
+
const existing = queued.get(message.id);
|
|
1831
|
+
if (existing) return existing;
|
|
1832
|
+
const job = run(() => storeMessageMedia({
|
|
1833
|
+
...input,
|
|
1834
|
+
message,
|
|
1835
|
+
phoneNumberId
|
|
1836
|
+
})).then(() => {
|
|
1837
|
+
queued.delete(message.id);
|
|
1838
|
+
});
|
|
1839
|
+
queued.set(message.id, job);
|
|
1840
|
+
return job;
|
|
1841
|
+
} };
|
|
1842
|
+
}
|
|
1843
|
+
//#endregion
|
|
1367
1844
|
//#region src/webhook/webhook-processor.ts
|
|
1368
1845
|
function createWebhookProcessor(config) {
|
|
1369
|
-
|
|
1846
|
+
const mediaQueue = config.media ? createMediaQueue({
|
|
1847
|
+
media: config.media,
|
|
1848
|
+
logger: config.logger,
|
|
1849
|
+
log: config.log
|
|
1850
|
+
}) : void 0;
|
|
1851
|
+
return { process: (payload) => processPayload(payload, config, mediaQueue) };
|
|
1370
1852
|
}
|
|
1371
1853
|
/** Top-level dispatcher — iterates entries in the webhook payload. */
|
|
1372
|
-
async function processPayload(payload,
|
|
1854
|
+
async function processPayload(payload, processorConfig, mediaQueue) {
|
|
1855
|
+
const mediaJobs = [];
|
|
1856
|
+
const config = {
|
|
1857
|
+
...processorConfig,
|
|
1858
|
+
queueMedia: (message, phoneNumberId) => {
|
|
1859
|
+
if (!mediaQueue) return;
|
|
1860
|
+
mediaJobs.push(mediaQueue.add(message, phoneNumberId));
|
|
1861
|
+
}
|
|
1862
|
+
};
|
|
1373
1863
|
const log = config.log;
|
|
1864
|
+
await dispatchPayload(payload, config, log);
|
|
1865
|
+
await Promise.all(mediaJobs);
|
|
1866
|
+
}
|
|
1867
|
+
async function dispatchPayload(payload, config, log) {
|
|
1374
1868
|
try {
|
|
1375
1869
|
if (payload.object !== "whatsapp_business_account") {
|
|
1376
1870
|
log.debug("webhook.ignored_payload", { object: payload.object });
|
|
@@ -1430,7 +1924,7 @@ async function processMessagesChange(change, config, log) {
|
|
|
1430
1924
|
await processUnsupportedMessagesChange(change, config, log);
|
|
1431
1925
|
return;
|
|
1432
1926
|
}
|
|
1433
|
-
if (value.messages && value.messages.length > 0) for (const message of value.messages) await processIncomingMessage(message, resolveContact(value.contacts, message), config, log);
|
|
1927
|
+
if (value.messages && value.messages.length > 0) for (const message of value.messages) await processIncomingMessage(message, resolveContact(value.contacts, message), value.metadata?.phone_number_id, config, log);
|
|
1434
1928
|
if (value.statuses && value.statuses.length > 0) for (const status of value.statuses) await processStatusUpdate(status, config, log);
|
|
1435
1929
|
if (value.errors && value.errors.length > 0) {
|
|
1436
1930
|
const errorHandler = config.hooks.onError ?? ((err) => {
|
|
@@ -1455,6 +1949,7 @@ async function processHistoryChange(change, config, log) {
|
|
|
1455
1949
|
const tally = {
|
|
1456
1950
|
importedMessages: 0,
|
|
1457
1951
|
duplicateMessages: 0,
|
|
1952
|
+
upgradedMessages: 0,
|
|
1458
1953
|
failedMessages: 0
|
|
1459
1954
|
};
|
|
1460
1955
|
const threadMessageIds = /* @__PURE__ */ new Set();
|
|
@@ -1767,6 +2262,7 @@ async function processSmbMessageEchoesChange(change, config, log) {
|
|
|
1767
2262
|
const tally = {
|
|
1768
2263
|
importedMessages: 0,
|
|
1769
2264
|
duplicateMessages: 0,
|
|
2265
|
+
upgradedMessages: 0,
|
|
1770
2266
|
failedMessages: 0
|
|
1771
2267
|
};
|
|
1772
2268
|
const echoes = rawValue.message_echoes ?? [];
|
|
@@ -1937,10 +2433,15 @@ function readHistoryProgress(chunks) {
|
|
|
1937
2433
|
return latest;
|
|
1938
2434
|
}
|
|
1939
2435
|
function countImportOutcome(tally, outcome) {
|
|
1940
|
-
if (outcome === "
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
2436
|
+
if (outcome === "skipped") return;
|
|
2437
|
+
tally[TALLY_KEY[outcome]] += 1;
|
|
2438
|
+
}
|
|
2439
|
+
const TALLY_KEY = {
|
|
2440
|
+
created: "importedMessages",
|
|
2441
|
+
duplicate: "duplicateMessages",
|
|
2442
|
+
upgraded: "upgradedMessages",
|
|
2443
|
+
failed: "failedMessages"
|
|
2444
|
+
};
|
|
1944
2445
|
async function importCoexistenceMessage(input) {
|
|
1945
2446
|
if (!input.config.database?.coexistence) return "skipped";
|
|
1946
2447
|
const { message, thread, log } = input;
|
|
@@ -1960,7 +2461,7 @@ async function importCoexistenceMessage(input) {
|
|
|
1960
2461
|
message.to_user_id,
|
|
1961
2462
|
thread?.context?.user_id
|
|
1962
2463
|
].find(isNonEmptyString);
|
|
1963
|
-
|
|
2464
|
+
const result = await input.config.logger.importMessage({
|
|
1964
2465
|
phone,
|
|
1965
2466
|
waMessageId: message.id,
|
|
1966
2467
|
direction,
|
|
@@ -1974,9 +2475,12 @@ async function importCoexistenceMessage(input) {
|
|
|
1974
2475
|
...userId ? { userId } : {},
|
|
1975
2476
|
...message.history_context?.status ? { historyStatus: message.history_context.status } : {},
|
|
1976
2477
|
...sentAt ? {} : { invalidTimestamp: true },
|
|
2478
|
+
...isMediaPlaceholder(message) ? { mediaPending: true } : {},
|
|
1977
2479
|
raw: message
|
|
1978
2480
|
}
|
|
1979
|
-
})
|
|
2481
|
+
});
|
|
2482
|
+
if (result === "created" || result === "upgraded") input.config.queueMedia(message, input.metadata?.phone_number_id);
|
|
2483
|
+
return result;
|
|
1980
2484
|
} catch (error) {
|
|
1981
2485
|
log.error("webhook.coexistence_import_failed", {
|
|
1982
2486
|
field: input.source,
|
|
@@ -2030,8 +2534,10 @@ async function runOptionalHook(run, logEvent, log) {
|
|
|
2030
2534
|
* 1. Extracts human-readable content
|
|
2031
2535
|
* 2. Atomically logs and deduplicates by waMessageId
|
|
2032
2536
|
* 3. Calls {@link WebhookProcessorHooks.onMessage}
|
|
2537
|
+
* 4. Queues its media download when a `MediaStore` is configured. The hook
|
|
2538
|
+
* runs first so the bot's reply is never held up by a slow file.
|
|
2033
2539
|
*/
|
|
2034
|
-
async function processIncomingMessage(message, contact, config, log) {
|
|
2540
|
+
async function processIncomingMessage(message, contact, phoneNumberId, config, log) {
|
|
2035
2541
|
const phone = message.from;
|
|
2036
2542
|
log.info("webhook.message_received", {
|
|
2037
2543
|
waMessageId: message.id,
|
|
@@ -2071,6 +2577,7 @@ async function processIncomingMessage(message, contact, config, log) {
|
|
|
2071
2577
|
...serializeError(error)
|
|
2072
2578
|
});
|
|
2073
2579
|
}
|
|
2580
|
+
config.queueMedia(message, phoneNumberId);
|
|
2074
2581
|
}
|
|
2075
2582
|
/**
|
|
2076
2583
|
* Processes a single delivery status update:
|
|
@@ -2146,26 +2653,39 @@ function toWebhookError(error) {
|
|
|
2146
2653
|
//#endregion
|
|
2147
2654
|
exports.BetterZapClientError = require_client.BetterZapClientError;
|
|
2148
2655
|
exports.CoexistenceService = CoexistenceService;
|
|
2656
|
+
exports.DEFAULT_MEDIA_CONCURRENCY = require_types.DEFAULT_MEDIA_CONCURRENCY;
|
|
2657
|
+
exports.DEFAULT_MEDIA_MAX_BYTES = require_types.DEFAULT_MEDIA_MAX_BYTES;
|
|
2658
|
+
exports.DEFAULT_MEDIA_TIMEOUT_MS = require_types.DEFAULT_MEDIA_TIMEOUT_MS;
|
|
2149
2659
|
exports.EMPTY_TEMPLATE_REGISTRY = EMPTY_TEMPLATE_REGISTRY;
|
|
2150
2660
|
exports.FREEFORM_MESSAGE_WINDOW_MS = FREEFORM_MESSAGE_WINDOW_MS;
|
|
2151
2661
|
exports.InMemoryCoexistenceStore = InMemoryCoexistenceStore;
|
|
2662
|
+
exports.MAX_SIGNED_URL_EXPIRES_IN = require_types.MAX_SIGNED_URL_EXPIRES_IN;
|
|
2663
|
+
exports.MEDIA_PLACEHOLDER_TYPE = MEDIA_PLACEHOLDER_TYPE;
|
|
2152
2664
|
exports.MessageLoggerService = MessageLoggerService;
|
|
2665
|
+
exports.STORABLE_MEDIA_TYPES = require_types.STORABLE_MEDIA_TYPES;
|
|
2153
2666
|
exports.WHATSAPP_MESSAGE_TYPES = WHATSAPP_MESSAGE_TYPES;
|
|
2154
2667
|
exports.WhatsAppService = WhatsAppService;
|
|
2155
2668
|
exports.apiKeyTransport = require_client.apiKeyTransport;
|
|
2669
|
+
exports.clampSignedUrlExpiry = require_types.clampSignedUrlExpiry;
|
|
2156
2670
|
exports.createCoexistenceEmbeddedSignupConfig = createCoexistenceEmbeddedSignupConfig;
|
|
2157
2671
|
exports.createFreeformMessageWindow = createFreeformMessageWindow;
|
|
2158
2672
|
exports.createLogger = createLogger;
|
|
2159
2673
|
exports.createWebhookProcessor = createWebhookProcessor;
|
|
2160
2674
|
exports.createZapClient = require_client.createZapClient;
|
|
2161
2675
|
exports.defineTemplates = defineTemplates;
|
|
2162
|
-
exports.delay = delay;
|
|
2676
|
+
exports.delay = require_types.delay;
|
|
2163
2677
|
exports.firstLinkIn = firstLinkIn;
|
|
2164
2678
|
exports.formatPhone = formatPhone;
|
|
2165
2679
|
exports.getLatestIncomingMessageAt = getLatestIncomingMessageAt;
|
|
2680
|
+
exports.getMediaStorageKey = getMediaStorageKey;
|
|
2166
2681
|
exports.getMessageContent = getMessageContent;
|
|
2682
|
+
exports.getMessageMedia = getMessageMedia;
|
|
2683
|
+
exports.getMessageMediaPayload = getMessageMediaPayload;
|
|
2684
|
+
exports.getStoredMessageMedia = getStoredMessageMedia;
|
|
2167
2685
|
exports.getTemplateNames = getTemplateNames;
|
|
2168
2686
|
exports.hasConfiguredTemplates = hasConfiguredTemplates;
|
|
2687
|
+
exports.hasPendingMedia = hasPendingMedia;
|
|
2688
|
+
exports.isMediaPlaceholder = isMediaPlaceholder;
|
|
2169
2689
|
exports.launchCoexistenceEmbeddedSignup = launchCoexistenceEmbeddedSignup;
|
|
2170
2690
|
exports.noopLogger = noopLogger;
|
|
2171
2691
|
exports.normalizeCoexistenceSessionEvent = normalizeCoexistenceSessionEvent;
|
|
@@ -2177,4 +2697,7 @@ exports.resolveConversationFreeformMessageWindow = resolveConversationFreeformMe
|
|
|
2177
2697
|
exports.serializeError = serializeError;
|
|
2178
2698
|
exports.serializeTemplateFromRegistry = serializeTemplateFromRegistry;
|
|
2179
2699
|
exports.sessionTransport = require_client.sessionTransport;
|
|
2700
|
+
exports.storeMessageMedia = storeMessageMedia;
|
|
2701
|
+
exports.toError = require_types.toError;
|
|
2180
2702
|
exports.toLegacyCoexistenceSessionEvent = toLegacyCoexistenceSessionEvent;
|
|
2703
|
+
exports.tryCatch = require_types.tryCatch;
|