Add Activepieces integration for workflow automation
- Add Activepieces fork with SmoothSchedule custom piece - Create integrations app with Activepieces service layer - Add embed token endpoint for iframe integration - Create Automations page with embedded workflow builder - Add sidebar visibility fix for embed mode - Add list inactive customers endpoint to Public API - Include SmoothSchedule triggers: event created/updated/cancelled - Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod, httpClient, AuthenticationType, QueryParams } from '@activepieces/pieces-common';
|
||||
import { twilioAuth } from '../..';
|
||||
|
||||
export const twilioDownloadRecordingMedia = createAction({
|
||||
auth: twilioAuth,
|
||||
name: 'download_recording_media',
|
||||
displayName: 'Download Recording Media',
|
||||
description: 'Download the media file for a specific recording.',
|
||||
props: {
|
||||
recording_sid: Property.ShortText({
|
||||
displayName: 'Recording SID',
|
||||
description: 'The unique identifier (SID) of the recording to download. It starts with "RE".',
|
||||
required: true,
|
||||
}),
|
||||
format: Property.StaticDropdown({
|
||||
displayName: 'Format',
|
||||
description: 'The desired audio format for the download file.',
|
||||
required: false,
|
||||
defaultValue: 'mp3',
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'MP3', value: 'mp3' },
|
||||
{ label: 'WAV', value: 'wav' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
channels: Property.StaticDropdown({
|
||||
displayName: 'Channels',
|
||||
description: 'Specify whether to download a mono or dual-channel file. Note: Dual-channel may not be available for all recordings.',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Mono', value: 1 },
|
||||
{ label: 'Dual', value: 2 },
|
||||
]
|
||||
}
|
||||
})
|
||||
},
|
||||
async run(context) {
|
||||
const { recording_sid, format, channels } = context.propsValue;
|
||||
const account_sid = context.auth.username;
|
||||
const auth_token = context.auth.password;
|
||||
|
||||
const fileFormat = format ?? 'mp3';
|
||||
const path = `Recordings/${recording_sid}.${fileFormat}`;
|
||||
|
||||
const queryParams: QueryParams = {};
|
||||
if (channels) {
|
||||
queryParams['RequestedChannels'] = channels.toString();
|
||||
}
|
||||
|
||||
const response = await httpClient.sendRequest<ArrayBuffer>({
|
||||
method: HttpMethod.GET,
|
||||
url: `https://api.twilio.com/2010-04-01/Accounts/${account_sid}/${path}`,
|
||||
queryParams: queryParams,
|
||||
authentication: {
|
||||
type: AuthenticationType.BASIC,
|
||||
username: account_sid,
|
||||
password: auth_token,
|
||||
},
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
|
||||
const fileData = Buffer.from(response.body);
|
||||
|
||||
return await context.files.write({
|
||||
fileName: `${recording_sid}.${fileFormat}`,
|
||||
data: fileData,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { callTwilioApi } from '../common';
|
||||
import { twilioAuth } from '../..';
|
||||
|
||||
export const twilioGetMessage = createAction({
|
||||
auth: twilioAuth,
|
||||
name: 'get_message',
|
||||
description: 'Retrieves the details of a specific message.',
|
||||
displayName: 'Get Message',
|
||||
props: {
|
||||
message_sid: Property.ShortText({
|
||||
displayName: 'Message SID',
|
||||
description: 'The unique identifier (SID) of the message to retrieve. It starts with "SM" or "MM".',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { message_sid } = context.propsValue;
|
||||
const account_sid = context.auth.username;
|
||||
const auth_token = context.auth.password;
|
||||
|
||||
const path = `Messages/${message_sid}.json`;
|
||||
|
||||
const response = await callTwilioApi(
|
||||
HttpMethod.GET,
|
||||
path,
|
||||
{ account_sid, auth_token }
|
||||
);
|
||||
|
||||
return response.body;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { callTwilioApi, twilioCommon } from '../common';
|
||||
import { twilioAuth } from '../..';
|
||||
|
||||
export const twilioMakeCall = createAction({
|
||||
auth: twilioAuth,
|
||||
name: 'make_call',
|
||||
description: 'Call a number and say a message.',
|
||||
displayName: 'Call Phone',
|
||||
props: {
|
||||
from: twilioCommon.phone_number,
|
||||
to: Property.ShortText({
|
||||
displayName: 'To',
|
||||
description: 'The phone number to call. Must be in E.164 format (e.g., +15558675310).',
|
||||
required: true,
|
||||
}),
|
||||
message: Property.LongText({
|
||||
displayName: 'Message to Say',
|
||||
description: 'The text message to be converted to speech and spoken to the recipient.',
|
||||
required: true,
|
||||
}),
|
||||
voice: Property.StaticDropdown({
|
||||
displayName: 'Voice',
|
||||
description: 'The voice to use for the text-to-speech message.',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Alice (Default)', value: 'alice' },
|
||||
{ label: 'Man', value: 'man' },
|
||||
{ label: 'Woman', value: 'woman' },
|
||||
]
|
||||
}
|
||||
}),
|
||||
language: Property.StaticDropdown({
|
||||
displayName: 'Language',
|
||||
description: 'The language to use for the text-to-speech message.',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'English (US)', value: 'en-US' },
|
||||
{ label: 'English (UK)', value: 'en-GB' },
|
||||
{ label: 'Spanish', value: 'es-ES' },
|
||||
{ label: 'French', value: 'fr-FR' },
|
||||
{ label: 'German', value: 'de-DE' },
|
||||
]
|
||||
}
|
||||
}),
|
||||
sendDigits: Property.ShortText({
|
||||
displayName: 'Send DTMF Tones',
|
||||
description: "A string of keys to dial after the call is connected. Use 'w' for a half-second pause.",
|
||||
required: false,
|
||||
}),
|
||||
timeout: Property.Number({
|
||||
displayName: 'Timeout (seconds)',
|
||||
description: 'The number of seconds to let the phone ring before assuming no answer. Default is 60.',
|
||||
required: false,
|
||||
})
|
||||
},
|
||||
async run(context) {
|
||||
const { from, to, message, voice, language, sendDigits, timeout } = context.propsValue;
|
||||
|
||||
const account_sid = context.auth.username;
|
||||
const auth_token = context.auth.password;
|
||||
|
||||
// Construct TwiML for the <Say> verb
|
||||
const voiceAttr = voice ? ` voice="${voice}"` : '';
|
||||
const langAttr = language ? ` language="${language}"` : '';
|
||||
const escapedMessage = message
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
const twiml = `<Response><Say${voiceAttr}${langAttr}>${escapedMessage}</Say></Response>`;
|
||||
|
||||
const bodyParams: Record<string, unknown> = {
|
||||
From: from,
|
||||
To: to,
|
||||
Twiml: twiml,
|
||||
};
|
||||
|
||||
if (sendDigits) {
|
||||
bodyParams['SendDigits'] = sendDigits;
|
||||
}
|
||||
if (timeout) {
|
||||
bodyParams['Timeout'] = timeout;
|
||||
}
|
||||
|
||||
const response = await callTwilioApi(
|
||||
HttpMethod.POST,
|
||||
'Calls.json',
|
||||
{ account_sid, auth_token },
|
||||
bodyParams
|
||||
);
|
||||
|
||||
return response.body;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { AuthenticationType, httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { twilioAuth } from '../..';
|
||||
|
||||
export const twilioPhoneNumberLookup = createAction({
|
||||
auth: twilioAuth,
|
||||
name: 'phone_number_lookup',
|
||||
description: 'Lookup information about a phone number.',
|
||||
displayName: 'Phone Number Lookup',
|
||||
props: {
|
||||
phone_number:Property.ShortText({
|
||||
displayName:'Phone Number',
|
||||
required:true
|
||||
})
|
||||
},
|
||||
async run(context) {
|
||||
const { phone_number } = context.propsValue;
|
||||
|
||||
const account_sid = context.auth.username;
|
||||
const auth_token = context.auth.password;
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method:HttpMethod.GET,
|
||||
url:`https://lookups.twilio.com/v2/PhoneNumbers/${encodeURIComponent(phone_number)}`,
|
||||
authentication: {
|
||||
type: AuthenticationType.BASIC,
|
||||
username: account_sid,
|
||||
password: auth_token,
|
||||
},
|
||||
queryParams:{
|
||||
Fields:'line_type_intelligence'
|
||||
}
|
||||
})
|
||||
|
||||
return response.body
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { callTwilioApi, twilioCommon } from '../common';
|
||||
import { twilioAuth } from '../..';
|
||||
|
||||
export const twilioSendSms = createAction({
|
||||
auth: twilioAuth,
|
||||
name: 'send_sms',
|
||||
description: 'Send a new SMS message',
|
||||
displayName: 'Send SMS',
|
||||
props: {
|
||||
from: twilioCommon.phone_number,
|
||||
body: Property.ShortText({
|
||||
description: 'The body of the message to send',
|
||||
displayName: 'Message Body',
|
||||
required: true,
|
||||
}),
|
||||
to: Property.ShortText({
|
||||
description: 'The phone number to send the message to',
|
||||
displayName: 'To',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { body, to, from } = context.propsValue;
|
||||
const account_sid = context.auth.username;
|
||||
const auth_token = context.auth.password;
|
||||
return await callTwilioApi(
|
||||
HttpMethod.POST,
|
||||
'Messages.json',
|
||||
{ account_sid, auth_token },
|
||||
{
|
||||
From: from,
|
||||
Body: body,
|
||||
To: to,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
Property,
|
||||
BasicAuthPropertyValue,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import {
|
||||
HttpMethod,
|
||||
HttpMessageBody,
|
||||
httpClient,
|
||||
AuthenticationType,
|
||||
} from '@activepieces/pieces-common';
|
||||
import { twilioAuth } from '../..';
|
||||
|
||||
export const twilioCommon = {
|
||||
phone_number: Property.Dropdown({
|
||||
auth: twilioAuth,
|
||||
description: 'The phone number to send the message from',
|
||||
displayName: 'From',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'connect your account first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
const basicAuthProperty = auth;
|
||||
const response = await callTwilioApi<{
|
||||
incoming_phone_numbers: {
|
||||
phone_number: string;
|
||||
friendly_name: string;
|
||||
}[];
|
||||
}>(HttpMethod.GET, 'IncomingPhoneNumbers.json', {
|
||||
account_sid: basicAuthProperty.username,
|
||||
auth_token: basicAuthProperty.password,
|
||||
});
|
||||
return {
|
||||
disabled: false,
|
||||
options: response.body.incoming_phone_numbers.map((number: any) => ({
|
||||
value: number.phone_number,
|
||||
label: number.friendly_name,
|
||||
})),
|
||||
};
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export const callTwilioApi = async <T extends HttpMessageBody>(
|
||||
method: HttpMethod,
|
||||
path: string,
|
||||
auth: { account_sid: string; auth_token: string },
|
||||
body?: any
|
||||
) => {
|
||||
return await httpClient.sendRequest<T>({
|
||||
method,
|
||||
url: `https://api.twilio.com/2010-04-01/Accounts/${auth.account_sid}/${path}`,
|
||||
authentication: {
|
||||
type: AuthenticationType.BASIC,
|
||||
username: auth.account_sid,
|
||||
password: auth.auth_token,
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: body,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
import { createTrigger, TriggerStrategy, PiecePropValueSchema, AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework';
|
||||
import { twilioAuth } from '../..';
|
||||
import { AuthenticationType, DedupeStrategy, httpClient, HttpMethod, Polling, pollingHelper } from '@activepieces/pieces-common';
|
||||
|
||||
interface Call {
|
||||
sid: string;
|
||||
status: string;
|
||||
direction: string;
|
||||
from: string;
|
||||
to: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
duration: string;
|
||||
price: string;
|
||||
price_unit: string;
|
||||
date_created:string
|
||||
}
|
||||
|
||||
interface CallsResponse {
|
||||
calls: Call[];
|
||||
next_page_uri:string
|
||||
|
||||
}
|
||||
|
||||
|
||||
const polling: Polling<
|
||||
AppConnectionValueForAuthProperty<typeof twilioAuth>,
|
||||
Record<string, unknown>
|
||||
> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
async items({ auth, lastFetchEpochMS }) {
|
||||
const isTest = lastFetchEpochMS === 0;
|
||||
const account_sid = auth.username;
|
||||
const auth_token = auth.password;
|
||||
|
||||
let currentUri:
|
||||
| string
|
||||
| null = `/2010-04-01/Accounts/${account_sid}/Calls.json?PageSize=${
|
||||
isTest ? 1 : 1000
|
||||
}`;
|
||||
|
||||
const results = [];
|
||||
let stop = false;
|
||||
|
||||
do {
|
||||
const response: any = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `https://api.twilio.com${currentUri}`,
|
||||
authentication: {
|
||||
type: AuthenticationType.BASIC,
|
||||
username: account_sid,
|
||||
password: auth_token,
|
||||
},
|
||||
});
|
||||
|
||||
const payload = response.body as CallsResponse;
|
||||
|
||||
const calls = payload.calls ?? [];
|
||||
|
||||
for (const call of calls) {
|
||||
const ts = new Date(call.date_created).getTime();
|
||||
|
||||
if(call.status !== 'completed') continue;
|
||||
|
||||
if (isTest || ts > lastFetchEpochMS) {
|
||||
results.push(call);
|
||||
} else {
|
||||
stop = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isTest) break;
|
||||
currentUri = payload?.next_page_uri ?? null;
|
||||
} while (currentUri && !stop);
|
||||
|
||||
return results.map((call) => {
|
||||
return {
|
||||
epochMilliSeconds: new Date(call.date_created).getTime(),
|
||||
data: call,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const twilioNewCall = createTrigger({
|
||||
auth: twilioAuth,
|
||||
name: 'new_call',
|
||||
displayName: 'New Call',
|
||||
description: 'Triggers when a call completes (incoming or outgoing).',
|
||||
props: {},
|
||||
sampleData:{
|
||||
"account_sid": "ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"answered_by": "machine_start",
|
||||
"api_version": "2010-04-01",
|
||||
"caller_name": "callerid1",
|
||||
"date_created": "Fri, 18 Oct 2019 17:00:00 +0000",
|
||||
"date_updated": "Fri, 18 Oct 2019 17:01:00 +0000",
|
||||
"direction": "outbound-api",
|
||||
"duration": "4",
|
||||
"end_time": "Fri, 18 Oct 2019 17:03:00 +0000",
|
||||
"forwarded_from": "calledvia1",
|
||||
"from": "+13051416799",
|
||||
"from_formatted": "(305) 141-6799",
|
||||
"group_sid": "GPdeadbeefdeadbeefdeadbeefdeadbeef",
|
||||
"parent_call_sid": "CAdeadbeefdeadbeefdeadbeefdeadbeef",
|
||||
"phone_number_sid": "PNdeadbeefdeadbeefdeadbeefdeadbeef",
|
||||
"price": "-0.200",
|
||||
"price_unit": "USD",
|
||||
"sid": "CAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"start_time": "Fri, 18 Oct 2019 17:02:00 +0000",
|
||||
"status": "completed",
|
||||
"subresource_uris": {
|
||||
"notifications": "/2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Calls/CAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Notifications.json",
|
||||
"recordings": "/2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Calls/CAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Recordings.json",
|
||||
"payments": "/2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Calls/CAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Payments.json",
|
||||
"events": "/2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Calls/CAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Events.json",
|
||||
"siprec": "/2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Calls/CAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Siprec.json",
|
||||
"streams": "/2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Calls/CAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Streams.json",
|
||||
"transcriptions": "/2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Calls/CAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Transcriptions.json",
|
||||
"user_defined_message_subscriptions": "/2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Calls/CAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/UserDefinedMessageSubscriptions.json",
|
||||
"user_defined_messages": "/2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Calls/CAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/UserDefinedMessages.json"
|
||||
},
|
||||
"to": "+13051913581",
|
||||
"to_formatted": "(305) 191-3581",
|
||||
"trunk_sid": "TKdeadbeefdeadbeefdeadbeefdeadbeef",
|
||||
"uri": "/2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Calls/CAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.json",
|
||||
"queue_time": "1000"
|
||||
},
|
||||
type: TriggerStrategy.POLLING,
|
||||
|
||||
|
||||
async onEnable(context) {
|
||||
await pollingHelper.onEnable(polling, {
|
||||
auth: context.auth,
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
});
|
||||
},
|
||||
async onDisable(context) {
|
||||
await pollingHelper.onDisable(polling, {
|
||||
auth: context.auth,
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
});
|
||||
},
|
||||
async test(context) {
|
||||
return await pollingHelper.test(polling, context);
|
||||
},
|
||||
async run(context) {
|
||||
return await pollingHelper.poll(polling, context);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
|
||||
import {
|
||||
HttpMethod,
|
||||
HttpResponse,
|
||||
httpClient,
|
||||
AuthenticationType,
|
||||
} from '@activepieces/pieces-common';
|
||||
import { callTwilioApi, twilioCommon } from '../common';
|
||||
import { twilioAuth } from '../..';
|
||||
|
||||
export const twilioNewIncomingSms = createTrigger({
|
||||
auth: twilioAuth,
|
||||
name: 'new_incoming_sms',
|
||||
displayName: 'New Incoming SMS',
|
||||
description: 'Triggers when a new SMS message is received',
|
||||
props: {
|
||||
phone_number: twilioCommon.phone_number,
|
||||
},
|
||||
sampleData: {
|
||||
body: 'Hello',
|
||||
num_segments: '1',
|
||||
direction: 'inbound',
|
||||
from: '+12184191735',
|
||||
date_updated: 'Wed, 08 Feb 2023 01:40:51 +0000',
|
||||
price: null,
|
||||
error_message: null,
|
||||
uri: '/2010-04-01/Accounts/ACc0ea1238d61fe90d78a69a3de71d45619/Messages/SM8c3920d3f2ac481ba83e639a69dadd63.json',
|
||||
account_sid: 'ACc0ea716d61fe90d78a123a3de71d45619',
|
||||
num_media: '0',
|
||||
to: '+12184191735',
|
||||
date_created: 'Wed, 08 Feb 2023 01:40:50 +0000',
|
||||
status: 'failed',
|
||||
sid: 'SM8c3920d3f2ac481ba83e639a69dadd63',
|
||||
date_sent: 'Wed, 08 Feb 2023 01:40:51 +0000',
|
||||
messaging_service_sid: 'MG88e323e6a88ce67ba3bf12e1bcb7e0b8',
|
||||
error_code: 21211,
|
||||
price_unit: 'USD',
|
||||
api_version: '2010-04-01',
|
||||
subresource_uris: {
|
||||
media:
|
||||
'/2010-04-01/Accounts/ACc0ea716d61fe90d78a69a3de71d45619/Messages/SM8c3920d3f2ac481ba83e639a69dadd63/Media.json',
|
||||
feedback:
|
||||
'/2010-04-01/Accounts/ACc0ea716d61fe90d78a69a3de71d45619/Messages/SM8c3920d3f2ac481ba83e639a69dadd63/Feedback.json',
|
||||
},
|
||||
},
|
||||
// Twilio API only allows one webhook per phone number, so we need to poll
|
||||
type: TriggerStrategy.POLLING,
|
||||
async onEnable(context) {
|
||||
const { phone_number } = context.propsValue;
|
||||
const account_sid = context.auth.username;
|
||||
const auth_token = context.auth.password;
|
||||
const response = await callTwilioApi<MessagePaginationResponse>(
|
||||
HttpMethod.GET,
|
||||
`Messages.json?PageSize=20&To=${phone_number}`,
|
||||
{ account_sid, auth_token },
|
||||
{}
|
||||
);
|
||||
await context.store.put<LastMessage>('_new_incoming_sms_trigger', {
|
||||
lastMessageId:
|
||||
response.body.messages.length === 0
|
||||
? null
|
||||
: response.body.messages[0].sid,
|
||||
});
|
||||
},
|
||||
async onDisable(context) {
|
||||
await context.store.put('_new_incoming_sms_trigger', null);
|
||||
},
|
||||
async run(context) {
|
||||
const account_sid = context.auth.username;
|
||||
const auth_token = context.auth.password;
|
||||
const newMessages: unknown[] = [];
|
||||
const lastMessage = await context.store.get<LastMessage>(
|
||||
'_new_incoming_sms_trigger'
|
||||
);
|
||||
let currentUri:
|
||||
| string
|
||||
| null = `2010-04-01/Accounts/${account_sid}/Messages.json?PageSize=20&To=${context.propsValue.phone_number}`;
|
||||
let firstMessageId = undefined;
|
||||
while (currentUri !== undefined && currentUri !== null) {
|
||||
const res: HttpResponse<MessagePaginationResponse> =
|
||||
await httpClient.sendRequest<MessagePaginationResponse>({
|
||||
method: HttpMethod.GET,
|
||||
url: `https://api.twilio.com/${currentUri}`,
|
||||
authentication: {
|
||||
type: AuthenticationType.BASIC,
|
||||
username: account_sid,
|
||||
password: auth_token,
|
||||
},
|
||||
});
|
||||
const messages = res.body.messages;
|
||||
if (!firstMessageId && messages.length > 0) {
|
||||
firstMessageId = messages[0].sid;
|
||||
}
|
||||
currentUri = res.body.next_page_uri;
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const message = messages[i];
|
||||
if (message.sid === lastMessage?.lastMessageId) {
|
||||
currentUri = null;
|
||||
break;
|
||||
}
|
||||
if (message.direction === 'inbound') {
|
||||
newMessages.push(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
await context.store.put<LastMessage>('_new_incoming_sms_trigger', {
|
||||
lastMessageId: firstMessageId ?? lastMessage!.lastMessageId,
|
||||
});
|
||||
return newMessages;
|
||||
},
|
||||
});
|
||||
|
||||
interface LastMessage {
|
||||
lastMessageId: string | null;
|
||||
}
|
||||
|
||||
interface MessagePaginationResponse {
|
||||
messages: { sid: string; to: string; status: string; direction: string }[];
|
||||
next_page_uri: string;
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
AppConnectionValueForAuthProperty,
|
||||
createTrigger,
|
||||
PiecePropValueSchema,
|
||||
TriggerStrategy,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import {
|
||||
AuthenticationType,
|
||||
DedupeStrategy,
|
||||
httpClient,
|
||||
HttpMethod,
|
||||
Polling,
|
||||
pollingHelper,
|
||||
} from '@activepieces/pieces-common';
|
||||
import { twilioAuth } from '../..';
|
||||
|
||||
// The full structure of an IncomingPhoneNumber object from the Twilio API
|
||||
interface TwilioPhoneNumber {
|
||||
sid: string;
|
||||
account_sid: string;
|
||||
friendly_name: string;
|
||||
phone_number: string;
|
||||
voice_url: string;
|
||||
voice_method: string;
|
||||
sms_url: string;
|
||||
sms_method: string;
|
||||
date_created: string;
|
||||
date_updated: string;
|
||||
capabilities: {
|
||||
voice: boolean;
|
||||
sms: boolean;
|
||||
mms: boolean;
|
||||
};
|
||||
uri: string;
|
||||
}
|
||||
|
||||
interface IncomingPhoneNumbersResponse {
|
||||
incoming_phone_numbers: TwilioPhoneNumber[];
|
||||
next_page_uri:string
|
||||
}
|
||||
|
||||
const polling: Polling<
|
||||
AppConnectionValueForAuthProperty<typeof twilioAuth>,
|
||||
Record<string, unknown>
|
||||
> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
async items({ auth, lastFetchEpochMS }) {
|
||||
const isTest = lastFetchEpochMS === 0;
|
||||
const account_sid = auth.username;
|
||||
const auth_token = auth.password;
|
||||
|
||||
let currentUri:
|
||||
| string
|
||||
| null = `/2010-04-01/Accounts/${account_sid}/IncomingPhoneNumbers.json?PageSize=${
|
||||
isTest ? 10 : 1000
|
||||
}`;
|
||||
|
||||
const results = [];
|
||||
let stop = false;
|
||||
|
||||
do {
|
||||
const response: any = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `https://api.twilio.com${currentUri}`,
|
||||
authentication: {
|
||||
type: AuthenticationType.BASIC,
|
||||
username: account_sid,
|
||||
password: auth_token,
|
||||
},
|
||||
});
|
||||
|
||||
const payload = response.body as IncomingPhoneNumbersResponse;
|
||||
|
||||
const numbers = payload.incoming_phone_numbers ?? [];
|
||||
|
||||
for (const num of numbers) {
|
||||
const ts = new Date(num.date_created).getTime();
|
||||
|
||||
if (isTest || ts > lastFetchEpochMS) {
|
||||
results.push(num);
|
||||
} else {
|
||||
stop = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isTest) break;
|
||||
currentUri = payload?.next_page_uri ?? null;
|
||||
} while (currentUri && !stop);
|
||||
|
||||
return results.map((num) => {
|
||||
return {
|
||||
epochMilliSeconds: new Date(num.date_created).getTime(),
|
||||
data: num,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const twilioNewPhoneNumber = createTrigger({
|
||||
auth: twilioAuth,
|
||||
name: 'new_phone_number',
|
||||
displayName: 'New Phone Number',
|
||||
description: 'Triggers when you add a new phone number to your account.',
|
||||
props: {},
|
||||
sampleData: {
|
||||
sid: 'PNXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
|
||||
account_sid: 'ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
|
||||
friendly_name: '(555) 123-4567',
|
||||
phone_number: '+15551234567',
|
||||
voice_url: 'https://demo.twilio.com/welcome/voice/',
|
||||
voice_method: 'POST',
|
||||
sms_url: 'https://demo.twilio.com/welcome/sms/',
|
||||
sms_method: 'POST',
|
||||
date_created: '2025-08-28T11:44:10+00:00',
|
||||
date_updated: '2025-08-28T11:44:10+00:00',
|
||||
capabilities: {
|
||||
voice: true,
|
||||
sms: true,
|
||||
mms: true,
|
||||
},
|
||||
uri: '/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/IncomingPhoneNumbers/PNXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.json',
|
||||
},
|
||||
type: TriggerStrategy.POLLING,
|
||||
|
||||
async onEnable(context) {
|
||||
await pollingHelper.onEnable(polling, {
|
||||
auth: context.auth,
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
});
|
||||
},
|
||||
async onDisable(context) {
|
||||
await pollingHelper.onDisable(polling, {
|
||||
auth: context.auth,
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
});
|
||||
},
|
||||
async test(context) {
|
||||
return await pollingHelper.test(polling, context);
|
||||
},
|
||||
async run(context) {
|
||||
return await pollingHelper.poll(polling, context);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
import { AppConnectionValueForAuthProperty, createTrigger, PiecePropValueSchema, TriggerStrategy } from '@activepieces/pieces-framework';
|
||||
import { AuthenticationType, DedupeStrategy, httpClient, HttpMethod, Polling, pollingHelper } from '@activepieces/pieces-common';
|
||||
import { twilioAuth } from '../..';
|
||||
|
||||
interface Recording {
|
||||
sid: string;
|
||||
status: string;
|
||||
date_created: string;
|
||||
call_sid: string;
|
||||
duration: string;
|
||||
source: string;
|
||||
price: string;
|
||||
price_unit: string;
|
||||
}
|
||||
|
||||
interface RecordingsResponse {
|
||||
recordings: Recording[];
|
||||
next_page_uri:string
|
||||
|
||||
}
|
||||
|
||||
|
||||
const polling: Polling<
|
||||
AppConnectionValueForAuthProperty<typeof twilioAuth>,
|
||||
Record<string, unknown>
|
||||
> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
async items({ auth, lastFetchEpochMS }) {
|
||||
const isTest = lastFetchEpochMS === 0;
|
||||
const account_sid = auth.username;
|
||||
const auth_token = auth.password;
|
||||
|
||||
let currentUri:
|
||||
| string
|
||||
| null = `/2010-04-01/Accounts/${account_sid}/Recordings.json?PageSize=${
|
||||
isTest ? 10 : 1000
|
||||
}`;
|
||||
|
||||
const results = [];
|
||||
let stop = false;
|
||||
|
||||
do {
|
||||
const response: any = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `https://api.twilio.com${currentUri}`,
|
||||
authentication: {
|
||||
type: AuthenticationType.BASIC,
|
||||
username: account_sid,
|
||||
password: auth_token,
|
||||
},
|
||||
});
|
||||
|
||||
const payload = response.body as RecordingsResponse;
|
||||
|
||||
const recordings = payload.recordings ?? [];
|
||||
|
||||
for (const recording of recordings) {
|
||||
const ts = new Date(recording.date_created).getTime();
|
||||
|
||||
if (isTest || ts > lastFetchEpochMS) {
|
||||
results.push(recording);
|
||||
} else {
|
||||
stop = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isTest) break;
|
||||
currentUri = payload?.next_page_uri ?? null;
|
||||
} while (currentUri && !stop);
|
||||
|
||||
return results.map((recording) => {
|
||||
return {
|
||||
epochMilliSeconds: new Date(recording.date_created).getTime(),
|
||||
data: recording,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const twilioNewRecording = createTrigger({
|
||||
auth: twilioAuth,
|
||||
name: 'new_recording',
|
||||
displayName: 'New Recording',
|
||||
description: 'Triggers when a new call recording is completed and available.',
|
||||
props: {},
|
||||
sampleData: {
|
||||
"account_sid": "ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"api_version": "2010-04-01",
|
||||
"call_sid": "CAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"conference_sid": "CFaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"channels": 1,
|
||||
"date_created": "Fri, 14 Oct 2016 21:56:34 +0000",
|
||||
"date_updated": "Fri, 14 Oct 2016 21:56:38 +0000",
|
||||
"start_time": "Fri, 14 Oct 2016 21:56:34 +0000",
|
||||
"price": "0.04",
|
||||
"price_unit": "USD",
|
||||
"duration": "4",
|
||||
"sid": "REaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"source": "StartConferenceRecordingAPI",
|
||||
"status": "completed",
|
||||
"error_code": null,
|
||||
"uri": "/2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Recordings/REaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.json",
|
||||
"subresource_uris": {
|
||||
"add_on_results": "/2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Recordings/REaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/AddOnResults.json",
|
||||
"transcriptions": "/2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Recordings/REaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Transcriptions.json"
|
||||
},
|
||||
"encryption_details": {
|
||||
"encryption_public_key_sid": "CRaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"encryption_cek": "OV4h6zrsxMIW7h0Zfqwfn6TI2GCNl54KALlg8wn8YB8KYZhXt6HlgvBWAmQTlfYVeLWydMiCewY0YkDDT1xmNe5huEo9vjuKBS5OmYK4CZkSx1NVv3XOGrZHpd2Pl/5WJHVhUK//AUO87uh5qnUP2E0KoLh1nyCLeGcEkXU0RfpPn/6nxjof/n6m6OzZOyeIRK4Oed5+rEtjqFDfqT0EVKjs6JAxv+f0DCc1xYRHl2yV8bahUPVKs+bHYdy4PVszFKa76M/Uae4jFA9Lv233JqWcxj+K2UoghuGhAFbV/JQIIswY2CBYI8JlVSifSqNEl9vvsTJ8bkVMm3MKbG2P7Q==",
|
||||
"encryption_iv": "8I2hhNIYNTrwxfHk"
|
||||
},
|
||||
"media_url": "http://api.twilio.com/2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Recordings/REaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
},
|
||||
type: TriggerStrategy.POLLING,
|
||||
|
||||
async onEnable(context) {
|
||||
await pollingHelper.onEnable(polling, {
|
||||
auth: context.auth,
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
});
|
||||
},
|
||||
async onDisable(context) {
|
||||
await pollingHelper.onDisable(polling, {
|
||||
auth: context.auth,
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
});
|
||||
},
|
||||
async test(context) {
|
||||
return await pollingHelper.test(polling, context);
|
||||
},
|
||||
async run(context) {
|
||||
return await pollingHelper.poll(polling, context);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
import {
|
||||
AppConnectionValueForAuthProperty,
|
||||
createTrigger,
|
||||
PiecePropValueSchema,
|
||||
TriggerStrategy,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import {
|
||||
AuthenticationType,
|
||||
DedupeStrategy,
|
||||
httpClient,
|
||||
HttpMethod,
|
||||
Polling,
|
||||
pollingHelper,
|
||||
} from '@activepieces/pieces-common';
|
||||
import { twilioAuth } from '../..';
|
||||
|
||||
interface Transcription {
|
||||
sid: string;
|
||||
status: string;
|
||||
date_created: string;
|
||||
recording_sid: string;
|
||||
duration: string;
|
||||
price: string;
|
||||
price_unit: string;
|
||||
transcription_text: string;
|
||||
}
|
||||
|
||||
interface TranscriptionsResponse {
|
||||
transcriptions: Transcription[];
|
||||
next_page_uri: string;
|
||||
}
|
||||
|
||||
const polling: Polling<
|
||||
AppConnectionValueForAuthProperty<typeof twilioAuth>,
|
||||
Record<string, unknown>
|
||||
> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
async items({ auth, lastFetchEpochMS }) {
|
||||
const isTest = lastFetchEpochMS === 0;
|
||||
const account_sid = auth.username;
|
||||
const auth_token = auth.password;
|
||||
|
||||
let currentUri:
|
||||
| string
|
||||
| null = `/2010-04-01/Accounts/${account_sid}/Transcriptions.json?PageSize=${
|
||||
isTest ? 10 : 1000
|
||||
}`;
|
||||
|
||||
const results = [];
|
||||
let stop = false;
|
||||
|
||||
do {
|
||||
const response: any = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `https://api.twilio.com${currentUri}`,
|
||||
authentication: {
|
||||
type: AuthenticationType.BASIC,
|
||||
username: account_sid,
|
||||
password: auth_token,
|
||||
},
|
||||
});
|
||||
|
||||
const payload = response.body as TranscriptionsResponse;
|
||||
|
||||
const transcriptions = payload.transcriptions ?? [];
|
||||
|
||||
for (const recording of transcriptions) {
|
||||
const ts = new Date(recording.date_created).getTime();
|
||||
|
||||
if (recording.status !== 'completed') continue;
|
||||
|
||||
if (isTest || ts > lastFetchEpochMS) {
|
||||
results.push(recording);
|
||||
} else {
|
||||
stop = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isTest) break;
|
||||
currentUri = payload?.next_page_uri ?? null;
|
||||
} while (currentUri && !stop);
|
||||
|
||||
return results.map((recording) => {
|
||||
return {
|
||||
epochMilliSeconds: new Date(recording.date_created).getTime(),
|
||||
data: recording,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const twilioNewTranscription = createTrigger({
|
||||
auth: twilioAuth,
|
||||
name: 'new_transcription',
|
||||
displayName: 'New Transcription',
|
||||
description: 'Triggers when a new call recording transcription is completed.',
|
||||
props: {},
|
||||
sampleData: {
|
||||
"account_sid": "ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"api_version": "2008-08-01",
|
||||
"date_created": "Thu, 25 Aug 2011 20:59:45 +0000",
|
||||
"date_updated": "Thu, 25 Aug 2011 20:59:45 +0000",
|
||||
"duration": "10",
|
||||
"price": "0.00000",
|
||||
"price_unit": "USD",
|
||||
"recording_sid": "REaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"sid": "TRaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"status": "completed",
|
||||
"transcription_text": null,
|
||||
"type": "fast",
|
||||
"uri": "/2010-04-01/Accounts/ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Transcriptions/TRaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.json"
|
||||
},
|
||||
type: TriggerStrategy.POLLING,
|
||||
|
||||
|
||||
async onEnable(context) {
|
||||
await pollingHelper.onEnable(polling, {
|
||||
auth: context.auth,
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
});
|
||||
},
|
||||
async onDisable(context) {
|
||||
await pollingHelper.onDisable(polling, {
|
||||
auth: context.auth,
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
});
|
||||
},
|
||||
async test(context) {
|
||||
return await pollingHelper.test(polling, context);
|
||||
},
|
||||
async run(context) {
|
||||
return await pollingHelper.poll(polling, context);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user