@ganju/utils 0.0.20 → 0.0.22
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/constants.d.ts +16 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +43 -0
- package/dist/data.d.ts +7 -5
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +27 -3
- package/dist/dataApi.d.ts +82 -6
- package/dist/dataApi.d.ts.map +1 -1
- package/dist/dataApi.js +47 -4
- package/dist/dataAttachment.d.ts +8 -0
- package/dist/dataAttachment.d.ts.map +1 -0
- package/dist/dataAttachment.js +19 -0
- package/dist/dataCompiler.d.ts +4 -0
- package/dist/dataCompiler.d.ts.map +1 -1
- package/dist/dataCompiler.js +27 -2
- package/dist/dataDdl.d.ts.map +1 -1
- package/dist/dataDdl.js +5 -0
- package/dist/dataEngine.d.ts +25 -3
- package/dist/dataEngine.d.ts.map +1 -1
- package/dist/dataEngine.js +508 -14
- package/dist/dataImport.d.ts +3 -0
- package/dist/dataImport.d.ts.map +1 -1
- package/dist/dataImport.js +12 -2
- package/dist/dataIndexAdvice.d.ts +36 -0
- package/dist/dataIndexAdvice.d.ts.map +1 -0
- package/dist/dataIndexAdvice.js +89 -0
- package/dist/dataRpc.d.ts +20 -1
- package/dist/dataRpc.d.ts.map +1 -1
- package/dist/dataSheet.d.ts +34 -0
- package/dist/dataSheet.d.ts.map +1 -0
- package/dist/dataSheet.js +100 -0
- package/dist/dataTools.js +3 -3
- package/dist/dataTriggers.d.ts +66 -4
- package/dist/dataTriggers.d.ts.map +1 -1
- package/dist/dataTriggers.js +446 -20
- package/dist/dataTypes.d.ts +69 -0
- package/dist/dataTypes.d.ts.map +1 -1
- package/dist/dataTypes.js +21 -1
- package/dist/index.d.ts +91 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/localizeDataError.d.ts.map +1 -1
- package/dist/localizeDataError.js +109 -3
- package/dist/schema.d.ts +67 -6
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +57 -2
- package/dist/untrustedContent.d.ts +7 -0
- package/dist/untrustedContent.d.ts.map +1 -1
- package/dist/untrustedContent.js +13 -1
- package/dist/whatsappWebhook.d.ts +28 -0
- package/dist/whatsappWebhook.d.ts.map +1 -0
- package/dist/whatsappWebhook.js +30 -0
- package/package.json +1 -1
- package/src/constants.ts +46 -0
- package/src/data.ts +33 -3
- package/src/dataApi.ts +54 -4
- package/src/dataAttachment.ts +23 -0
- package/src/dataCompiler.ts +41 -1
- package/src/dataDdl.ts +7 -0
- package/src/dataEngine.ts +680 -15
- package/src/dataImport.ts +16 -1
- package/src/dataIndexAdvice.ts +127 -0
- package/src/dataRpc.ts +27 -2
- package/src/dataSheet.ts +147 -0
- package/src/dataTools.ts +3 -3
- package/src/dataTriggers.ts +599 -20
- package/src/dataTypes.ts +108 -0
- package/src/index.ts +15 -0
- package/src/localizeDataError.ts +183 -4
- package/src/schema.ts +60 -2
- package/src/untrustedContent.ts +15 -0
- package/src/whatsappWebhook.ts +55 -0
package/src/dataTypes.ts
CHANGED
|
@@ -65,9 +65,43 @@ export interface DataTableInput {
|
|
|
65
65
|
access?: DataAccess;
|
|
66
66
|
customerScoped?: boolean;
|
|
67
67
|
history?: boolean;
|
|
68
|
+
source?: DataTableSource | null;
|
|
68
69
|
columns: DataColumnInput[];
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
// A table kept in step with a spreadsheet or CSV in the project's knowledge:
|
|
73
|
+
// `key` is the unique column that names a row in both, and the sheet wins.
|
|
74
|
+
// `title` is the file's name when the source was saved, for messages.
|
|
75
|
+
export interface DataTableSource {
|
|
76
|
+
uri: string;
|
|
77
|
+
sheet?: string | null;
|
|
78
|
+
key: string;
|
|
79
|
+
title?: string | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// How the last sync went. A refused one changed nothing.
|
|
83
|
+
export interface DataTableSync {
|
|
84
|
+
at: string;
|
|
85
|
+
status: 'ok' | 'failed';
|
|
86
|
+
inserted: number;
|
|
87
|
+
updated: number;
|
|
88
|
+
deleted: number;
|
|
89
|
+
unchanged: number;
|
|
90
|
+
error: string | null;
|
|
91
|
+
// The version of the file (its stored time) the rows were last made from.
|
|
92
|
+
// A sync of an older one is skipped, so two that overlap can't leave the
|
|
93
|
+
// older sheet's rows.
|
|
94
|
+
version?: string | null;
|
|
95
|
+
// Only in a sync's answer: skipped, an older file than the rows already are.
|
|
96
|
+
stale?: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Who a sync's changes are made by, in row history and trigger events.
|
|
100
|
+
export const DATA_SHEET_SYNC_ACTOR = 'system:sheet-sync';
|
|
101
|
+
// A source file is read whole, so it stays small.
|
|
102
|
+
export const DATA_SHEET_MAX_BYTES = 5 * 1024 * 1024;
|
|
103
|
+
export const DATA_SHEET_MAX_ROWS = 20_000;
|
|
104
|
+
|
|
71
105
|
// A column as the database object stores it: the input plus its stable id,
|
|
72
106
|
// which names its indexes and constraints so a rename never orphans them.
|
|
73
107
|
export interface DataColumn {
|
|
@@ -100,6 +134,9 @@ export interface DataTable {
|
|
|
100
134
|
history: boolean;
|
|
101
135
|
position: number;
|
|
102
136
|
columns: DataColumn[];
|
|
137
|
+
// Set while the table is synced from a sheet: its rows are then read-only.
|
|
138
|
+
source?: DataTableSource | null;
|
|
139
|
+
sync?: DataTableSync | null;
|
|
103
140
|
}
|
|
104
141
|
|
|
105
142
|
export interface DataSchema {
|
|
@@ -138,11 +175,33 @@ export interface DataCallerLimits {
|
|
|
138
175
|
readOnly?: boolean;
|
|
139
176
|
}
|
|
140
177
|
|
|
178
|
+
// What one query on a table did: the columns it filtered and sorted on, and
|
|
179
|
+
// how many rows it read to return how many. The API sums them per column for
|
|
180
|
+
// a week to suggest indexes.
|
|
181
|
+
export interface DataQueryShape {
|
|
182
|
+
table: string;
|
|
183
|
+
filtered: string[];
|
|
184
|
+
sorted: string[];
|
|
185
|
+
rowsRead: number;
|
|
186
|
+
returned: number;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Rows one write operation wrote to a table, which tells a write-heavy table.
|
|
190
|
+
export interface DataWriteShape {
|
|
191
|
+
table: string;
|
|
192
|
+
rowsWritten: number;
|
|
193
|
+
}
|
|
194
|
+
|
|
141
195
|
export interface DataUsage {
|
|
142
196
|
rowsRead: number;
|
|
143
197
|
rowsWritten: number;
|
|
198
|
+
queries?: DataQueryShape[];
|
|
199
|
+
writes?: DataWriteShape[];
|
|
144
200
|
}
|
|
145
201
|
|
|
202
|
+
// At most this many shapes travel back with one call.
|
|
203
|
+
export const DATA_MAX_USAGE_SHAPES = 20;
|
|
204
|
+
|
|
146
205
|
export const DATA_ERROR_CODES = [
|
|
147
206
|
'invalid',
|
|
148
207
|
'not_found',
|
|
@@ -278,6 +337,46 @@ export interface DataDeleteImpact {
|
|
|
278
337
|
truncated: boolean;
|
|
279
338
|
}
|
|
280
339
|
|
|
340
|
+
// A file column links a knowledge item by its uri. Reads return it as
|
|
341
|
+
// { uri }, which the workers fill with the item's title and type.
|
|
342
|
+
export const DATA_FILE_URI_MAX = 2048;
|
|
343
|
+
export const DATA_FILE_LINKS_MAX_URIS = 500;
|
|
344
|
+
|
|
345
|
+
export interface DataFileValue {
|
|
346
|
+
uri: string;
|
|
347
|
+
// The item's id and size: only in the dashboard's reads, for previews.
|
|
348
|
+
id?: string;
|
|
349
|
+
size?: number;
|
|
350
|
+
title?: string;
|
|
351
|
+
mimeType?: string;
|
|
352
|
+
// No knowledge item has this uri any more.
|
|
353
|
+
missing?: true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// The rows whose file columns point at some uris, per column.
|
|
357
|
+
export interface DataFileLinks {
|
|
358
|
+
links: {
|
|
359
|
+
table: string;
|
|
360
|
+
tableLabel: string | null;
|
|
361
|
+
column: string;
|
|
362
|
+
columnLabel: string | null;
|
|
363
|
+
required: boolean;
|
|
364
|
+
rows: number;
|
|
365
|
+
}[];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export interface DataFileLinkChange {
|
|
369
|
+
from: string;
|
|
370
|
+
// null clears the link.
|
|
371
|
+
to: string | null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export interface DataFileLinksReplaced {
|
|
375
|
+
updated: number;
|
|
376
|
+
// Required columns can't be emptied: their links stay, and show as missing.
|
|
377
|
+
kept: { table: string; column: string; rows: number }[];
|
|
378
|
+
}
|
|
379
|
+
|
|
281
380
|
// A table copy running in the background, as callers see it.
|
|
282
381
|
export interface DataTableCopy {
|
|
283
382
|
table: string;
|
|
@@ -302,6 +401,15 @@ export const toTableInput = (table: DataTable): DataTableInput => ({
|
|
|
302
401
|
access: table.access,
|
|
303
402
|
customerScoped: table.customerScoped,
|
|
304
403
|
history: table.history,
|
|
404
|
+
...(table.source
|
|
405
|
+
? {
|
|
406
|
+
source: {
|
|
407
|
+
uri: table.source.uri,
|
|
408
|
+
...(table.source.sheet ? { sheet: table.source.sheet } : {}),
|
|
409
|
+
key: table.source.key
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
: {}),
|
|
305
413
|
columns: table.columns.map(column => {
|
|
306
414
|
const out: DataColumnInput = { name: column.name, type: column.type };
|
|
307
415
|
if (column.label) out.label = column.label;
|
package/src/index.ts
CHANGED
|
@@ -129,6 +129,8 @@ import { oauthProviders } from './oauthProviders';
|
|
|
129
129
|
import type { OAuthProviderConfig } from './oauthProviders';
|
|
130
130
|
import { resolveAttachment } from './attachment';
|
|
131
131
|
import { isExposedResource } from './exposedResource';
|
|
132
|
+
import { dataAttachmentOf, skipsIndexing } from './dataAttachment';
|
|
133
|
+
import type { DataAttachment } from './dataAttachment';
|
|
132
134
|
import type { ExposableResource } from './exposedResource';
|
|
133
135
|
import {
|
|
134
136
|
isSyncableResource,
|
|
@@ -270,9 +272,11 @@ import type {
|
|
|
270
272
|
import {
|
|
271
273
|
wrapUntrustedContent,
|
|
272
274
|
unwrapUntrustedContent,
|
|
275
|
+
stripUntrustedMarks,
|
|
273
276
|
isUntrustedToolResult,
|
|
274
277
|
findInstructionLikeText,
|
|
275
278
|
UNTRUSTED_CONTENT_TAG,
|
|
279
|
+
UNTRUSTED_CONTENT_MARK_PATTERN,
|
|
276
280
|
UNTRUSTED_CONTENT_META_KEY,
|
|
277
281
|
UNTRUSTED_CONTENT_NOTE,
|
|
278
282
|
INSTRUCTION_WARNING_METADATA_KEY,
|
|
@@ -391,6 +395,10 @@ import {
|
|
|
391
395
|
cleanWhatsappTemplateParam,
|
|
392
396
|
whatsappSendError
|
|
393
397
|
} from './whatsappTemplates';
|
|
398
|
+
import {
|
|
399
|
+
whatsappMessageChange,
|
|
400
|
+
whatsappAccountChanges
|
|
401
|
+
} from './whatsappWebhook';
|
|
394
402
|
import type {
|
|
395
403
|
WhatsappTemplateCategory,
|
|
396
404
|
WhatsappTemplateDraft,
|
|
@@ -428,6 +436,8 @@ export const utils = {
|
|
|
428
436
|
whatsappTemplateSendable,
|
|
429
437
|
cleanWhatsappTemplateParam,
|
|
430
438
|
whatsappSendError,
|
|
439
|
+
whatsappMessageChange,
|
|
440
|
+
whatsappAccountChanges,
|
|
431
441
|
getEnv,
|
|
432
442
|
fetcher,
|
|
433
443
|
constants,
|
|
@@ -485,6 +495,8 @@ export const utils = {
|
|
|
485
495
|
oauthProviders,
|
|
486
496
|
resolveAttachment,
|
|
487
497
|
isExposedResource,
|
|
498
|
+
dataAttachmentOf,
|
|
499
|
+
skipsIndexing,
|
|
488
500
|
isSyncableResource,
|
|
489
501
|
isResourceSyncInProgress,
|
|
490
502
|
resourceSyncProvider,
|
|
@@ -566,9 +578,11 @@ export const utils = {
|
|
|
566
578
|
stableJson,
|
|
567
579
|
wrapUntrustedContent,
|
|
568
580
|
unwrapUntrustedContent,
|
|
581
|
+
stripUntrustedMarks,
|
|
569
582
|
isUntrustedToolResult,
|
|
570
583
|
findInstructionLikeText,
|
|
571
584
|
UNTRUSTED_CONTENT_TAG,
|
|
585
|
+
UNTRUSTED_CONTENT_MARK_PATTERN,
|
|
572
586
|
UNTRUSTED_CONTENT_META_KEY,
|
|
573
587
|
UNTRUSTED_CONTENT_NOTE,
|
|
574
588
|
INSTRUCTION_WARNING_METADATA_KEY,
|
|
@@ -730,6 +744,7 @@ export type {
|
|
|
730
744
|
AttachmentResource,
|
|
731
745
|
ResolvedAttachment,
|
|
732
746
|
ResolveAttachmentResult,
|
|
747
|
+
DataAttachment,
|
|
733
748
|
ExposableResource,
|
|
734
749
|
SyncableResource,
|
|
735
750
|
ResourceSyncProvider,
|
package/src/localizeDataError.ts
CHANGED
|
@@ -39,6 +39,21 @@ const guardSuffix = (suffix: string): string => {
|
|
|
39
39
|
return suffix;
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
+
// Meta's other template statuses, as the refusal writes them.
|
|
43
|
+
const TEMPLATE_STATUS_ES: Record<string, string> = {
|
|
44
|
+
paused: 'pausada',
|
|
45
|
+
disabled: 'desactivada',
|
|
46
|
+
flagged: 'marcada',
|
|
47
|
+
'in appeal': 'en apelación',
|
|
48
|
+
'pending deletion': 'pendiente de eliminación',
|
|
49
|
+
deleted: 'eliminada',
|
|
50
|
+
'limit exceeded': 'por encima del límite'
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// "The WhatsApp template x (es)", as a channel's refusal names it.
|
|
54
|
+
const templateLabel = (label: string): string =>
|
|
55
|
+
label.replace(/^The WhatsApp template /, 'La plantilla de WhatsApp ');
|
|
56
|
+
|
|
42
57
|
export const DATA_ERROR_MESSAGES_ES: Record<string, string | Render> = {
|
|
43
58
|
'Unknown column "{0}" in {1}. Columns: {2}.':
|
|
44
59
|
'Columna desconocida "{0}" en {1}. Columnas: {2}.',
|
|
@@ -124,8 +139,22 @@ export const DATA_ERROR_MESSAGES_ES: Record<string, string | Render> = {
|
|
|
124
139
|
'{0}: "min" is a number.': '{0}: "min" es un número.',
|
|
125
140
|
"{0}: a {1} column can't be unique.":
|
|
126
141
|
'{0}: una columna {1} no puede ser única.',
|
|
127
|
-
'{0}: only text columns can be searchable (text, long_text, email, url, phone, enum).':
|
|
128
|
-
'{0}: solo las columnas de texto pueden ser buscables (text, long_text, email, url, phone, enum).',
|
|
142
|
+
'{0}: only text and file columns can be searchable (text, long_text, email, url, phone, enum, file).':
|
|
143
|
+
'{0}: solo las columnas de texto y de archivo pueden ser buscables (text, long_text, email, url, phone, enum, file).',
|
|
144
|
+
"{0} takes a knowledge item's uri, e.g. resource://menu-photo.":
|
|
145
|
+
'{0} lleva el uri de un elemento del conocimiento, p. ej. resource://menu-photo.',
|
|
146
|
+
'{0}: a uri is at most {1} characters.':
|
|
147
|
+
'{0}: un uri tiene como máximo {1} caracteres.',
|
|
148
|
+
'"uris" lists 1 to {0} knowledge item uris.':
|
|
149
|
+
'"uris" lista de 1 a {0} uris de elementos del conocimiento.',
|
|
150
|
+
'No resource in this project has the uri {0}; link an existing resource by its uri.':
|
|
151
|
+
'Ningún recurso de este proyecto tiene el uri {0}; enlaza un recurso que exista por su uri.',
|
|
152
|
+
'File links are for the owner.':
|
|
153
|
+
'Los enlaces a archivos son para el dueño.',
|
|
154
|
+
'"changes" lists 1 to {0} { from, to } pairs.':
|
|
155
|
+
'"changes" lista de 1 a {0} pares { from, to }.',
|
|
156
|
+
'Each change is { from, to }; to is null to clear.':
|
|
157
|
+
'Cada cambio es { from, to }; to es null para quitar el enlace.',
|
|
129
158
|
'{0}: a column named "rank" can\'t be searchable; rename it (e.g. "ranking").':
|
|
130
159
|
'{0}: una columna llamada "rank" no puede ser buscable; cámbiale el nombre (p. ej. "ranking").',
|
|
131
160
|
"{0}: a {1} column can't be filterable; mark it searchable instead.":
|
|
@@ -395,7 +424,24 @@ export const DATA_ERROR_MESSAGES_ES: Record<string, string | Render> = {
|
|
|
395
424
|
'A trigger is an object.': 'Un disparador es un objeto.',
|
|
396
425
|
'There is no table named {0}.': 'No hay ninguna tabla llamada {0}.',
|
|
397
426
|
'A trigger needs "table".': 'Un disparador necesita "table".',
|
|
398
|
-
'"on" is insert, update or
|
|
427
|
+
'"on" is insert, update, delete or time.':
|
|
428
|
+
'"on" es insert, update, delete o time.',
|
|
429
|
+
// Time triggers.
|
|
430
|
+
'"after" is for "on": "time" only.': '"after" es solo para "on": "time".',
|
|
431
|
+
'A time trigger needs "after": { "column", "minutes" }, such as { "column": "created_at", "minutes": 30 }.':
|
|
432
|
+
'Un disparador por tiempo necesita "after": { "column", "minutes" }, por ejemplo { "column": "created_at", "minutes": 30 }.',
|
|
433
|
+
'"after.column" is created_at, updated_at or a date or datetime column of {0}.':
|
|
434
|
+
'"after.column" es created_at, updated_at o una columna de fecha o de fecha y hora de {0}.',
|
|
435
|
+
'"after.minutes" is a whole number of minutes from {0} to {1} (30 days), negative for before the column\'s time.':
|
|
436
|
+
'"after.minutes" es un número entero de minutos de {0} a {1} (30 días), negativo para antes de la hora de la columna.',
|
|
437
|
+
'"after.minutes" can\'t be negative on {0}: that time has already passed when the row is written. Use a date or datetime column for a reminder before.':
|
|
438
|
+
'"after.minutes" no puede ser negativo en {0}: esa hora ya pasó cuando se escribe la fila. Usa una columna de fecha o de fecha y hora para un recordatorio antes.',
|
|
439
|
+
'"when".{0}.{1}: a time trigger reads the row as it is when it comes due, so its tests are "equals" only.':
|
|
440
|
+
'"when".{0}.{1}: un disparador por tiempo lee la fila tal como está cuando se cumple el plazo, así que sus pruebas son solo "equals".',
|
|
441
|
+
'A database holds up to {0} time triggers.':
|
|
442
|
+
'Una base de datos tiene hasta {0} disparadores por tiempo.',
|
|
443
|
+
'{0}.{1} stays a date or datetime while time triggers count from it ({2}); change or delete them first.':
|
|
444
|
+
'{0}.{1} debe seguir siendo de fecha o de fecha y hora mientras haya disparadores por tiempo que cuenten desde ella ({2}); cámbialos o elimínalos primero.',
|
|
399
445
|
"A trigger's name is lowercase letters, digits, _ and -, starting with a letter.":
|
|
400
446
|
'El nombre de un disparador usa minúsculas, dígitos, _ y -, empezando por una letra.',
|
|
401
447
|
'"rateLimit" is 1 to {0} runs a minute.':
|
|
@@ -469,6 +515,9 @@ export const DATA_ERROR_MESSAGES_ES: Record<string, string | Render> = {
|
|
|
469
515
|
'Type the table\'s name, "{0}", to drop it.':
|
|
470
516
|
'Escribe el nombre de la tabla, "{0}", para eliminarla.',
|
|
471
517
|
'There is no such run.': 'No existe esa ejecución.',
|
|
518
|
+
'There is no such index suggestion.': 'No existe esa sugerencia de índice.',
|
|
519
|
+
'An unused index is only flagged; remove it from the column if you want to, or dismiss the suggestion.':
|
|
520
|
+
'Un índice sin uso solo se señala; quítalo de la columna si quieres, o descarta la sugerencia.',
|
|
472
521
|
'Only a failed or skipped run can be tried again.':
|
|
473
522
|
'Solo se puede reintentar una ejecución fallida u omitida.',
|
|
474
523
|
'This run kept no event to send again.':
|
|
@@ -516,7 +565,137 @@ export const DATA_ERROR_MESSAGES_ES: Record<string, string | Render> = {
|
|
|
516
565
|
'"{0}" is reserved by SQLite; pick another column name (e.g. "{1}_ref").':
|
|
517
566
|
'"{0}" está reservado por SQLite; elige otro nombre de columna (p. ej. "{1}_ref").',
|
|
518
567
|
'"{0}" is a column every table already has.':
|
|
519
|
-
'"{0}" es una columna que ya tienen todas las tablas.'
|
|
568
|
+
'"{0}" es una columna que ya tienen todas las tablas.',
|
|
569
|
+
// A test send of a trigger's customer message.
|
|
570
|
+
"You haven't linked a chat with this project's bots: write to the bot from your own phone and send /link.":
|
|
571
|
+
'No has vinculado un chat con los bots de este proyecto: escríbele al bot desde tu propio teléfono y envía /link.',
|
|
572
|
+
"{0} isn't one of your linked chats with this project's bots.":
|
|
573
|
+
'{0} no es uno de tus chats vinculados con los bots de este proyecto.',
|
|
574
|
+
'Several of your chats are linked to this project\'s bots; pick one with "participantId".':
|
|
575
|
+
'Varios de tus chats están vinculados con los bots de este proyecto; elige uno con "participantId".',
|
|
576
|
+
"This organization's plan no longer includes Data, so its triggers are paused and can't be tested. Upgrade to Pro to send a test.":
|
|
577
|
+
'El plan de esta organización ya no incluye Datos, así que sus disparadores están en pausa y no se pueden probar. Pasa a Pro para enviar una prueba.',
|
|
578
|
+
'{0} has no rows yet; add one to build the test from.':
|
|
579
|
+
'{0} aún no tiene filas; agrega una para armar la prueba.',
|
|
580
|
+
'Reading a row as its trigger event is for the project owner.':
|
|
581
|
+
'Leer una fila como su evento de disparador es solo para el dueño del proyecto.',
|
|
582
|
+
// The channel's refusals, which a test send answers with.
|
|
583
|
+
"This channel's credentials can't be read; connect it again.":
|
|
584
|
+
'No se pueden leer las credenciales de este canal; conéctalo de nuevo.',
|
|
585
|
+
'A message needs "text".': 'Un mensaje necesita "text".',
|
|
586
|
+
'A message is at most {0} characters.':
|
|
587
|
+
'Un mensaje tiene como máximo {0} caracteres.',
|
|
588
|
+
'{0} refused the message: {1}': '{0} rechazó el mensaje: {1}',
|
|
589
|
+
'{0} refused the message.': '{0} rechazó el mensaje.',
|
|
590
|
+
"Ganju can't send messages on {0} channels.":
|
|
591
|
+
'Ganju no puede enviar mensajes en canales de {0}.',
|
|
592
|
+
'WhatsApp: {0}': 'WhatsApp: {0}',
|
|
593
|
+
'The {0} channel is paused.': 'El canal de {0} está en pausa.',
|
|
594
|
+
"{0} isn't on this channel's WhatsApp Business account. Create it in the channel's Message templates, or check its name and language.":
|
|
595
|
+
a =>
|
|
596
|
+
`${templateLabel(a[0])} no está en la cuenta de WhatsApp Business de este canal. Créala en Plantillas de mensajes del canal, o revisa su nombre e idioma.`,
|
|
597
|
+
'{0} {1}; only approved templates can be sent.': a => {
|
|
598
|
+
const whole = `${a[0]} ${a[1]}`;
|
|
599
|
+
const m =
|
|
600
|
+
/^(The WhatsApp template [\s\S]+? \([^)]*\)) (is still in Meta's review|was rejected by Meta|is ([\s\S]+?) by Meta)( \([\s\S]*\))?$/.exec(
|
|
601
|
+
whole
|
|
602
|
+
);
|
|
603
|
+
if (!m) return `${whole}; solo se pueden enviar plantillas aprobadas.`;
|
|
604
|
+
const why = m[2].startsWith('is still')
|
|
605
|
+
? 'sigue en revisión de Meta'
|
|
606
|
+
: m[2].startsWith('was rejected')
|
|
607
|
+
? 'fue rechazada por Meta'
|
|
608
|
+
: `está ${TEMPLATE_STATUS_ES[m[3]] ?? `en el estado ${m[3]}`} por Meta`;
|
|
609
|
+
return `${templateLabel(m[1])} ${why}${m[4] ?? ''}; solo se pueden enviar plantillas aprobadas.`;
|
|
610
|
+
},
|
|
611
|
+
"{0} has a media header, a header variable or a button that takes a value; Ganju only fills the message's variables. Make a version without them.":
|
|
612
|
+
a =>
|
|
613
|
+
`${templateLabel(a[0])} tiene un encabezado con archivo, una variable en el encabezado o un botón que recibe un valor; Ganju solo llena las variables del mensaje. Haz una versión sin ellos.`,
|
|
614
|
+
"There is no customer {0} on this project's channels.":
|
|
615
|
+
'No hay ningún cliente {0} en los canales de este proyecto.',
|
|
616
|
+
'A message needs "text", a "template", or both.':
|
|
617
|
+
'Un mensaje necesita "text", una "template", o ambos.',
|
|
618
|
+
'This customer has never written to the bot privately, so there is no chat to message them in.':
|
|
619
|
+
'Este cliente nunca le ha escrito al bot en privado, así que no hay un chat donde escribirle.',
|
|
620
|
+
"This customer already got the channel's {0} messages for the last 24 hours.":
|
|
621
|
+
'Este cliente ya recibió los {0} mensajes del canal de las últimas 24 horas.',
|
|
622
|
+
"Outside WhatsApp's 24-hour window (this customer last wrote more than 24 hours ago); use an approved template.":
|
|
623
|
+
'Fuera de la ventana de 24 horas de WhatsApp (este cliente escribió por última vez hace más de 24 horas); usa una plantilla aprobada.',
|
|
624
|
+
'Templates are for WhatsApp; {0} takes "text".':
|
|
625
|
+
'Las plantillas son para WhatsApp; {0} recibe "text".',
|
|
626
|
+
'The WhatsApp template {0} has {1} variable; {2} value was given.':
|
|
627
|
+
'La plantilla de WhatsApp {0} tiene {1} variable; se dio {2} valor.',
|
|
628
|
+
'The WhatsApp template {0} has {1} variable; {2} values were given.':
|
|
629
|
+
'La plantilla de WhatsApp {0} tiene {1} variable; se dieron {2} valores.',
|
|
630
|
+
'The WhatsApp template {0} has {1} variables; {2} value was given.':
|
|
631
|
+
'La plantilla de WhatsApp {0} tiene {1} variables; se dio {2} valor.',
|
|
632
|
+
'The WhatsApp template {0} has {1} variables; {2} values were given.':
|
|
633
|
+
'La plantilla de WhatsApp {0} tiene {1} variables; se dieron {2} valores.',
|
|
634
|
+
// Tables synced from a sheet
|
|
635
|
+
'{0} is synced from "{1}"; change the sheet, or stop syncing it.':
|
|
636
|
+
'{0} se sincroniza desde "{1}"; cambia la hoja, o deja de sincronizarla.',
|
|
637
|
+
'"source" is { "uri", "sheet"?, "key" }, or null to stop syncing.':
|
|
638
|
+
'"source" es { "uri", "sheet"?, "key" }, o null para dejar de sincronizar.',
|
|
639
|
+
'"source" takes uri, sheet and key, not "{0}".':
|
|
640
|
+
'"source" recibe uri, sheet y key, no "{0}".',
|
|
641
|
+
'"source.uri" is the uri of a spreadsheet or CSV in the project\'s knowledge.':
|
|
642
|
+
'"source.uri" es el uri de una hoja de cálculo o un CSV en el conocimiento del proyecto.',
|
|
643
|
+
'"source.sheet" is the name of a sheet in the file, up to 100 characters.':
|
|
644
|
+
'"source.sheet" es el nombre de una hoja del archivo, de hasta 100 caracteres.',
|
|
645
|
+
'"source.key" is the column that names a row in both the sheet and {0}. Columns: {1}.':
|
|
646
|
+
'"source.key" es la columna que identifica una fila tanto en la hoja como en {0}. Columnas: {1}.',
|
|
647
|
+
'{0}.{1} must be unique to be the sync key.':
|
|
648
|
+
'{0}.{1} debe ser única para ser la clave de la sincronización.',
|
|
649
|
+
'{0}.{1} is the key the sheet sync matches rows by, so it stays unique.':
|
|
650
|
+
'{0}.{1} es la clave con la que la sincronización empareja las filas, así que sigue siendo única.',
|
|
651
|
+
"{0}.{1} is the sheet sync's key: rename it on its own, then make the other changes.":
|
|
652
|
+
'{0}.{1} es la clave de la sincronización: cámbiale el nombre por separado, y luego haz los demás cambios.',
|
|
653
|
+
'{0}.{1} is the key the sheet sync matches rows by; stop syncing the table, or pick another key, first.':
|
|
654
|
+
'{0}.{1} es la clave con la que la sincronización empareja las filas; primero deja de sincronizar la tabla, o elige otra clave.',
|
|
655
|
+
"{0} isn't synced from a sheet.": '{0} no se sincroniza desde una hoja.',
|
|
656
|
+
'A sync takes "columns" and "rows".':
|
|
657
|
+
'Una sincronización recibe "columns" y "rows".',
|
|
658
|
+
"The sheet has no column for {0}'s key, {1}.":
|
|
659
|
+
'La hoja no tiene una columna para la clave de {0}, {1}.',
|
|
660
|
+
'The sheet has no column for {0}, which is required.':
|
|
661
|
+
'La hoja no tiene una columna para {0}, que es obligatoria.',
|
|
662
|
+
'The sheet has no column for {0}, which are required.':
|
|
663
|
+
'La hoja no tiene columnas para {0}, que son obligatorias.',
|
|
664
|
+
'The sheet has {0} rows; a synced table takes up to {1}.':
|
|
665
|
+
'La hoja tiene {0} filas; una tabla sincronizada recibe hasta {1}.',
|
|
666
|
+
'Line {0}: {1}': (a, t) => `Línea ${a[0]}: ${t(a[1])}`,
|
|
667
|
+
'A row is an object of column: value.':
|
|
668
|
+
'Una fila es un objeto de columna: valor.',
|
|
669
|
+
'The key, {0}, is empty.': 'La clave, {0}, está vacía.',
|
|
670
|
+
'{0} {1} is already on line {2}; each line of the sheet needs its own key.':
|
|
671
|
+
'{0} {1} ya está en la línea {2}; cada línea de la hoja necesita su propia clave.',
|
|
672
|
+
'Rows in {0} still point at a row the sheet no longer has ({1}.{2}); put them back in the sheet, or change those rows first.':
|
|
673
|
+
'Filas de {0} todavía apuntan a una fila que la hoja ya no tiene ({1}.{2}); vuelve a ponerla en la hoja, o cambia esas filas primero.',
|
|
674
|
+
'Rows in {0} still point at {1} rows the sheet no longer has ({2}.{3}); put them back in the sheet, or change those rows first.':
|
|
675
|
+
'Filas de {0} todavía apuntan a {1} filas que la hoja ya no tiene ({2}.{3}); vuelve a ponerlas en la hoja, o cambia esas filas primero.',
|
|
676
|
+
"There is no file {0} in the project's knowledge.":
|
|
677
|
+
'No hay ningún archivo {0} en el conocimiento del proyecto.',
|
|
678
|
+
'{0} is {1}; a table syncs from a spreadsheet or CSV.':
|
|
679
|
+
'{0} es {1}; una tabla se sincroniza desde una hoja de cálculo o un CSV.',
|
|
680
|
+
"{0} is no longer in the project's knowledge; pick the file again, or stop syncing {1}.":
|
|
681
|
+
'{0} ya no está en el conocimiento del proyecto; vuelve a elegir el archivo, o deja de sincronizar {1}.',
|
|
682
|
+
'{0} is over {1} MB; a synced table reads files up to that size.':
|
|
683
|
+
'{0} pesa más de {1} MB; una tabla sincronizada lee archivos de hasta ese tamaño.',
|
|
684
|
+
'The sheet could not be read; try "Sync now" again in a few minutes.':
|
|
685
|
+
'No se pudo leer la hoja; vuelve a intentar "Sincronizar ahora" en unos minutos.',
|
|
686
|
+
"{0} can't be both customer-scoped and synced from a sheet: synced rows have no customer, so the bots wouldn't see them.":
|
|
687
|
+
'{0} no puede estar limitada al cliente y a la vez sincronizarse desde una hoja: las filas sincronizadas no tienen cliente, así que los bots no las verían.',
|
|
688
|
+
'Can\'t delete: it would change a row of {0} ({1}.{2}), which is synced from "{3}"; change the sheet first, or stop syncing it.':
|
|
689
|
+
'No se puede eliminar: cambiaría una fila de {0} ({1}.{2}), que se sincroniza desde "{3}"; primero cambia la hoja, o deja de sincronizarla.',
|
|
690
|
+
'Can\'t delete: it would change {0} rows of {1} ({2}.{3}), which is synced from "{4}"; change the sheet first, or stop syncing it.':
|
|
691
|
+
'No se puede eliminar: cambiaría {0} filas de {1} ({2}.{3}), que se sincroniza desde "{4}"; primero cambia la hoja, o deja de sincronizarla.',
|
|
692
|
+
'{0} has no sheet "{1}". Sheets: {2}.': '{0} no tiene una hoja "{1}". Hojas: {2}.',
|
|
693
|
+
'{0} is over {1} MB.': '{0} pesa más de {1} MB.',
|
|
694
|
+
'{0} is {1}; a table reads CSV and Excel files.':
|
|
695
|
+
'{0} es {1}; una tabla lee archivos CSV y de Excel.',
|
|
696
|
+
'A post needs "chatId".': 'Una publicación necesita "chatId".',
|
|
697
|
+
'WhatsApp has no group chats for bots; message a customer with send().':
|
|
698
|
+
'WhatsApp no tiene chats de grupo para bots; escríbele a un cliente con send().'
|
|
520
699
|
};
|
|
521
700
|
|
|
522
701
|
interface Compiled {
|
package/src/schema.ts
CHANGED
|
@@ -418,6 +418,14 @@ const ARTIFACT_SYNC_ONE_DRIVE = z.object({
|
|
|
418
418
|
organizationId: z.uuid()
|
|
419
419
|
});
|
|
420
420
|
|
|
421
|
+
// A knowledge item uploaded from a Data row: which file column it was for,
|
|
422
|
+
// and whether it is indexed for search too (the column's searchable switch).
|
|
423
|
+
const DATA_ATTACHMENT_METADATA = z.object({
|
|
424
|
+
table: z.string().min(1).max(64),
|
|
425
|
+
column: z.string().min(1).max(64),
|
|
426
|
+
index: z.boolean()
|
|
427
|
+
});
|
|
428
|
+
|
|
421
429
|
const ARTIFACT_CREATE_RESOURCE = z.object({
|
|
422
430
|
title: z.string().min(3).max(200),
|
|
423
431
|
uri: z.string(),
|
|
@@ -529,6 +537,20 @@ const ARTIFACT_GET_RESOURCE_BY_ID = z.object({
|
|
|
529
537
|
organizationId: z.uuid()
|
|
530
538
|
});
|
|
531
539
|
|
|
540
|
+
// The knowledge items a Data file column can link, newest first.
|
|
541
|
+
const ARTIFACT_PICK_RESOURCE = z.object({
|
|
542
|
+
projectId: z.uuid(),
|
|
543
|
+
userId: z.uuid(),
|
|
544
|
+
organizationId: z.uuid(),
|
|
545
|
+
q: z.string().trim().max(200).optional(),
|
|
546
|
+
limit: z.coerce.number().int().min(1).max(50).default(20),
|
|
547
|
+
// Only spreadsheets and CSV, for a table's sheet source.
|
|
548
|
+
tabular: z
|
|
549
|
+
.enum(['1', 'true'])
|
|
550
|
+
.optional()
|
|
551
|
+
.transform(v => v !== undefined)
|
|
552
|
+
});
|
|
553
|
+
|
|
532
554
|
const ARTIFACT_REMOVE_RESOURCE = z.object({
|
|
533
555
|
resourceId: z.uuid(),
|
|
534
556
|
projectId: z.uuid(),
|
|
@@ -585,6 +607,14 @@ const DATA_COLUMN_INPUT = z.strictObject({
|
|
|
585
607
|
});
|
|
586
608
|
|
|
587
609
|
// The CLI's schema file holds a list of these.
|
|
610
|
+
// A spreadsheet or CSV in the project's knowledge the table's rows come
|
|
611
|
+
// from, matched by `key`. Null stops syncing.
|
|
612
|
+
const DATA_TABLE_SOURCE = z.strictObject({
|
|
613
|
+
uri: z.string().min(1).max(2048),
|
|
614
|
+
sheet: z.string().trim().min(1).max(100).nullable().optional(),
|
|
615
|
+
key: DATA_IDENTIFIER
|
|
616
|
+
});
|
|
617
|
+
|
|
588
618
|
const DATA_TABLE_INPUT = z.strictObject({
|
|
589
619
|
name: DATA_IDENTIFIER,
|
|
590
620
|
label: DATA_TEXT(DATA_MAX_LABEL),
|
|
@@ -593,6 +623,7 @@ const DATA_TABLE_INPUT = z.strictObject({
|
|
|
593
623
|
access: z.enum(DATA_ACCESS_LEVELS).optional(),
|
|
594
624
|
customerScoped: z.boolean().optional(),
|
|
595
625
|
history: z.boolean().optional(),
|
|
626
|
+
source: DATA_TABLE_SOURCE.nullable().optional(),
|
|
596
627
|
columns: z.array(DATA_COLUMN_INPUT).max(DATA_MAX_COLUMNS)
|
|
597
628
|
});
|
|
598
629
|
|
|
@@ -798,9 +829,18 @@ const DATA_TRIGGER_INPUT = z.strictObject({
|
|
|
798
829
|
name: DATA_TRIGGER_NAME.optional(),
|
|
799
830
|
description: z.string().max(300).nullish(),
|
|
800
831
|
table: DATA_IDENTIFIER,
|
|
801
|
-
on: z.enum(['insert', 'update', 'delete']),
|
|
832
|
+
on: z.enum(['insert', 'update', 'delete', 'time']),
|
|
833
|
+
// With "time": fires this many minutes after the column's time (negative:
|
|
834
|
+
// before it). The column is created_at, updated_at or a date / datetime.
|
|
835
|
+
after: z
|
|
836
|
+
.strictObject({
|
|
837
|
+
column: DATA_IDENTIFIER,
|
|
838
|
+
minutes: z.number().int().min(-43_200).max(43_200)
|
|
839
|
+
})
|
|
840
|
+
.nullish(),
|
|
802
841
|
// { column: { changedTo | changedFrom | equals: value, changed: true } },
|
|
803
|
-
// combined with { all: [...] } or { any: [...] }.
|
|
842
|
+
// combined with { all: [...] } or { any: [...] }. A time trigger takes
|
|
843
|
+
// equals only.
|
|
804
844
|
when: z.record(z.string(), z.unknown()).nullish(),
|
|
805
845
|
enabled: z.boolean().optional(),
|
|
806
846
|
// false: refuse to save over an existing trigger of this name.
|
|
@@ -834,6 +874,17 @@ const DATA_TRIGGER_PATCH = z.strictObject({ enabled: z.boolean() });
|
|
|
834
874
|
const DATA_TRIGGER_DEPLOY_BODY = z.strictObject({
|
|
835
875
|
triggers: z.array(DATA_TRIGGER_INPUT).max(50)
|
|
836
876
|
});
|
|
877
|
+
// A test of a "customer" action as the dialog has it, sent to the signed-in
|
|
878
|
+
// owner's own chat with the bot, built from one row (the newest by default).
|
|
879
|
+
const DATA_TRIGGER_TEST_SEND_BODY = z.strictObject({
|
|
880
|
+
table: DATA_IDENTIFIER,
|
|
881
|
+
customer: DATA_TRIGGER_INPUT.shape.customer.unwrap(),
|
|
882
|
+
rowId: z.number().int().min(1).optional(),
|
|
883
|
+
// Which of the owner's linked chats, when there is more than one.
|
|
884
|
+
participantId: z.string().min(1).max(64).optional(),
|
|
885
|
+
// Send the template even inside WhatsApp's 24-hour window.
|
|
886
|
+
asTemplate: z.boolean().optional()
|
|
887
|
+
});
|
|
837
888
|
const DATA_TRIGGER_RUN_LIST_QUERY = z.object({
|
|
838
889
|
limit: z.coerce.number().int().min(1).max(50).optional()
|
|
839
890
|
});
|
|
@@ -848,6 +899,8 @@ const DATA_TRIGGER_PATH_PARAMS = DATA_DATABASE_GET.extend({ name: DATA_TRIGGER_N
|
|
|
848
899
|
const DATA_TRIGGER_RUN_PARAMS = DATA_TRIGGER_PATH_PARAMS.extend({
|
|
849
900
|
runId: z.string().min(1).max(64)
|
|
850
901
|
});
|
|
902
|
+
// An index suggestion, by its id.
|
|
903
|
+
const DATA_INDEX_ADVICE_PARAMS = DATA_DATABASE_GET.extend({ adviceId: z.uuid() });
|
|
851
904
|
|
|
852
905
|
const ARTIFACT_UPLOAD_RESOURCE_FILE = z.object({
|
|
853
906
|
resourceId: z.uuid(),
|
|
@@ -2150,6 +2203,8 @@ export const Schema = {
|
|
|
2150
2203
|
ARTIFACT_GET_PROMPT,
|
|
2151
2204
|
ARTIFACT_REMOVE_PROMPT,
|
|
2152
2205
|
ARTIFACT_CREATE_RESOURCE,
|
|
2206
|
+
DATA_ATTACHMENT_METADATA,
|
|
2207
|
+
ARTIFACT_PICK_RESOURCE,
|
|
2153
2208
|
ARTIFACT_CREATE_RESOURCE_VIEW,
|
|
2154
2209
|
ARTIFACT_CREATE_WEBSITE,
|
|
2155
2210
|
ARTIFACT_CREATE_WEBSITE_VIEW,
|
|
@@ -2169,6 +2224,7 @@ export const Schema = {
|
|
|
2169
2224
|
DATA_DATABASE_CREATE,
|
|
2170
2225
|
DATA_DATABASE_DELETE,
|
|
2171
2226
|
DATA_COLUMN_INPUT,
|
|
2227
|
+
DATA_TABLE_SOURCE,
|
|
2172
2228
|
DATA_TABLE_INPUT,
|
|
2173
2229
|
DATA_TABLE_PATCH,
|
|
2174
2230
|
DATA_COLUMN_PATCH,
|
|
@@ -2196,9 +2252,11 @@ export const Schema = {
|
|
|
2196
2252
|
DATA_TRIGGER_INPUT,
|
|
2197
2253
|
DATA_TRIGGER_PATCH,
|
|
2198
2254
|
DATA_TRIGGER_DEPLOY_BODY,
|
|
2255
|
+
DATA_TRIGGER_TEST_SEND_BODY,
|
|
2199
2256
|
DATA_TRIGGER_RUN_LIST_QUERY,
|
|
2200
2257
|
DATA_TRIGGER_PATH_PARAMS,
|
|
2201
2258
|
DATA_TRIGGER_RUN_PARAMS,
|
|
2259
|
+
DATA_INDEX_ADVICE_PARAMS,
|
|
2202
2260
|
DATA_COLUMN_PARAMS,
|
|
2203
2261
|
DATA_ROW_PARAMS,
|
|
2204
2262
|
ARTIFACT_REMOVE_RESOURCE,
|
package/src/untrustedContent.ts
CHANGED
|
@@ -121,6 +121,21 @@ export const unwrapUntrustedContent = (text: string): string => {
|
|
|
121
121
|
return match ? match[2] : text;
|
|
122
122
|
};
|
|
123
123
|
|
|
124
|
+
// Either line wrapUntrustedContent adds around a block, with the newline that
|
|
125
|
+
// joins it to the body. A `<` of ours inside a body was escaped, so every
|
|
126
|
+
// match is a real mark. A source string so SQL can strip the same marks.
|
|
127
|
+
export const UNTRUSTED_CONTENT_MARK_PATTERN =
|
|
128
|
+
`\\n?<${UNTRUSTED_CONTENT_TAG} id="[0-9a-f]+" source="[^"]*">\\n?` +
|
|
129
|
+
`|\\n?</${UNTRUSTED_CONTENT_TAG} id="[0-9a-f]+">`;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Text with the marks around every wrapped block removed and the bodies kept,
|
|
133
|
+
* for showing a recorded result to a person: an error that quotes an
|
|
134
|
+
* endpoint's answer reads as the message and the answer.
|
|
135
|
+
*/
|
|
136
|
+
export const stripUntrustedMarks = (text: string): string =>
|
|
137
|
+
text.replace(new RegExp(UNTRUSTED_CONTENT_MARK_PATTERN, 'g'), '\n').trim();
|
|
138
|
+
|
|
124
139
|
/** Whether a tool result carries the flag set by the server that ran it. */
|
|
125
140
|
export const isUntrustedToolResult = (result: unknown): boolean => {
|
|
126
141
|
if (!result || typeof result !== 'object') return false;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// What one WhatsApp channel takes from a webhook. A Meta app sends every
|
|
2
|
+
// number's events to each of its callbacks, signed with the same app secret,
|
|
3
|
+
// so a channel keeps only what was addressed to its own number and business
|
|
4
|
+
// account; otherwise another channel's bot answers its messages.
|
|
5
|
+
|
|
6
|
+
export interface WhatsappWebhookChange<V> {
|
|
7
|
+
field?: string;
|
|
8
|
+
value?: V;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface WhatsappWebhookEnvelope<V> {
|
|
12
|
+
entry?: Array<{ id?: string; changes?: Array<WhatsappWebhookChange<V>> }>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface WhatsappAddressedValue {
|
|
16
|
+
metadata?: { phone_number_id?: string };
|
|
17
|
+
messages?: unknown[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The change carrying a message for this channel's number, wherever Meta put
|
|
22
|
+
* it in the batch. A change that doesn't say which number it's for is kept.
|
|
23
|
+
*/
|
|
24
|
+
export const whatsappMessageChange = <V extends WhatsappAddressedValue>(
|
|
25
|
+
payload: WhatsappWebhookEnvelope<V>,
|
|
26
|
+
phoneNumberId: string
|
|
27
|
+
): V | undefined =>
|
|
28
|
+
(payload.entry ?? [])
|
|
29
|
+
.flatMap(entry => entry.changes ?? [])
|
|
30
|
+
.find(change => {
|
|
31
|
+
const value = change.value;
|
|
32
|
+
if (!value?.messages?.length) return false;
|
|
33
|
+
const to = value.metadata?.phone_number_id;
|
|
34
|
+
return !to || to === phoneNumberId;
|
|
35
|
+
})?.value;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The changes with one of `fields` from this channel's business account. An
|
|
39
|
+
* entry without an id, or a channel that hasn't saved its account, keeps
|
|
40
|
+
* them: template events only touch the channel's own stored templates.
|
|
41
|
+
*/
|
|
42
|
+
export const whatsappAccountChanges = <V>(
|
|
43
|
+
payload: WhatsappWebhookEnvelope<V>,
|
|
44
|
+
businessAccountId: string | undefined,
|
|
45
|
+
fields: readonly string[]
|
|
46
|
+
): WhatsappWebhookChange<V>[] =>
|
|
47
|
+
(payload.entry ?? [])
|
|
48
|
+
.filter(
|
|
49
|
+
entry => !entry.id || !businessAccountId || entry.id === businessAccountId
|
|
50
|
+
)
|
|
51
|
+
.flatMap(entry =>
|
|
52
|
+
(entry.changes ?? []).filter(
|
|
53
|
+
change => !!change.field && fields.includes(change.field)
|
|
54
|
+
)
|
|
55
|
+
);
|