mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-23 11:44:06 -08:00
feat(Facebook Lead Ads Trigger Node): Add Facebook Lead Ads Trigger Node (#7113)
Github issue / Community forum post (link here to close automatically): https://community.n8n.io/t/facebook-lead-ads-integration/4590/19 --------- Co-authored-by: Marcus <marcus@n8n.io>
This commit is contained in:
parent
647372be27
commit
ac814a9c61
|
@ -29,6 +29,21 @@ function findReferencedMethods(obj, refs = {}, latestName = '') {
|
|||
return refs;
|
||||
}
|
||||
|
||||
function addWebhookLifecycle(nodeType) {
|
||||
if (nodeType.description.webhooks) {
|
||||
nodeType.description.webhooks = nodeType.description.webhooks.map((webhook) => {
|
||||
const webhookMethods =
|
||||
nodeType?.webhookMethods?.[webhook.name] ?? nodeType?.webhookMethods?.default;
|
||||
webhook.hasLifecycleMethods = Boolean(
|
||||
webhookMethods?.checkExists && webhookMethods?.create && webhookMethods?.delete,
|
||||
);
|
||||
return webhook;
|
||||
});
|
||||
}
|
||||
|
||||
return nodeType;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const loader = new PackageDirectoryLoader(packageDir);
|
||||
await loader.loadAll();
|
||||
|
@ -60,6 +75,7 @@ function findReferencedMethods(obj, refs = {}, latestName = '') {
|
|||
.map((data) => {
|
||||
const nodeType = NodeHelpers.getVersionedNodeType(data.type);
|
||||
NodeHelpers.applySpecialNodeParameters(nodeType);
|
||||
addWebhookLifecycle(nodeType);
|
||||
return data.type;
|
||||
})
|
||||
.flatMap((nodeData) => {
|
||||
|
|
|
@ -61,12 +61,12 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { INodeTypeDescription, IWebhookDescription } from 'n8n-workflow';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import { useToast } from '@/composables';
|
||||
import { FORM_TRIGGER_NODE_TYPE, OPEN_URL_PANEL_TRIGGER_NODE_TYPES } from '@/constants';
|
||||
import { copyPaste } from '@/mixins/copyPaste';
|
||||
import { useToast } from '@/composables';
|
||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
|
||||
export default defineComponent({
|
||||
|
@ -94,7 +94,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
return (this.nodeType as INodeTypeDescription).webhooks!.filter(
|
||||
(webhookData) => webhookData.restartWebhook !== true,
|
||||
(webhookData) => webhookData.restartWebhook !== true && !webhookData.hasLifecycleMethods,
|
||||
);
|
||||
},
|
||||
baseText() {
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export class FacebookLeadAdsOAuth2Api implements ICredentialType {
|
||||
name = 'facebookLeadAdsOAuth2Api';
|
||||
|
||||
extends = ['oAuth2Api'];
|
||||
|
||||
displayName = 'Facebook Lead Ads OAuth2 API';
|
||||
|
||||
documentationUrl = 'facebookleadads';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Grant Type',
|
||||
name: 'grantType',
|
||||
type: 'hidden',
|
||||
default: 'authorizationCode',
|
||||
},
|
||||
{
|
||||
displayName: 'Authorization URL',
|
||||
name: 'authUrl',
|
||||
type: 'hidden',
|
||||
default: 'https://www.facebook.com/v17.0/dialog/oauth',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Access Token URL',
|
||||
name: 'accessTokenUrl',
|
||||
type: 'hidden',
|
||||
default: 'https://graph.facebook.com/v17.0/oauth/access_token',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Scope',
|
||||
name: 'scope',
|
||||
type: 'hidden',
|
||||
default: 'leads_retrieval pages_show_list pages_manage_metadata pages_manage_ads',
|
||||
},
|
||||
{
|
||||
displayName: 'Auth URI Query Parameters',
|
||||
name: 'authQueryParameters',
|
||||
type: 'hidden',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'hidden',
|
||||
default: 'header',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -1,15 +1,16 @@
|
|||
import { createHmac } from 'crypto';
|
||||
import type {
|
||||
IHookFunctions,
|
||||
IWebhookFunctions,
|
||||
IDataObject,
|
||||
IHookFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
IWebhookFunctions,
|
||||
IWebhookResponseData,
|
||||
JsonObject,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
|
@ -17,7 +18,7 @@ import { snakeCase } from 'change-case';
|
|||
|
||||
import { facebookApiRequest, getAllFields, getFields } from './GenericFunctions';
|
||||
|
||||
import { createHmac } from 'crypto';
|
||||
import type { FacebookWebhookSubscription } from './types';
|
||||
|
||||
export class FacebookTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
|
@ -177,18 +178,27 @@ export class FacebookTrigger implements INodeType {
|
|||
const object = this.getNodeParameter('object') as string;
|
||||
const appId = this.getNodeParameter('appId') as string;
|
||||
|
||||
const { data } = await facebookApiRequest.call(this, 'GET', `/${appId}/subscriptions`, {});
|
||||
const { data } = (await facebookApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/${appId}/subscriptions`,
|
||||
{},
|
||||
)) as { data: FacebookWebhookSubscription[] };
|
||||
|
||||
for (const webhook of data) {
|
||||
if (
|
||||
webhook.target === webhookUrl &&
|
||||
webhook.object === object &&
|
||||
webhook.status === true
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const subscription = data.find((webhook) => webhook.object === object && webhook.status);
|
||||
|
||||
if (!subscription) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
if (subscription.callback_url !== webhookUrl) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`The Facebook App ID ${appId} already has a webhook subscription. Delete it or use another App before executing the trigger. Due to Facebook API limitations, you can have just one trigger per App.`,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
async create(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
|
|
29
packages/nodes-base/nodes/Facebook/types.ts
Normal file
29
packages/nodes-base/nodes/Facebook/types.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
export interface FacebookEvent {
|
||||
object: string;
|
||||
entry: FacebookPageEventEntry[];
|
||||
}
|
||||
|
||||
export interface FacebookPageEventEntry {
|
||||
id: string;
|
||||
time: number;
|
||||
changes: [
|
||||
{
|
||||
field: 'leadgen';
|
||||
value: {
|
||||
ad_id: string;
|
||||
form_id: string;
|
||||
leadgen_id: string;
|
||||
created_time: number;
|
||||
page_id: string;
|
||||
adgroup_id: string;
|
||||
};
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export interface FacebookWebhookSubscription {
|
||||
object: string;
|
||||
callback_url: string;
|
||||
fields: string[];
|
||||
status: boolean;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"node": "n8n-nodes-base.facebookLeadAdsTrigger",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Marketing & Content"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/credentials/facebookleadads/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.facebookleadadstrigger/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,311 @@
|
|||
import { createHmac } from 'crypto';
|
||||
import {
|
||||
NodeOperationError,
|
||||
type IDataObject,
|
||||
type IHookFunctions,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
type IWebhookFunctions,
|
||||
type IWebhookResponseData,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
appWebhookSubscriptionCreate,
|
||||
appWebhookSubscriptionDelete,
|
||||
appWebhookSubscriptionList,
|
||||
facebookEntityDetail,
|
||||
installAppOnPage,
|
||||
} from './GenericFunctions';
|
||||
import { listSearch } from './methods';
|
||||
import type { FacebookForm, FacebookFormLeadData, FacebookPageEvent } from './types';
|
||||
|
||||
export class FacebookLeadAdsTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Facebook Lead Ads Trigger',
|
||||
name: 'facebookLeadAdsTrigger',
|
||||
icon: 'file:facebook.svg',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["event"]}}',
|
||||
description: 'Handle Facebook Lead Ads events via webhooks',
|
||||
defaults: {
|
||||
name: 'Facebook Lead Ads Trigger',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'facebookLeadAdsOAuth2Api',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
name: 'setup',
|
||||
httpMethod: 'GET',
|
||||
responseMode: 'onReceived',
|
||||
path: 'webhook',
|
||||
},
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'POST',
|
||||
responseMode: 'onReceived',
|
||||
path: 'webhook',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName:
|
||||
'Due to Facebook API limitations, you can use just one Facebook Lead Ads trigger for each Facebook App',
|
||||
name: 'facebookLeadAdsNotice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Event',
|
||||
name: 'event',
|
||||
type: 'options',
|
||||
required: true,
|
||||
default: 'newLead',
|
||||
options: [
|
||||
{
|
||||
name: 'New Lead',
|
||||
value: 'newLead',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Page',
|
||||
name: 'page',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
required: true,
|
||||
description: 'The page linked to the form for retrieving new leads',
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchListMethod: 'pageList',
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '121637951029080',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Form',
|
||||
name: 'form',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
required: true,
|
||||
description: 'The form to monitor for fetching lead details upon submission',
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchListMethod: 'formList',
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '121637951029080',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Simplify Output',
|
||||
name: 'simplifyOutput',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to return a simplified version of the webhook event instead of all fields',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
listSearch,
|
||||
};
|
||||
|
||||
webhookMethods = {
|
||||
default: {
|
||||
async checkExists(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookUrl = this.getNodeWebhookUrl('default') as string;
|
||||
const credentials = await this.getCredentials('facebookLeadAdsOAuth2Api');
|
||||
const appId = credentials.clientId as string;
|
||||
|
||||
const webhooks = await appWebhookSubscriptionList.call(this, appId);
|
||||
|
||||
const subscription = webhooks.find(
|
||||
(webhook) =>
|
||||
webhook.object === 'page' &&
|
||||
webhook.fields.find((field) => field.name === 'leadgen') &&
|
||||
webhook.active,
|
||||
);
|
||||
|
||||
if (!subscription) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (subscription.callback_url !== webhookUrl) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`The Facebook App ID ${appId} already has a webhook subscription. Delete it or use another App before executing the trigger. Due to Facebook API limitations, you can have just one trigger per App.`,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
async create(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookUrl = this.getNodeWebhookUrl('default') as string;
|
||||
const credentials = await this.getCredentials('facebookLeadAdsOAuth2Api');
|
||||
const appId = credentials.clientId as string;
|
||||
const pageId = this.getNodeParameter('page', '', { extractValue: true }) as string;
|
||||
const verifyToken = this.getNode().id;
|
||||
|
||||
await appWebhookSubscriptionCreate.call(this, appId, {
|
||||
object: 'page',
|
||||
callback_url: webhookUrl,
|
||||
verify_token: verifyToken,
|
||||
fields: ['leadgen'],
|
||||
include_values: true,
|
||||
});
|
||||
|
||||
await installAppOnPage.call(this, pageId, 'leadgen');
|
||||
|
||||
return true;
|
||||
},
|
||||
async delete(this: IHookFunctions): Promise<boolean> {
|
||||
const credentials = await this.getCredentials('facebookLeadAdsOAuth2Api');
|
||||
const appId = credentials.clientId as string;
|
||||
|
||||
await appWebhookSubscriptionDelete.call(this, appId, 'page');
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||
const bodyData = this.getBodyData() as unknown as FacebookPageEvent;
|
||||
const query = this.getQueryData() as IDataObject;
|
||||
const res = this.getResponseObject();
|
||||
const req = this.getRequestObject();
|
||||
const headerData = this.getHeaderData() as IDataObject;
|
||||
const credentials = await this.getCredentials('facebookLeadAdsOAuth2Api');
|
||||
const pageId = this.getNodeParameter('page', '', { extractValue: true }) as string;
|
||||
const formId = this.getNodeParameter('form', '', { extractValue: true }) as string;
|
||||
|
||||
// Check if we're getting facebook's challenge request (https://developers.facebook.com/docs/graph-api/webhooks/getting-started)
|
||||
if (this.getWebhookName() === 'setup') {
|
||||
if (query['hub.challenge']) {
|
||||
if (this.getNode().id !== query['hub.verify_token']) {
|
||||
return {};
|
||||
}
|
||||
|
||||
res.status(200).send(query['hub.challenge']).end();
|
||||
|
||||
return { noWebhookResponse: true };
|
||||
}
|
||||
}
|
||||
|
||||
const computedSignature = createHmac('sha256', credentials.clientSecret as string)
|
||||
.update(req.rawBody)
|
||||
.digest('hex');
|
||||
if (headerData['x-hub-signature-256'] !== `sha256=${computedSignature}`) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (bodyData.object !== 'page') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const events = await Promise.all(
|
||||
bodyData.entry
|
||||
.map((entry) =>
|
||||
entry.changes
|
||||
.filter(
|
||||
(change) =>
|
||||
change.field === 'leadgen' &&
|
||||
change.value.page_id === pageId &&
|
||||
change.value.form_id === formId,
|
||||
)
|
||||
.map((change) => change.value),
|
||||
)
|
||||
.flat()
|
||||
.map(async (event) => {
|
||||
const [lead, form] = await Promise.all([
|
||||
facebookEntityDetail.call(
|
||||
this,
|
||||
event.leadgen_id,
|
||||
'field_data,created_time,ad_id,ad_name,adset_id,adset_name,form_id',
|
||||
) as Promise<FacebookFormLeadData>,
|
||||
facebookEntityDetail.call(
|
||||
this,
|
||||
event.form_id,
|
||||
'id,name,locale,status,page,questions',
|
||||
) as Promise<FacebookForm>,
|
||||
]);
|
||||
|
||||
const simplifyOutput = this.getNodeParameter('options.simplifyOutput', true) as boolean;
|
||||
|
||||
if (simplifyOutput) {
|
||||
return {
|
||||
id: lead.id,
|
||||
data: lead.field_data.reduce(
|
||||
(acc, field) => ({ ...acc, [field.name]: field.values[0] }),
|
||||
{},
|
||||
),
|
||||
form: {
|
||||
id: form.id,
|
||||
name: form.name,
|
||||
locale: form.locale,
|
||||
status: form.status,
|
||||
},
|
||||
ad: { id: lead.ad_id, name: lead.ad_name },
|
||||
adset: { id: lead.adset_id, name: lead.adset_name },
|
||||
page: form.page,
|
||||
created_time: lead.created_time,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: lead.id,
|
||||
field_data: lead.field_data,
|
||||
form,
|
||||
ad: { id: lead.ad_id, name: lead.ad_name },
|
||||
adset: { id: lead.adset_id, name: lead.adset_name },
|
||||
page: form.page,
|
||||
created_time: lead.created_time,
|
||||
event,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
if (events.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
workflowData: [this.helpers.returnJsonArray(events)],
|
||||
};
|
||||
}
|
||||
}
|
222
packages/nodes-base/nodes/FacebookLeadAds/GenericFunctions.ts
Normal file
222
packages/nodes-base/nodes/FacebookLeadAds/GenericFunctions.ts
Normal file
|
@ -0,0 +1,222 @@
|
|||
import type { OptionsWithUri } from 'request';
|
||||
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
IWebhookFunctions,
|
||||
JsonObject,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
import type {
|
||||
CreateFacebookAppWebhookSubscription,
|
||||
FacebookAppWebhookSubscription,
|
||||
FacebookAppWebhookSubscriptionsResponse,
|
||||
FacebookFormListResponse,
|
||||
FacebookPage,
|
||||
FacebookPageListResponse,
|
||||
} from './types';
|
||||
|
||||
export async function facebookApiRequest(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
|
||||
method: string,
|
||||
resource: string,
|
||||
body = {},
|
||||
qs: IDataObject = {},
|
||||
): Promise<any> {
|
||||
const options: OptionsWithUri = {
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
},
|
||||
method,
|
||||
qs,
|
||||
body,
|
||||
gzip: true,
|
||||
uri: `https://graph.facebook.com/v17.0${resource}`,
|
||||
json: true,
|
||||
};
|
||||
|
||||
try {
|
||||
return await this.helpers.requestOAuth2.call(this, 'facebookLeadAdsOAuth2Api', options);
|
||||
} catch (error) {
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject, {
|
||||
message: error?.error?.error?.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function appAccessTokenRead(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
|
||||
): Promise<{ access_token: string }> {
|
||||
const credentials = await this.getCredentials('facebookLeadAdsOAuth2Api');
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
headers: {
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
method: 'POST',
|
||||
form: {
|
||||
client_id: credentials.clientId,
|
||||
client_secret: credentials.clientSecret,
|
||||
grant_type: 'client_credentials',
|
||||
},
|
||||
uri: credentials.accessTokenUrl as string,
|
||||
json: true,
|
||||
};
|
||||
try {
|
||||
return await this.helpers.request(options);
|
||||
} catch (error) {
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||
}
|
||||
}
|
||||
|
||||
export async function facebookAppApiRequest(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
|
||||
method: string,
|
||||
resource: string,
|
||||
body?: { type: 'json'; payload: IDataObject } | { type: 'form'; payload: IDataObject },
|
||||
qs: IDataObject = {},
|
||||
): Promise<any> {
|
||||
const tokenResponse = await appAccessTokenRead.call(this);
|
||||
const appAccessToken = tokenResponse.access_token;
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
authorization: `Bearer ${appAccessToken}`,
|
||||
},
|
||||
method,
|
||||
qs,
|
||||
gzip: true,
|
||||
uri: `https://graph.facebook.com/v17.0${resource}`,
|
||||
json: true,
|
||||
};
|
||||
|
||||
if (body?.type === 'json') {
|
||||
options.body = body.payload;
|
||||
} else if (body?.type === 'form') {
|
||||
options.form = body.payload;
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.helpers.request(options);
|
||||
} catch (error) {
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||
}
|
||||
}
|
||||
|
||||
export async function appWebhookSubscriptionList(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
|
||||
appId: string,
|
||||
): Promise<FacebookAppWebhookSubscription[]> {
|
||||
const response = (await facebookAppApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/${appId}/subscriptions`,
|
||||
)) as FacebookAppWebhookSubscriptionsResponse;
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function appWebhookSubscriptionCreate(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
|
||||
appId: string,
|
||||
subscription: CreateFacebookAppWebhookSubscription,
|
||||
) {
|
||||
return facebookAppApiRequest.call(this, 'POST', `/${appId}/subscriptions`, {
|
||||
type: 'form',
|
||||
payload: { ...subscription },
|
||||
});
|
||||
}
|
||||
|
||||
export async function appWebhookSubscriptionDelete(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
|
||||
appId: string,
|
||||
object: string,
|
||||
) {
|
||||
return facebookAppApiRequest.call(this, 'DELETE', `/${appId}/subscriptions`, {
|
||||
type: 'form',
|
||||
payload: { object },
|
||||
});
|
||||
}
|
||||
|
||||
export async function facebookPageList(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
|
||||
cursor?: string,
|
||||
): Promise<FacebookPageListResponse> {
|
||||
const response = (await facebookApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
'/me/accounts',
|
||||
{},
|
||||
{ cursor, fields: 'id,name' },
|
||||
)) as FacebookPageListResponse;
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function facebookEntityDetail(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
|
||||
entityId: string,
|
||||
fields = 'id,name,access_token',
|
||||
): Promise<any> {
|
||||
return facebookApiRequest.call(this, 'GET', `/${entityId}`, {}, { fields });
|
||||
}
|
||||
|
||||
export async function facebookPageApiRequest(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
|
||||
method: string,
|
||||
resource: string,
|
||||
body = {},
|
||||
qs: IDataObject = {},
|
||||
): Promise<any> {
|
||||
const pageId = this.getNodeParameter('page', '', { extractValue: true }) as string;
|
||||
const page = (await facebookEntityDetail.call(this, pageId)) as FacebookPage;
|
||||
const pageAccessToken = page.access_token;
|
||||
const options: OptionsWithUri = {
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
authorization: `Bearer ${pageAccessToken}`,
|
||||
},
|
||||
method,
|
||||
qs,
|
||||
body,
|
||||
gzip: true,
|
||||
uri: `https://graph.facebook.com/v17.0${resource}`,
|
||||
json: true,
|
||||
};
|
||||
|
||||
try {
|
||||
return await this.helpers.request.call(this, options);
|
||||
} catch (error) {
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||
}
|
||||
}
|
||||
|
||||
export async function installAppOnPage(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
|
||||
pageId: string,
|
||||
fields: string,
|
||||
) {
|
||||
return facebookPageApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/${pageId}/subscribed_apps`,
|
||||
{},
|
||||
{ subscribed_fields: fields },
|
||||
);
|
||||
}
|
||||
|
||||
export async function facebookFormList(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
|
||||
pageId: string,
|
||||
cursor?: string,
|
||||
): Promise<FacebookFormListResponse> {
|
||||
const response = (await facebookPageApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/${pageId}/leadgen_forms`,
|
||||
{},
|
||||
{ cursor, fields: 'id,name' },
|
||||
)) as FacebookFormListResponse;
|
||||
return response;
|
||||
}
|
1
packages/nodes-base/nodes/FacebookLeadAds/facebook.svg
Normal file
1
packages/nodes-base/nodes/FacebookLeadAds/facebook.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60"><path d="M59.5 30C59.5 13.71 46.29.5 30 .5S.5 13.71.5 30c0 14.72 10.79 26.93 24.89 29.14V38.53H17.9V30h7.49v-6.5c0-7.39 4.4-11.48 11.14-11.48 3.23 0 6.6.58 6.6.58v7.26h-3.72c-3.66 0-4.81 2.27-4.81 4.61V30h8.18l-1.31 8.53H34.6v20.61C48.71 56.93 59.5 44.72 59.5 30z" fill="#1877f2"/><path d="M41.48 38.53L42.79 30h-8.18v-5.53c0-2.33 1.14-4.61 4.81-4.61h3.72V12.6s-3.38-.58-6.6-.58c-6.74 0-11.14 4.08-11.14 11.48V30h-7.5v8.53h7.49v20.61c1.5.24 3.04.36 4.61.36s3.11-.12 4.61-.36V38.53h6.87z" fill="#fff"/></svg>
|
After Width: | Height: | Size: 567 B |
|
@ -0,0 +1 @@
|
|||
export * as listSearch from './listSearch';
|
|
@ -0,0 +1,42 @@
|
|||
import type { ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow';
|
||||
import { facebookFormList, facebookPageList } from '../GenericFunctions';
|
||||
|
||||
const filterMatches = (name: string, filter?: string): boolean =>
|
||||
!filter || name?.toLowerCase().includes(filter.toLowerCase());
|
||||
|
||||
export async function pageList(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const { data: pages, paging } = await facebookPageList.call(this, paginationToken);
|
||||
return {
|
||||
results: pages
|
||||
.filter((page) => filterMatches(page.name, filter))
|
||||
.map((page) => ({
|
||||
name: page.name,
|
||||
value: page.id,
|
||||
url: `https://facebook.com/${page.id}`,
|
||||
})),
|
||||
paginationToken: paging?.cursors?.after,
|
||||
};
|
||||
}
|
||||
|
||||
export async function formList(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const pageId = this.getNodeParameter('page', '', { extractValue: true }) as string;
|
||||
|
||||
const { data: forms, paging } = await facebookFormList.call(this, pageId, paginationToken);
|
||||
return {
|
||||
results: forms
|
||||
.filter((form) => filterMatches(form.name, filter))
|
||||
.map((form) => ({
|
||||
name: form.name,
|
||||
value: form.id,
|
||||
})),
|
||||
paginationToken: paging?.cursors?.after,
|
||||
};
|
||||
}
|
105
packages/nodes-base/nodes/FacebookLeadAds/types.ts
Normal file
105
packages/nodes-base/nodes/FacebookLeadAds/types.ts
Normal file
|
@ -0,0 +1,105 @@
|
|||
import type { GenericValue } from 'n8n-workflow';
|
||||
|
||||
export type BaseFacebookResponse<TData> = { data: TData };
|
||||
export type BasePaginatedFacebookResponse<TData> = BaseFacebookResponse<TData> & {
|
||||
paging: { cursors: { before?: string; after?: string } };
|
||||
};
|
||||
|
||||
export type FacebookAppWebhookSubscriptionsResponse = BaseFacebookResponse<
|
||||
FacebookAppWebhookSubscription[]
|
||||
>;
|
||||
|
||||
export interface FacebookAppWebhookSubscription {
|
||||
object: string;
|
||||
callback_url: string;
|
||||
active: boolean;
|
||||
fields: FacebookAppWebhookSubscriptionField[];
|
||||
}
|
||||
|
||||
export interface FacebookAppWebhookSubscriptionField {
|
||||
name: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface CreateFacebookAppWebhookSubscription {
|
||||
object: string;
|
||||
callback_url: string;
|
||||
fields: string[];
|
||||
include_values: boolean;
|
||||
verify_token: string;
|
||||
}
|
||||
|
||||
export type FacebookPageListResponse = BasePaginatedFacebookResponse<FacebookPage[]>;
|
||||
export type FacebookFormListResponse = BasePaginatedFacebookResponse<FacebookForm[]>;
|
||||
|
||||
export interface FacebookPage {
|
||||
id: string;
|
||||
name: string;
|
||||
access_token: string;
|
||||
category: string;
|
||||
category_list: FacebookPageCategory[];
|
||||
tasks: string[];
|
||||
}
|
||||
|
||||
export interface FacebookPageCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface FacebookFormQuestion {
|
||||
id: string;
|
||||
key: string;
|
||||
label: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface FacebookForm {
|
||||
id: string;
|
||||
name: string;
|
||||
locale: string;
|
||||
status: string;
|
||||
page: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
questions: FacebookFormQuestion[];
|
||||
}
|
||||
|
||||
export interface FacebookPageEvent {
|
||||
object: 'page';
|
||||
entry: FacebookPageEventEntry[];
|
||||
}
|
||||
|
||||
export interface FacebookPageEventEntry {
|
||||
id: string;
|
||||
time: number;
|
||||
changes: [
|
||||
{
|
||||
field: 'leadgen';
|
||||
value: {
|
||||
ad_id: string;
|
||||
form_id: string;
|
||||
leadgen_id: string;
|
||||
created_time: number;
|
||||
page_id: string;
|
||||
adgroup_id: string;
|
||||
};
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export interface FacebookFormLeadData {
|
||||
id: string;
|
||||
created_time: string;
|
||||
ad_id: string;
|
||||
ad_name: string;
|
||||
adset_id: string;
|
||||
adset_name: string;
|
||||
form_id: string;
|
||||
field_data: [
|
||||
{
|
||||
name: string;
|
||||
values: GenericValue[];
|
||||
},
|
||||
];
|
||||
}
|
|
@ -106,6 +106,7 @@
|
|||
"dist/credentials/F5BigIpApi.credentials.js",
|
||||
"dist/credentials/FacebookGraphApi.credentials.js",
|
||||
"dist/credentials/FacebookGraphAppApi.credentials.js",
|
||||
"dist/credentials/FacebookLeadAdsOAuth2Api.credentials.js",
|
||||
"dist/credentials/FigmaApi.credentials.js",
|
||||
"dist/credentials/FileMaker.credentials.js",
|
||||
"dist/credentials/FlowApi.credentials.js",
|
||||
|
@ -485,6 +486,7 @@
|
|||
"dist/nodes/ExecutionData/ExecutionData.node.js",
|
||||
"dist/nodes/Facebook/FacebookGraphApi.node.js",
|
||||
"dist/nodes/Facebook/FacebookTrigger.node.js",
|
||||
"dist/nodes/FacebookLeadAds/FacebookLeadAdsTrigger.node.js",
|
||||
"dist/nodes/Figma/FigmaTrigger.node.js",
|
||||
"dist/nodes/FileMaker/FileMaker.node.js",
|
||||
"dist/nodes/Filter/Filter.node.js",
|
||||
|
|
|
@ -2,23 +2,23 @@
|
|||
|
||||
import type * as express from 'express';
|
||||
import type FormData from 'form-data';
|
||||
import type { PathLike } from 'fs';
|
||||
import type { IncomingHttpHeaders } from 'http';
|
||||
import type { Readable } from 'stream';
|
||||
import type { URLSearchParams } from 'url';
|
||||
import type { OptionsWithUri, OptionsWithUrl } from 'request';
|
||||
import type { RequestPromiseOptions } from 'request-promise-native';
|
||||
import type { PathLike } from 'fs';
|
||||
import type { Readable } from 'stream';
|
||||
import type { URLSearchParams } from 'url';
|
||||
|
||||
import type { AuthenticationMethod } from './Authentication';
|
||||
import type { CODE_EXECUTION_MODES, CODE_LANGUAGES } from './Constants';
|
||||
import type { IDeferredPromise } from './DeferredPromise';
|
||||
import type { ExecutionStatus } from './ExecutionStatus';
|
||||
import type { ExpressionError } from './ExpressionError';
|
||||
import type { NodeApiError, NodeOperationError } from './NodeErrors';
|
||||
import type { Workflow } from './Workflow';
|
||||
import type { WorkflowHooks } from './WorkflowHooks';
|
||||
import type { WorkflowActivationError } from './WorkflowActivationError';
|
||||
import type { WorkflowOperationError } from './WorkflowErrors';
|
||||
import type { NodeApiError, NodeOperationError } from './NodeErrors';
|
||||
import type { ExpressionError } from './ExpressionError';
|
||||
import type { ExecutionStatus } from './ExecutionStatus';
|
||||
import type { AuthenticationMethod } from './Authentication';
|
||||
import type { WorkflowHooks } from './WorkflowHooks';
|
||||
|
||||
export interface IAdditionalCredentialOptions {
|
||||
oauth2?: IOAuth2Options;
|
||||
|
@ -1627,6 +1627,7 @@ export interface IWebhookDescription {
|
|||
responseMode?: WebhookResponseMode | string;
|
||||
responseData?: WebhookResponseData | string;
|
||||
restartWebhook?: boolean;
|
||||
hasLifecycleMethods?: boolean; // set automatically by generate-ui-types
|
||||
ndvHideUrl?: boolean; // If true the webhook will not be displayed in the editor
|
||||
ndvHideMethod?: boolean; // If true the method will not be displayed in the editor
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue