diff --git a/packages/nodes-base/credentials/FacebookGraphSubscriptionApi.credentials.ts b/packages/nodes-base/credentials/FacebookGraphSubscriptionApi.credentials.ts new file mode 100644 index 000000000..b96f84134 --- /dev/null +++ b/packages/nodes-base/credentials/FacebookGraphSubscriptionApi.credentials.ts @@ -0,0 +1,21 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class FacebookGraphSubscriptionApi implements ICredentialType { + name = 'facebookGraphSubscriptionApi'; + displayName = 'Facebook Graph API'; + extends = [ + 'facebookGraphApi', + ]; + properties = [ + { + displayName: 'APP Secret', + name: 'appSecret', + type: 'string' as NodePropertyTypes, + default: '', + description: '(Optional) When the app secret is set the node will verify this signature to validate the integrity and origin of the payload.', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts b/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts index 42d13cc87..d8a9aa2d3 100644 --- a/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts +++ b/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts @@ -10,7 +10,9 @@ import { INodeTypeDescription, } from 'n8n-workflow'; -import { OptionsWithUri } from 'request'; +import { + OptionsWithUri, +} from 'request'; export class FacebookGraphApi implements INodeType { description: INodeTypeDescription = { @@ -22,7 +24,7 @@ export class FacebookGraphApi implements INodeType { description: 'Interacts with Facebook using the Graph API', defaults: { name: 'Facebook Graph API', - color: '#772244', + color: '#3B5998', }, inputs: ['main'], outputs: ['main'], diff --git a/packages/nodes-base/nodes/Facebook/FacebookTrigger.node.ts b/packages/nodes-base/nodes/Facebook/FacebookTrigger.node.ts new file mode 100644 index 000000000..cb0f16b7a --- /dev/null +++ b/packages/nodes-base/nodes/Facebook/FacebookTrigger.node.ts @@ -0,0 +1,251 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import * as uuid from 'uuid/v4'; + +import { + snakeCase, +} from 'change-case'; + +import { + facebookApiRequest, +} from './GenericFunctions'; + +import { + createHmac, +} from 'crypto'; + +export class FacebookTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Facebook Trigger', + name: 'facebookTrigger', + icon: 'file:facebook.png', + group: ['trigger'], + version: 1, + subtitle: '={{$parameter["appId"] +"/"+ $parameter["object"]}}', + description: 'Starts the workflow when a Facebook events occurs.', + defaults: { + name: 'Facebook Trigger', + color: '#3B5998', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'facebookGraphSubscriptionApi', + required: true, + }, + ], + webhooks: [ + { + name: 'setup', + httpMethod: 'GET', + responseMode: 'onReceived', + path: 'webhook', + }, + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Object', + name: 'object', + type: 'options', + options: [ + { + name: 'Ad Account', + value: 'adAccount', + description: 'Get updates about Ad Account', + }, + { + name: 'Application', + value: 'application', + description: 'Get updates about the app', + }, + { + name: 'Certificate Transparency', + value: 'certificateTransparency', + description: 'Get updates about Certificate Transparency', + }, + { + name: 'Group', + value: 'group', + description: 'Get updates about activity in groups and events in groups for Workplace', + }, + { + name: 'Instagram', + value: 'instagram', + description: 'Get updates about comments on your media', + }, + { + name: 'Link', + value: 'link', + description: 'Get updates about links for rich previews by an external provider', + }, + { + name: 'Page', + value: 'page', + description: 'Page updates', + }, + { + name: 'Permissions', + value: 'permissions', + description: 'Updates regarding granting or revoking permissions', + }, + { + name: 'User', + value: 'user', + description: 'User profile updates', + }, + { + name: 'Whatsapp Business Account', + value: 'whatsappBusinessAccount', + description: 'Get updates about Whatsapp business account', + }, + { + name: 'Workplace Security', + value: 'workplaceSecurity', + description: 'Get updates about Workplace Security', + }, + ], + required: true, + default: 'user', + description: 'The object to subscribe to', + }, + { + displayName: 'App ID', + name: 'appId', + type: 'string', + required: true, + default: '', + description: 'Facebook APP ID', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + default: {}, + placeholder: 'Add option', + options: [ + { + displayName: 'Include values', + name: 'includeValues', + type: 'boolean', + default: true, + description: 'Indicates if change notifications should include the new values.', + }, + ], + }, + ], + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default') as string; + const object = this.getNodeParameter('object') as string; + const appId = this.getNodeParameter('appId') as string; + + const { data } = await facebookApiRequest.call(this, 'GET', `/${appId}/subscriptions`, {}); + + for (const webhook of data) { + if (webhook.target === webhookUrl && webhook.object === object && webhook.status === true) { + return true; + } + } + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default') as string; + const object = this.getNodeParameter('object') as string; + const appId = this.getNodeParameter('appId') as string; + const options = this.getNodeParameter('options') as IDataObject; + + const body = { + object: snakeCase(object), + callback_url: webhookUrl, + verify_token: uuid(), + } as IDataObject; + + if (options.includeValues !== undefined) { + body.include_values = options.includeValues; + } + + const responseData = await facebookApiRequest.call(this, 'POST', `/${appId}/subscriptions`, body); + + webhookData.verifyToken = body.verify_token; + + if (responseData.success !== true) { + // Facebook did not return success, so something went wrong + throw new Error('Facebook webhook creation response did not contain the expected data.'); + } + return true; + }, + async delete(this: IHookFunctions): Promise { + const appId = this.getNodeParameter('appId') as string; + const object = this.getNodeParameter('object') as string; + + try { + await facebookApiRequest.call(this, 'DELETE', `/${appId}/subscriptions`, { object: snakeCase(object) }); + } catch (e) { + return false; + } + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const bodyData = this.getBodyData() as IDataObject; + const query = this.getQueryData() as IDataObject; + const res = this.getResponseObject(); + const req = this.getRequestObject(); + const headerData = this.getHeaderData() as IDataObject; + const credentials = this.getCredentials('facebookGraphSubscriptionApi') as IDataObject; + // 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']) { + //TODO + //compare hub.verify_token with the saved token + //const webhookData = this.getWorkflowStaticData('node'); + // if (webhookData.verifyToken !== query['hub.verify_token']) { + // return {}; + // } + res.status(200).send(query['hub.challenge']).end(); + return { + noWebhookResponse: true, + }; + } + } + + // validate signature if app secret is set + if (credentials.appSecret !== '') { + //@ts-ignore + const computedSignature = createHmac('sha1', credentials.appSecret as string).update(req.rawBody).digest('hex'); + if (headerData['x-hub-signature'] !== `sha1=${computedSignature}`) { + return {}; + } + } + + return { + workflowData: [ + this.helpers.returnJsonArray(bodyData.entry as IDataObject[]), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Facebook/GenericFunctions.ts b/packages/nodes-base/nodes/Facebook/GenericFunctions.ts new file mode 100644 index 000000000..45412ff8f --- /dev/null +++ b/packages/nodes-base/nodes/Facebook/GenericFunctions.ts @@ -0,0 +1,53 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function facebookApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + + let credentials; + + if (this.getNode().name.includes('Trigger')) { + credentials = this.getCredentials('facebookGraphSubscriptionApi') as IDataObject; + } else { + credentials = this.getCredentials('facebookGraphApi') as IDataObject; + } + + qs.access_token = credentials!.accessToken; + + const options: OptionsWithUri = { + headers: { + accept: 'application/json,text/*;q=0.99', + }, + method, + qs, + body, + gzip: true, + uri: uri ||`https://graph.facebook.com/v8.0${resource}`, + json: true, + }; + + try { + return await this.helpers.request!(options); + } catch (error) { + + if (error.response.body && error.response.body.error) { + const message = error.response.body.error.message; + throw new Error( + `Facebook Trigger error response [${error.statusCode}]: ${message}`, + ); + } + throw new Error(error); + } +} diff --git a/packages/nodes-base/nodes/Facebook/facebook.png b/packages/nodes-base/nodes/Facebook/facebook.png index e0cc04460..85e6670ee 100644 Binary files a/packages/nodes-base/nodes/Facebook/facebook.png and b/packages/nodes-base/nodes/Facebook/facebook.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 6ec629783..90973cc2a 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -66,6 +66,7 @@ "dist/credentials/EventbriteApi.credentials.js", "dist/credentials/EventbriteOAuth2Api.credentials.js", "dist/credentials/FacebookGraphApi.credentials.js", + "dist/credentials/FacebookGraphSubscriptionApi.credentials.js", "dist/credentials/FreshdeskApi.credentials.js", "dist/credentials/FileMaker.credentials.js", "dist/credentials/FlowApi.credentials.js", @@ -266,6 +267,7 @@ "dist/nodes/ExecuteCommand.node.js", "dist/nodes/ExecuteWorkflow.node.js", "dist/nodes/Facebook/FacebookGraphApi.node.js", + "dist/nodes/Facebook/FacebookTrigger.node.js", "dist/nodes/FileMaker/FileMaker.node.js", "dist/nodes/Ftp.node.js", "dist/nodes/Freshdesk/Freshdesk.node.js",