@automate.ax/integration-contracts 0.161.0 → 0.163.1
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/LICENSE +21 -0
- package/dist/github/additional-events.d.ts +1 -1
- package/dist/github/additional-events.js +1 -0
- package/dist/github/openapi-types.generated.d.ts +0 -21
- package/dist/github/rest-schemas.generated.json +1 -1
- package/dist/github/webhook-schemas.generated.json +1 -1
- package/dist/highlevel/events.d.ts +4 -4
- package/dist/highlevel/events.js +1 -1
- package/dist/meta-ads/events.d.ts +298 -1
- package/dist/meta-ads/events.js +402 -35
- package/dist/meta-ads/index.d.ts +1 -0
- package/dist/meta-ads/index.js +1 -0
- package/dist/meta-ads/pages.d.ts +34 -0
- package/dist/meta-ads/pages.js +79 -0
- package/dist/meta-ads/schemas.d.ts +86 -2
- package/dist/meta-ads/schemas.js +74 -1
- package/dist/outlook/graph.d.ts +20 -2
- package/dist/outlook/graph.js +26 -3
- package/package.json +5 -12
- package/src/github/additional-events.ts +2 -0
- package/src/github/openapi-types.generated.ts +0 -21
- package/src/github/rest-schemas.generated.json +1 -1
- package/src/github/webhook-schemas.generated.json +1 -1
- package/src/highlevel/events.ts +1 -1
- package/src/meta-ads/events.ts +475 -34
- package/src/meta-ads/index.ts +1 -0
- package/src/meta-ads/pages.ts +98 -0
- package/src/meta-ads/schemas.ts +83 -1
- package/src/outlook/graph.ts +32 -3
package/src/highlevel/events.ts
CHANGED
|
@@ -85,7 +85,7 @@ const MESSAGE_FIELDS = {
|
|
|
85
85
|
from: z.string().optional(),
|
|
86
86
|
messageId: z.string().optional(),
|
|
87
87
|
messageType: z.string().optional(),
|
|
88
|
-
messageTypeId: z.
|
|
88
|
+
messageTypeId: z.number().optional(),
|
|
89
89
|
messageTypeString: z.string().optional(),
|
|
90
90
|
provider: z.string().optional(),
|
|
91
91
|
source: z.string().optional(),
|
package/src/meta-ads/events.ts
CHANGED
|
@@ -102,6 +102,122 @@ export const META_ADS_LEAD_CREATED_EVENT_SCHEMA = z.object({
|
|
|
102
102
|
pageId: META_ADS_ID_SCHEMA,
|
|
103
103
|
})
|
|
104
104
|
|
|
105
|
+
const META_ADS_MESSAGE_ATTACHMENT_SCHEMA = z.object({
|
|
106
|
+
/** Provider attachment payload, such as `url`, `title`, or `stickerId`. */
|
|
107
|
+
payload: z.record(z.string(), encodableSchema).optional(),
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Provider attachment type, such as `image`, `video`, `ig_reel`, or
|
|
111
|
+
* `story_mention`.
|
|
112
|
+
*/
|
|
113
|
+
type: z.string(),
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const META_ADS_MESSAGE_REFERRAL_SCHEMA = z.object({
|
|
117
|
+
adId: z.string().optional(),
|
|
118
|
+
adsContextData: z
|
|
119
|
+
.object({
|
|
120
|
+
adTitle: z.string().optional(),
|
|
121
|
+
flowId: z.string().optional(),
|
|
122
|
+
photoUrl: z.string().optional(),
|
|
123
|
+
postId: z.string().optional(),
|
|
124
|
+
productId: z.string().optional(),
|
|
125
|
+
videoUrl: z.string().optional(),
|
|
126
|
+
})
|
|
127
|
+
.optional(),
|
|
128
|
+
ref: z.string().optional(),
|
|
129
|
+
source: z.string().optional(),
|
|
130
|
+
type: z.string().optional(),
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const META_ADS_RECEIVED_MESSAGE_BASE_SCHEMA = z.object({
|
|
134
|
+
attachments: META_ADS_MESSAGE_ATTACHMENT_SCHEMA.array(),
|
|
135
|
+
messageId: META_ADS_ID_SCHEMA,
|
|
136
|
+
quickReplyPayload: z.string().optional(),
|
|
137
|
+
recipientId: META_ADS_ID_SCHEMA,
|
|
138
|
+
referral: META_ADS_MESSAGE_REFERRAL_SCHEMA.optional(),
|
|
139
|
+
senderId: META_ADS_ID_SCHEMA,
|
|
140
|
+
text: z.string().optional(),
|
|
141
|
+
timestamp: z.number().int().nonnegative(),
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
export const META_ADS_PAGE_COMMENT_CREATED_EVENT_SCHEMA = z.object({
|
|
145
|
+
commentId: META_ADS_ID_SCHEMA,
|
|
146
|
+
createdTime: z.number().int().nonnegative(),
|
|
147
|
+
from: z.object({ id: z.string(), name: z.string().optional() }).optional(),
|
|
148
|
+
message: z.string().optional(),
|
|
149
|
+
pageId: META_ADS_ID_SCHEMA,
|
|
150
|
+
parentId: z.string().optional(),
|
|
151
|
+
photo: z.string().optional(),
|
|
152
|
+
post: z
|
|
153
|
+
.object({
|
|
154
|
+
id: z.string().optional(),
|
|
155
|
+
isPublished: z.boolean().optional(),
|
|
156
|
+
permalinkUrl: z.string().optional(),
|
|
157
|
+
promotionStatus: z.string().optional(),
|
|
158
|
+
statusType: z.string().optional(),
|
|
159
|
+
updatedTime: z.string().optional(),
|
|
160
|
+
})
|
|
161
|
+
.optional(),
|
|
162
|
+
postId: META_ADS_ID_SCHEMA,
|
|
163
|
+
video: z.string().optional(),
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
export const META_ADS_MESSENGER_MESSAGE_RECEIVED_EVENT_SCHEMA =
|
|
167
|
+
META_ADS_RECEIVED_MESSAGE_BASE_SCHEMA.extend({
|
|
168
|
+
pageId: META_ADS_ID_SCHEMA,
|
|
169
|
+
replyToMessageId: z.string().optional(),
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
export const META_ADS_INSTAGRAM_COMMENT_CREATED_EVENT_SCHEMA = z.object({
|
|
173
|
+
commentId: META_ADS_ID_SCHEMA,
|
|
174
|
+
from: z.object({ id: z.string(), username: z.string().optional() }),
|
|
175
|
+
instagramAccountId: META_ADS_ID_SCHEMA,
|
|
176
|
+
media: z.object({
|
|
177
|
+
adId: z.string().optional(),
|
|
178
|
+
adTitle: z.string().optional(),
|
|
179
|
+
id: META_ADS_ID_SCHEMA,
|
|
180
|
+
mediaProductType: z.string().optional(),
|
|
181
|
+
originalMediaId: z.string().optional(),
|
|
182
|
+
}),
|
|
183
|
+
occurredAt: z.number().int().nonnegative(),
|
|
184
|
+
parentId: z.string().optional(),
|
|
185
|
+
text: z.string().optional(),
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
export const META_ADS_INSTAGRAM_MENTION_CREATED_EVENT_SCHEMA = z.object({
|
|
189
|
+
commentId: z.string().optional(),
|
|
190
|
+
instagramAccountId: META_ADS_ID_SCHEMA,
|
|
191
|
+
kind: z.enum(["caption", "comment"]),
|
|
192
|
+
mediaId: META_ADS_ID_SCHEMA,
|
|
193
|
+
occurredAt: z.number().int().nonnegative(),
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
export const META_ADS_INSTAGRAM_MESSAGE_RECEIVED_EVENT_SCHEMA =
|
|
197
|
+
META_ADS_RECEIVED_MESSAGE_BASE_SCHEMA.extend({
|
|
198
|
+
instagramAccountId: META_ADS_ID_SCHEMA,
|
|
199
|
+
isUnsupported: z.boolean(),
|
|
200
|
+
replyTo: z
|
|
201
|
+
.object({
|
|
202
|
+
messageId: z.string().optional(),
|
|
203
|
+
story: z
|
|
204
|
+
.object({
|
|
205
|
+
id: z.string(),
|
|
206
|
+
linkStickerUrl: z.string().optional(),
|
|
207
|
+
url: z.string().optional(),
|
|
208
|
+
})
|
|
209
|
+
.optional(),
|
|
210
|
+
})
|
|
211
|
+
.optional(),
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
export const META_ADS_INSTAGRAM_MESSAGE_DELETED_EVENT_SCHEMA = z.object({
|
|
215
|
+
instagramAccountId: META_ADS_ID_SCHEMA,
|
|
216
|
+
messageId: META_ADS_ID_SCHEMA,
|
|
217
|
+
senderId: META_ADS_ID_SCHEMA,
|
|
218
|
+
timestamp: z.number().int().nonnegative(),
|
|
219
|
+
})
|
|
220
|
+
|
|
105
221
|
export const META_ADS_TRIGGER_CONFIG_SCHEMA = z.object({
|
|
106
222
|
account: z.string().optional(),
|
|
107
223
|
adAccountId: META_ADS_ID_SCHEMA,
|
|
@@ -112,9 +228,37 @@ export const META_ADS_LEAD_TRIGGER_CONFIG_SCHEMA = z.object({
|
|
|
112
228
|
pageId: META_ADS_ID_SCHEMA,
|
|
113
229
|
})
|
|
114
230
|
|
|
231
|
+
export const META_ADS_PAGE_TRIGGER_CONFIG_SCHEMA = z.object({
|
|
232
|
+
account: z.string().optional(),
|
|
233
|
+
pageId: META_ADS_ID_SCHEMA,
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
export const META_ADS_INSTAGRAM_TRIGGER_CONFIG_SCHEMA = z.object({
|
|
237
|
+
account: z.string().optional(),
|
|
238
|
+
instagramAccountId: META_ADS_ID_SCHEMA,
|
|
239
|
+
})
|
|
240
|
+
|
|
115
241
|
export interface NormalizedMetaAdsWebhook {
|
|
116
242
|
adAccountEvents: z.output<typeof META_ADS_EVENT_SCHEMA>[]
|
|
243
|
+
instagramCommentEvents: z.output<
|
|
244
|
+
typeof META_ADS_INSTAGRAM_COMMENT_CREATED_EVENT_SCHEMA
|
|
245
|
+
>[]
|
|
246
|
+
instagramMentionEvents: z.output<
|
|
247
|
+
typeof META_ADS_INSTAGRAM_MENTION_CREATED_EVENT_SCHEMA
|
|
248
|
+
>[]
|
|
249
|
+
instagramMessageDeletedEvents: z.output<
|
|
250
|
+
typeof META_ADS_INSTAGRAM_MESSAGE_DELETED_EVENT_SCHEMA
|
|
251
|
+
>[]
|
|
252
|
+
instagramMessageEvents: z.output<
|
|
253
|
+
typeof META_ADS_INSTAGRAM_MESSAGE_RECEIVED_EVENT_SCHEMA
|
|
254
|
+
>[]
|
|
117
255
|
leadEvents: z.output<typeof META_ADS_LEAD_CREATED_EVENT_SCHEMA>[]
|
|
256
|
+
messengerMessageEvents: z.output<
|
|
257
|
+
typeof META_ADS_MESSENGER_MESSAGE_RECEIVED_EVENT_SCHEMA
|
|
258
|
+
>[]
|
|
259
|
+
pageCommentEvents: z.output<
|
|
260
|
+
typeof META_ADS_PAGE_COMMENT_CREATED_EVENT_SCHEMA
|
|
261
|
+
>[]
|
|
118
262
|
}
|
|
119
263
|
|
|
120
264
|
export const metaAdsTriggerContracts = {
|
|
@@ -154,6 +298,22 @@ export const metaAdsTriggerContracts = {
|
|
|
154
298
|
configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
|
|
155
299
|
eventSchema: META_ADS_SUBSCRIPTION_EVENT_SCHEMA,
|
|
156
300
|
},
|
|
301
|
+
"meta-ads.instagramComment.created": {
|
|
302
|
+
configSchema: META_ADS_INSTAGRAM_TRIGGER_CONFIG_SCHEMA,
|
|
303
|
+
eventSchema: META_ADS_INSTAGRAM_COMMENT_CREATED_EVENT_SCHEMA,
|
|
304
|
+
},
|
|
305
|
+
"meta-ads.instagramMention.created": {
|
|
306
|
+
configSchema: META_ADS_INSTAGRAM_TRIGGER_CONFIG_SCHEMA,
|
|
307
|
+
eventSchema: META_ADS_INSTAGRAM_MENTION_CREATED_EVENT_SCHEMA,
|
|
308
|
+
},
|
|
309
|
+
"meta-ads.instagramMessage.deleted": {
|
|
310
|
+
configSchema: META_ADS_INSTAGRAM_TRIGGER_CONFIG_SCHEMA,
|
|
311
|
+
eventSchema: META_ADS_INSTAGRAM_MESSAGE_DELETED_EVENT_SCHEMA,
|
|
312
|
+
},
|
|
313
|
+
"meta-ads.instagramMessage.received": {
|
|
314
|
+
configSchema: META_ADS_INSTAGRAM_TRIGGER_CONFIG_SCHEMA,
|
|
315
|
+
eventSchema: META_ADS_INSTAGRAM_MESSAGE_RECEIVED_EVENT_SCHEMA,
|
|
316
|
+
},
|
|
157
317
|
"meta-ads.insights.updated": {
|
|
158
318
|
configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
|
|
159
319
|
eventSchema: META_ADS_SUBSCRIPTION_EVENT_SCHEMA,
|
|
@@ -162,6 +322,10 @@ export const metaAdsTriggerContracts = {
|
|
|
162
322
|
configSchema: META_ADS_LEAD_TRIGGER_CONFIG_SCHEMA,
|
|
163
323
|
eventSchema: META_ADS_LEAD_CREATED_EVENT_SCHEMA,
|
|
164
324
|
},
|
|
325
|
+
"meta-ads.messengerMessage.received": {
|
|
326
|
+
configSchema: META_ADS_PAGE_TRIGGER_CONFIG_SCHEMA,
|
|
327
|
+
eventSchema: META_ADS_MESSENGER_MESSAGE_RECEIVED_EVENT_SCHEMA,
|
|
328
|
+
},
|
|
165
329
|
"meta-ads.object.created": {
|
|
166
330
|
configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
|
|
167
331
|
eventSchema: META_ADS_SUBSCRIPTION_EVENT_SCHEMA,
|
|
@@ -170,10 +334,94 @@ export const metaAdsTriggerContracts = {
|
|
|
170
334
|
configSchema: META_ADS_TRIGGER_CONFIG_SCHEMA,
|
|
171
335
|
eventSchema: META_ADS_SUBSCRIPTION_EVENT_SCHEMA,
|
|
172
336
|
},
|
|
337
|
+
"meta-ads.pageComment.created": {
|
|
338
|
+
configSchema: META_ADS_PAGE_TRIGGER_CONFIG_SCHEMA,
|
|
339
|
+
eventSchema: META_ADS_PAGE_COMMENT_CREATED_EVENT_SCHEMA,
|
|
340
|
+
},
|
|
173
341
|
} as const
|
|
174
342
|
|
|
343
|
+
const LEADGEN_VALUE_SCHEMA = z.object({
|
|
344
|
+
ad_id: z.string().optional(),
|
|
345
|
+
adgroup_id: z.string().optional(),
|
|
346
|
+
created_time: z.number().int().nonnegative(),
|
|
347
|
+
form_id: z.string(),
|
|
348
|
+
leadgen_id: z.string(),
|
|
349
|
+
page_id: z.string(),
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
const FEED_COMMENT_ADD_SCHEMA = z.object({
|
|
353
|
+
item: z.literal("comment"),
|
|
354
|
+
verb: z.literal("add"),
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
const FEED_COMMENT_VALUE_SCHEMA =
|
|
358
|
+
META_ADS_PAGE_COMMENT_CREATED_EVENT_SCHEMA.omit({ pageId: true }).extend({
|
|
359
|
+
createdTime: z.union([
|
|
360
|
+
META_ADS_PAGE_COMMENT_CREATED_EVENT_SCHEMA.shape.createdTime,
|
|
361
|
+
z
|
|
362
|
+
.string()
|
|
363
|
+
.transform((value) => Math.floor(Date.parse(value) / 1_000))
|
|
364
|
+
.pipe(META_ADS_PAGE_COMMENT_CREATED_EVENT_SCHEMA.shape.createdTime),
|
|
365
|
+
]),
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
const INSTAGRAM_COMMENT_VALUE_SCHEMA = z.object({
|
|
369
|
+
commentId: z.string().optional(),
|
|
370
|
+
from: META_ADS_INSTAGRAM_COMMENT_CREATED_EVENT_SCHEMA.shape.from.extend({
|
|
371
|
+
selfIgScopedId: z.string().optional(),
|
|
372
|
+
}),
|
|
373
|
+
id: z.string().optional(),
|
|
374
|
+
media: META_ADS_INSTAGRAM_COMMENT_CREATED_EVENT_SCHEMA.shape.media,
|
|
375
|
+
parentId: z.string().optional(),
|
|
376
|
+
text: z.string().optional(),
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
const INSTAGRAM_MENTION_VALUE_SCHEMA = z.object({
|
|
380
|
+
commentId: z.string().optional(),
|
|
381
|
+
mediaId: META_ADS_ID_SCHEMA,
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
const MESSAGING_EVENT_SCHEMA = z.object({
|
|
385
|
+
message: z.object({
|
|
386
|
+
attachments: META_ADS_MESSAGE_ATTACHMENT_SCHEMA.extend({
|
|
387
|
+
payload: META_ADS_MESSAGE_ATTACHMENT_SCHEMA.shape.payload.nullable(),
|
|
388
|
+
})
|
|
389
|
+
.array()
|
|
390
|
+
.optional(),
|
|
391
|
+
isDeleted: z.boolean().optional(),
|
|
392
|
+
isEcho: z.boolean().optional(),
|
|
393
|
+
isUnsupported: z.boolean().optional(),
|
|
394
|
+
mid: META_ADS_ID_SCHEMA,
|
|
395
|
+
quickReply: z.object({ payload: z.string() }).optional(),
|
|
396
|
+
referral: META_ADS_MESSAGE_REFERRAL_SCHEMA.optional(),
|
|
397
|
+
replyTo: z
|
|
398
|
+
.object({
|
|
399
|
+
mid: z.string().optional(),
|
|
400
|
+
story: z
|
|
401
|
+
.object({
|
|
402
|
+
id: z.string(),
|
|
403
|
+
linkStickerUrl: z.string().optional(),
|
|
404
|
+
url: z.string().optional(),
|
|
405
|
+
})
|
|
406
|
+
.optional(),
|
|
407
|
+
})
|
|
408
|
+
.optional(),
|
|
409
|
+
text: z.string().optional(),
|
|
410
|
+
}),
|
|
411
|
+
recipient: z.object({ id: META_ADS_ID_SCHEMA }),
|
|
412
|
+
sender: z.object({ id: META_ADS_ID_SCHEMA }),
|
|
413
|
+
timestamp: z.number().int().nonnegative(),
|
|
414
|
+
})
|
|
415
|
+
|
|
175
416
|
/**
|
|
176
|
-
* Normalizes Meta's Page and ad-account envelopes into stable
|
|
417
|
+
* Normalizes Meta's Page, Instagram, and ad-account envelopes into stable
|
|
418
|
+
* public events.
|
|
419
|
+
*
|
|
420
|
+
* Classification happens here: only new comments, inbound messages, and
|
|
421
|
+
* mentions become semantic events. The Page's own comments, Instagram's own
|
|
422
|
+
* comments and message echoes are dropped, and unsent Instagram messages become
|
|
423
|
+
* deletion events. Malformed comment, mention, and message items are dropped so
|
|
424
|
+
* they can't reject a delivery that also carries leads.
|
|
177
425
|
*
|
|
178
426
|
* @param input - Untrusted provider webhook envelope.
|
|
179
427
|
*/
|
|
@@ -181,38 +429,231 @@ export function normalizeMetaAdsWebhook(
|
|
|
181
429
|
input: unknown,
|
|
182
430
|
): NormalizedMetaAdsWebhook {
|
|
183
431
|
const envelope = META_ADS_WEBHOOK_ENVELOPE_SCHEMA.parse(input)
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
432
|
+
const entries = envelope.entry.map((entry) => ({
|
|
433
|
+
...entry,
|
|
434
|
+
changes: entry.changes ?? [],
|
|
435
|
+
messaging: entry.messaging ?? [],
|
|
436
|
+
}))
|
|
437
|
+
const pageEntries = envelope.object === "page" ? entries : []
|
|
438
|
+
const instagramEntries = envelope.object === "instagram" ? entries : []
|
|
439
|
+
return {
|
|
440
|
+
adAccountEvents:
|
|
441
|
+
envelope.object === "ad_account"
|
|
442
|
+
? entries.flatMap((entry) =>
|
|
443
|
+
entry.changes.map((change) => ({
|
|
444
|
+
accountId: entry.id,
|
|
445
|
+
field: change.field,
|
|
446
|
+
occurredAt: entry.time,
|
|
447
|
+
value: encodableSchema.parse(fromMeta(change.value)),
|
|
448
|
+
})),
|
|
449
|
+
)
|
|
450
|
+
: [],
|
|
451
|
+
instagramCommentEvents: instagramEntries.flatMap((entry) =>
|
|
452
|
+
entry.changes
|
|
453
|
+
.filter(({ field }) => field === "comments")
|
|
454
|
+
.flatMap(({ value }) => parseInstagramComment(entry, value) ?? []),
|
|
455
|
+
),
|
|
456
|
+
instagramMentionEvents: instagramEntries.flatMap((entry) =>
|
|
457
|
+
entry.changes
|
|
458
|
+
.filter(({ field }) => field === "mentions")
|
|
459
|
+
.flatMap(({ value }) => parseInstagramMention(entry, value) ?? []),
|
|
460
|
+
),
|
|
461
|
+
instagramMessageDeletedEvents: instagramEntries.flatMap((entry) =>
|
|
462
|
+
entry.messaging.flatMap((event) => {
|
|
463
|
+
const message = parseReceivedMessage(event)
|
|
464
|
+
if (!message?.isDeleted || message.isEcho) return []
|
|
465
|
+
return [
|
|
466
|
+
{
|
|
467
|
+
instagramAccountId: message.common.recipientId,
|
|
468
|
+
messageId: message.common.messageId,
|
|
469
|
+
senderId: message.common.senderId,
|
|
470
|
+
timestamp: message.common.timestamp,
|
|
471
|
+
},
|
|
472
|
+
]
|
|
473
|
+
}),
|
|
474
|
+
),
|
|
475
|
+
instagramMessageEvents: instagramEntries.flatMap((entry) =>
|
|
476
|
+
entry.messaging.flatMap((event) => {
|
|
477
|
+
const message = parseReceivedMessage(event)
|
|
478
|
+
if (!message || message.isEcho || message.isDeleted) return []
|
|
479
|
+
return [
|
|
480
|
+
{
|
|
481
|
+
...message.common,
|
|
482
|
+
instagramAccountId: message.common.recipientId,
|
|
483
|
+
isUnsupported: message.isUnsupported,
|
|
484
|
+
...(message.replyTo && {
|
|
485
|
+
replyTo: {
|
|
486
|
+
messageId: message.replyTo.mid,
|
|
487
|
+
story: message.replyTo.story,
|
|
488
|
+
},
|
|
489
|
+
}),
|
|
490
|
+
},
|
|
491
|
+
]
|
|
492
|
+
}),
|
|
493
|
+
),
|
|
494
|
+
leadEvents: pageEntries.flatMap((entry) =>
|
|
495
|
+
entry.changes
|
|
496
|
+
.filter(({ field }) => field === "leadgen")
|
|
497
|
+
.map(({ value }) => {
|
|
498
|
+
const lead = LEADGEN_VALUE_SCHEMA.parse(value)
|
|
499
|
+
return {
|
|
500
|
+
adId: lead.ad_id,
|
|
501
|
+
adgroupId: lead.adgroup_id,
|
|
502
|
+
createdTime: lead.created_time,
|
|
503
|
+
formId: lead.form_id,
|
|
504
|
+
leadgenId: lead.leadgen_id,
|
|
505
|
+
pageId: lead.page_id,
|
|
506
|
+
}
|
|
507
|
+
}),
|
|
508
|
+
),
|
|
509
|
+
messengerMessageEvents: pageEntries.flatMap((entry) =>
|
|
510
|
+
entry.messaging.flatMap((event) => {
|
|
511
|
+
const message = parseReceivedMessage(event)
|
|
512
|
+
if (!message || message.isEcho) return []
|
|
513
|
+
return [
|
|
514
|
+
{
|
|
515
|
+
...message.common,
|
|
516
|
+
pageId: entry.id,
|
|
517
|
+
replyToMessageId: message.replyTo?.mid,
|
|
518
|
+
},
|
|
519
|
+
]
|
|
520
|
+
}),
|
|
521
|
+
),
|
|
522
|
+
pageCommentEvents: pageEntries.flatMap((entry) =>
|
|
523
|
+
entry.changes.flatMap(({ field, value }) => {
|
|
524
|
+
if (
|
|
525
|
+
field !== "feed" ||
|
|
526
|
+
!FEED_COMMENT_ADD_SCHEMA.safeParse(value).success
|
|
527
|
+
)
|
|
528
|
+
return []
|
|
529
|
+
const comment = parsePageComment(entry.id, value)
|
|
530
|
+
return !comment || comment.from?.id === entry.id ? [] : [comment]
|
|
531
|
+
}),
|
|
532
|
+
),
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Normalizes one Page `feed` comment-add change, or returns `undefined` when
|
|
538
|
+
* the item is malformed so one bad item can't reject a whole delivery.
|
|
539
|
+
*
|
|
540
|
+
* Meta documents `created_time` only as a datetime, so both Unix seconds and
|
|
541
|
+
* ISO strings are accepted. Top-level comments may carry the post ID as
|
|
542
|
+
* `parent_id`; only a distinct parent identifies a reply.
|
|
543
|
+
*
|
|
544
|
+
* @param pageId - Page that owns the feed.
|
|
545
|
+
* @param input - Raw feed change value.
|
|
546
|
+
*/
|
|
547
|
+
function parsePageComment(
|
|
548
|
+
pageId: string,
|
|
549
|
+
input: unknown,
|
|
550
|
+
): z.output<typeof META_ADS_PAGE_COMMENT_CREATED_EVENT_SCHEMA> | undefined {
|
|
551
|
+
const parsed = FEED_COMMENT_VALUE_SCHEMA.safeParse(fromMeta(input))
|
|
552
|
+
if (!parsed.success) return undefined
|
|
553
|
+
const { parentId, ...value } = parsed.data
|
|
554
|
+
return {
|
|
555
|
+
...value,
|
|
556
|
+
pageId,
|
|
557
|
+
...(parentId && parentId !== value.postId && { parentId }),
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Normalizes one Instagram `comments` change, or returns `undefined` when the
|
|
563
|
+
* item is malformed or the account wrote it.
|
|
564
|
+
*
|
|
565
|
+
* Meta's reference schema names the comment `id`, while its Facebook Login
|
|
566
|
+
* samples name it `comment_id`; both are accepted. The account's own comments
|
|
567
|
+
* match either the entry ID or the commenter's `self_ig_scoped_id`; dropping
|
|
568
|
+
* them keeps reply automations from triggering themselves.
|
|
569
|
+
*
|
|
570
|
+
* @param entry - Instagram webhook entry.
|
|
571
|
+
* @param entry.id - Instagram professional account ID.
|
|
572
|
+
* @param entry.time - Delivery time in Unix seconds.
|
|
573
|
+
* @param input - Raw comments change value.
|
|
574
|
+
*/
|
|
575
|
+
function parseInstagramComment(
|
|
576
|
+
entry: { id: string; time: number },
|
|
577
|
+
input: unknown,
|
|
578
|
+
):
|
|
579
|
+
| z.output<typeof META_ADS_INSTAGRAM_COMMENT_CREATED_EVENT_SCHEMA>
|
|
580
|
+
| undefined {
|
|
581
|
+
const parsed = INSTAGRAM_COMMENT_VALUE_SCHEMA.safeParse(fromMeta(input))
|
|
582
|
+
if (!parsed.success) return undefined
|
|
583
|
+
const {
|
|
584
|
+
commentId,
|
|
585
|
+
from: { selfIgScopedId, ...from },
|
|
586
|
+
id,
|
|
587
|
+
...value
|
|
588
|
+
} = parsed.data
|
|
589
|
+
const resolvedId = id ?? commentId
|
|
590
|
+
if (!resolvedId || from.id === entry.id || from.id === selfIgScopedId) {
|
|
591
|
+
return undefined
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
...value,
|
|
595
|
+
commentId: resolvedId,
|
|
596
|
+
from,
|
|
597
|
+
instagramAccountId: entry.id,
|
|
598
|
+
occurredAt: entry.time,
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Normalizes one Instagram `mentions` change into a caption or comment mention,
|
|
604
|
+
* or returns `undefined` when the item is malformed.
|
|
605
|
+
*
|
|
606
|
+
* @param entry - Instagram webhook entry.
|
|
607
|
+
* @param entry.id - Instagram professional account ID.
|
|
608
|
+
* @param entry.time - Delivery time in Unix seconds.
|
|
609
|
+
* @param input - Raw mentions change value.
|
|
610
|
+
*/
|
|
611
|
+
function parseInstagramMention(
|
|
612
|
+
entry: { id: string; time: number },
|
|
613
|
+
input: unknown,
|
|
614
|
+
):
|
|
615
|
+
| z.output<typeof META_ADS_INSTAGRAM_MENTION_CREATED_EVENT_SCHEMA>
|
|
616
|
+
| undefined {
|
|
617
|
+
const parsed = INSTAGRAM_MENTION_VALUE_SCHEMA.safeParse(fromMeta(input))
|
|
618
|
+
if (!parsed.success) return undefined
|
|
619
|
+
return {
|
|
620
|
+
...parsed.data,
|
|
621
|
+
instagramAccountId: entry.id,
|
|
622
|
+
kind: parsed.data.commentId ? "comment" : "caption",
|
|
623
|
+
occurredAt: entry.time,
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Parses one Messenger or Instagram `messaging` event that carries a message.
|
|
629
|
+
*
|
|
630
|
+
* Returns `undefined` for other event kinds, such as reads, reactions,
|
|
631
|
+
* postbacks, referrals, and handoff `admin_text` notices without a message ID,
|
|
632
|
+
* and for malformed messages.
|
|
633
|
+
*
|
|
634
|
+
* @param input - Raw messaging event.
|
|
635
|
+
*/
|
|
636
|
+
function parseReceivedMessage(input: unknown) {
|
|
637
|
+
const parsed = MESSAGING_EVENT_SCHEMA.safeParse(fromMeta(input))
|
|
638
|
+
if (!parsed.success) return undefined
|
|
639
|
+
const { message, recipient, sender, timestamp } = parsed.data
|
|
640
|
+
return {
|
|
641
|
+
common: {
|
|
642
|
+
attachments: (message.attachments ?? []).map(({ payload, type }) => ({
|
|
643
|
+
type,
|
|
644
|
+
...(payload && { payload }),
|
|
645
|
+
})),
|
|
646
|
+
messageId: message.mid,
|
|
647
|
+
quickReplyPayload: message.quickReply?.payload,
|
|
648
|
+
recipientId: recipient.id,
|
|
649
|
+
referral: message.referral,
|
|
650
|
+
senderId: sender.id,
|
|
651
|
+
text: message.text,
|
|
652
|
+
timestamp,
|
|
653
|
+
},
|
|
654
|
+
isDeleted: message.isDeleted === true,
|
|
655
|
+
isEcho: message.isEcho === true,
|
|
656
|
+
isUnsupported: message.isUnsupported === true,
|
|
657
|
+
replyTo: message.replyTo,
|
|
216
658
|
}
|
|
217
|
-
return { adAccountEvents, leadEvents }
|
|
218
659
|
}
|
package/src/meta-ads/index.ts
CHANGED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as z from "zod"
|
|
2
|
+
import { getMetaAdsApi, type MetaAdsResolvedAccount } from "./api"
|
|
3
|
+
|
|
4
|
+
const PAGE_TOKEN_SCHEMA = z.object({ accessToken: z.string().min(1) })
|
|
5
|
+
const LINKED_PAGE_SCHEMA = z
|
|
6
|
+
.object({
|
|
7
|
+
accessToken: z.string().min(1),
|
|
8
|
+
id: z.string(),
|
|
9
|
+
instagramBusinessAccount: z.object({ id: z.string() }).optional(),
|
|
10
|
+
})
|
|
11
|
+
.array()
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates a Graph client authenticated as one Page.
|
|
15
|
+
*
|
|
16
|
+
* Page comment, messaging, conversation, and `subscribed_apps` endpoints need a
|
|
17
|
+
* Page access token. The token is derived from the connected user token on each
|
|
18
|
+
* call and never persisted.
|
|
19
|
+
*
|
|
20
|
+
* @param account - Resolved Meta Ads OAuth account.
|
|
21
|
+
* @param pageId - Facebook Page the connected identity manages.
|
|
22
|
+
*/
|
|
23
|
+
export async function getMetaAdsPageApi(
|
|
24
|
+
account: MetaAdsResolvedAccount,
|
|
25
|
+
pageId: string,
|
|
26
|
+
) {
|
|
27
|
+
const { accessToken } = await getMetaAdsApi(account).call(pageId, {
|
|
28
|
+
query: { fields: ["access_token"] },
|
|
29
|
+
responseSchema: PAGE_TOKEN_SCHEMA,
|
|
30
|
+
})
|
|
31
|
+
return pageClient(accessToken)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Finds the managed Facebook Page linked to one Instagram professional account
|
|
36
|
+
* and creates a Graph client authenticated as that Page.
|
|
37
|
+
*
|
|
38
|
+
* Instagram messaging with Facebook Login sends through the linked Page, and
|
|
39
|
+
* Instagram webhooks are enabled by installing the app on that Page.
|
|
40
|
+
*
|
|
41
|
+
* @param account - Resolved Meta Ads OAuth account.
|
|
42
|
+
* @param instagramAccountId - Instagram professional account ID.
|
|
43
|
+
* @throws When no managed Page links to the Instagram account.
|
|
44
|
+
*/
|
|
45
|
+
export async function getMetaAdsInstagramPageApi(
|
|
46
|
+
account: MetaAdsResolvedAccount,
|
|
47
|
+
instagramAccountId: string,
|
|
48
|
+
) {
|
|
49
|
+
const page = await findLinkedPage(account, instagramAccountId)
|
|
50
|
+
if (!page) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Instagram account ${instagramAccountId} is not linked to a Facebook Page this Meta Ads account manages.`,
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
return { api: pageClient(page.accessToken), pageId: page.id }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Walks the connected identity's Pages until one links to the Instagram
|
|
60
|
+
* account.
|
|
61
|
+
*
|
|
62
|
+
* @param account - Resolved Meta Ads OAuth account.
|
|
63
|
+
* @param instagramAccountId - Instagram professional account ID.
|
|
64
|
+
* @param after - Cursor for the next `me/accounts` page.
|
|
65
|
+
*/
|
|
66
|
+
async function findLinkedPage(
|
|
67
|
+
account: MetaAdsResolvedAccount,
|
|
68
|
+
instagramAccountId: string,
|
|
69
|
+
after?: string,
|
|
70
|
+
): Promise<z.output<typeof LINKED_PAGE_SCHEMA>[number] | undefined> {
|
|
71
|
+
const { items, pageInfo } = await getMetaAdsApi(account).page("me/accounts", {
|
|
72
|
+
query: {
|
|
73
|
+
after,
|
|
74
|
+
fields: ["id", "access_token", "instagram_business_account"],
|
|
75
|
+
limit: 100,
|
|
76
|
+
},
|
|
77
|
+
responseSchema: LINKED_PAGE_SCHEMA,
|
|
78
|
+
})
|
|
79
|
+
const page = items.find(
|
|
80
|
+
({ instagramBusinessAccount }) =>
|
|
81
|
+
instagramBusinessAccount?.id === instagramAccountId,
|
|
82
|
+
)
|
|
83
|
+
if (page || !pageInfo?.next || !pageInfo.after) return page
|
|
84
|
+
return findLinkedPage(account, instagramAccountId, pageInfo.after)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Wraps one Page access token in the shared Graph client.
|
|
89
|
+
*
|
|
90
|
+
* @param accessToken - Page access token.
|
|
91
|
+
*/
|
|
92
|
+
function pageClient(accessToken: string) {
|
|
93
|
+
return getMetaAdsApi({
|
|
94
|
+
connectionMethodId: "oauth",
|
|
95
|
+
secret: { accessToken, expiresAt: null, tokenType: "bearer" },
|
|
96
|
+
serviceId: "meta-ads",
|
|
97
|
+
})
|
|
98
|
+
}
|