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