diff --git a/packages/nodes-base/nodes/Twilio/GenericFunctions.ts b/packages/nodes-base/nodes/Twilio/GenericFunctions.ts index 6c81b0b3be..4f18bbccf5 100644 --- a/packages/nodes-base/nodes/Twilio/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Twilio/GenericFunctions.ts @@ -4,6 +4,8 @@ import type { IDataObject, IHttpRequestMethods, IRequestOptions, + IHttpRequestOptions, + ILoadOptionsFunctions, } from 'n8n-workflow'; /** @@ -40,6 +42,24 @@ export async function twilioApiRequest( return await this.helpers.requestWithAuthentication.call(this, 'twilioApi', options); } +export async function twilioTriggerApiRequest( + this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, + method: IHttpRequestMethods, + endpoint: string, + body: FormData | IDataObject = {}, +): Promise { + const options: IHttpRequestOptions = { + method, + body, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + url: `https://events.twilio.com/v1/${endpoint}`, + json: true, + }; + return await this.helpers.requestWithAuthentication.call(this, 'twilioApi', options); +} + const XML_CHAR_MAP: { [key: string]: string } = { '<': '<', '>': '>', diff --git a/packages/nodes-base/nodes/Twilio/TwilioTrigger.node.json b/packages/nodes-base/nodes/Twilio/TwilioTrigger.node.json new file mode 100644 index 0000000000..c2fda14568 --- /dev/null +++ b/packages/nodes-base/nodes/Twilio/TwilioTrigger.node.json @@ -0,0 +1,19 @@ +{ + "node": "n8n-nodes-base.twilioTrigger", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Communication", "Development"], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/twilio" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.twilio-trigger/" + } + ] + }, + "alias": ["SMS", "Phone", "Voice"] +} diff --git a/packages/nodes-base/nodes/Twilio/TwilioTrigger.node.ts b/packages/nodes-base/nodes/Twilio/TwilioTrigger.node.ts new file mode 100644 index 0000000000..4bf3d253d6 --- /dev/null +++ b/packages/nodes-base/nodes/Twilio/TwilioTrigger.node.ts @@ -0,0 +1,196 @@ +import type { + IHookFunctions, + IWebhookFunctions, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { twilioTriggerApiRequest } from './GenericFunctions'; + +export class TwilioTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Twilio Trigger', + name: 'twilioTrigger', + icon: 'file:twilio.svg', + group: ['trigger'], + version: [1], + defaultVersion: 1, + subtitle: '=Updates: {{$parameter["updates"].join(", ")}}', + description: 'Starts the workflow on a Twilio update', + defaults: { + name: 'Twilio Trigger', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'twilioApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Trigger On', + name: 'updates', + type: 'multiOptions', + options: [ + { + name: 'New SMS', + value: 'com.twilio.messaging.inbound-message.received', + description: 'When an SMS message is received', + }, + { + name: 'New Call', + value: 'com.twilio.voice.insights.call-summary.complete', + description: 'When a call is received', + }, + ], + required: true, + default: [], + }, + { + displayName: "The 'New Call' event may take up to thirty minutes to be triggered", + name: 'callTriggerNotice', + type: 'notice', + default: '', + displayOptions: { + show: { + updates: ['com.twilio.voice.insights.call-summary.complete'], + }, + }, + }, + ], + }; + + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + + const { sinks } = (await twilioTriggerApiRequest.call(this, 'GET', 'Sinks')) || {}; + + const sink = sinks.find( + (entry: { sink_configuration: { destination: string | undefined } }) => + entry.sink_configuration.destination === webhookUrl, + ); + + if (sink) { + const { subscriptions } = + (await twilioTriggerApiRequest.call(this, 'GET', 'Subscriptions')) || {}; + + const subscription = subscriptions.find( + (entry: { sink_sid: any }) => entry.sink_sid === sink.sid, + ); + + if (subscription) { + const { types } = + (await twilioTriggerApiRequest.call( + this, + 'GET', + `Subscriptions/${subscription.sid}/SubscribedEvents`, + )) || {}; + + const typesFound = types.map((type: { type: any }) => type.type); + + const allowedUpdates = this.getNodeParameter('updates') as string[]; + + if (typesFound.sort().join(',') === allowedUpdates.sort().join(',')) { + return true; + } else { + return false; + } + } + } + return false; + }, + + async create(this: IHookFunctions): Promise { + const workflowData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + + const allowedUpdates = this.getNodeParameter('updates') as string[]; + + const bodySink = { + Description: 'Sink created by n8n Twilio Trigger Node.', + SinkConfiguration: `{ "destination": "${webhookUrl}", "method": "POST" }`, + SinkType: 'webhook', + }; + + const sink = await twilioTriggerApiRequest.call(this, 'POST', 'Sinks', bodySink); + + workflowData.sinkId = sink.sid; + + const body = { + Description: 'Subscription created by n8n Twilio Trigger Node.', + Types: `{ "type": "${allowedUpdates[0]}" }`, + SinkSid: sink.sid, + }; + + const subscription = await twilioTriggerApiRequest.call( + this, + 'POST', + 'Subscriptions', + body, + ); + workflowData.subscriptionId = subscription.sid; + // if there is more than one event type add the others on the existing subscription + if (allowedUpdates.length > 1) { + for (let index = 1; index < allowedUpdates.length; index++) { + await twilioTriggerApiRequest.call( + this, + 'POST', + `Subscriptions/${workflowData.subscriptionId}/SubscribedEvents`, + { + Type: allowedUpdates[index], + }, + ); + } + } + + return true; + }, + + async delete(this: IHookFunctions): Promise { + const workflowData = this.getWorkflowStaticData('node'); + const sinkId = workflowData.sinkId; + const subscriptionId = workflowData.subscriptionId; + + try { + if (sinkId) { + await twilioTriggerApiRequest.call(this, 'DELETE', `Sinks/${sinkId}`, {}); + workflowData.sinkId = ''; + } + if (subscriptionId) { + await twilioTriggerApiRequest.call( + this, + 'DELETE', + `Subscriptions/${subscriptionId}`, + {}, + ); + workflowData.subscriptionId = ''; + } + } catch (error) { + return false; + } + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const bodyData = this.getBodyData(); + + return { + workflowData: [this.helpers.returnJsonArray(bodyData)], + }; + } +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 12388ea86d..bd38382123 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -755,6 +755,7 @@ "dist/nodes/Trello/TrelloTrigger.node.js", "dist/nodes/Twake/Twake.node.js", "dist/nodes/Twilio/Twilio.node.js", + "dist/nodes/Twilio/TwilioTrigger.node.js", "dist/nodes/Twist/Twist.node.js", "dist/nodes/Twitter/Twitter.node.js", "dist/nodes/Typeform/TypeformTrigger.node.js",