Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Twilio Trigger Node #8859

Merged
merged 13 commits into from
Mar 29, 2024
20 changes: 20 additions & 0 deletions packages/nodes-base/nodes/Twilio/GenericFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type {
IDataObject,
IHttpRequestMethods,
IRequestOptions,
IHttpRequestOptions,
ILoadOptionsFunctions,
} from 'n8n-workflow';

/**
Expand Down Expand Up @@ -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<any> {
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 } = {
'<': '&lt;',
'>': '&gt;',
Expand Down
19 changes: 19 additions & 0 deletions packages/nodes-base/nodes/Twilio/TwilioTrigger.node.json
Original file line number Diff line number Diff line change
@@ -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"]
}
196 changes: 196 additions & 0 deletions packages/nodes-base/nodes/Twilio/TwilioTrigger.node.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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<boolean> {
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<boolean> {
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<IWebhookResponseData> {
const bodyData = this.getBodyData();

return {
workflowData: [this.helpers.returnJsonArray(bodyData)],
};
}
}
1 change: 1 addition & 0 deletions packages/nodes-base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,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",
Expand Down
Loading