diff --git a/packages/backend/src/apps/signalwire/actions/index.ts b/packages/backend/src/apps/signalwire/actions/index.ts
new file mode 100644
index 00000000..d1723dc2
--- /dev/null
+++ b/packages/backend/src/apps/signalwire/actions/index.ts
@@ -0,0 +1,3 @@
+import sendSms from './send-sms';
+
+export default [sendSms];
diff --git a/packages/backend/src/apps/signalwire/actions/send-sms/index.ts b/packages/backend/src/apps/signalwire/actions/send-sms/index.ts
new file mode 100644
index 00000000..fee65fbf
--- /dev/null
+++ b/packages/backend/src/apps/signalwire/actions/send-sms/index.ts
@@ -0,0 +1,63 @@
+import defineAction from '../../../../helpers/define-action';
+
+export default defineAction({
+ name: 'Send an SMS',
+ key: 'sendSms',
+ description: 'Sends an SMS',
+ arguments: [
+ {
+ label: 'From Number',
+ key: 'fromNumber',
+ type: 'dropdown' as const,
+ required: true,
+ description:
+ 'The number to send the SMS from. Include only country code. Example: 491234567890',
+ variables: true,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listIncomingPhoneNumbers',
+ },
+ ],
+ },
+ },
+ {
+ label: 'To Number',
+ key: 'toNumber',
+ type: 'string' as const,
+ required: true,
+ description:
+ 'The number to send the SMS to. Include only country code. Example: 491234567890',
+ variables: true,
+ },
+ {
+ label: 'Message',
+ key: 'message',
+ type: 'string' as const,
+ required: true,
+ description: 'The content of the message.',
+ variables: true,
+ },
+ ],
+
+ async run($) {
+ const requestPath = `/api/laml/2010-04-01/Accounts/${$.auth.data.accountSid}/Messages`;
+
+ const Body = $.step.parameters.message;
+ const From = $.step.parameters.fromNumber;
+ const To = '+' + ($.step.parameters.toNumber as string).trim();
+
+ const response = await $.http.post(requestPath, null, {
+ params: {
+ Body,
+ From,
+ To,
+ }
+ });
+
+ $.setActionItem({ raw: response.data });
+ },
+});
diff --git a/packages/backend/src/apps/signalwire/assets/favicon.svg b/packages/backend/src/apps/signalwire/assets/favicon.svg
new file mode 100644
index 00000000..1dda2037
--- /dev/null
+++ b/packages/backend/src/apps/signalwire/assets/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/backend/src/apps/signalwire/auth/index.ts b/packages/backend/src/apps/signalwire/auth/index.ts
new file mode 100644
index 00000000..32f9c922
--- /dev/null
+++ b/packages/backend/src/apps/signalwire/auth/index.ts
@@ -0,0 +1,65 @@
+import verifyCredentials from './verify-credentials';
+import isStillVerified from './is-still-verified';
+
+export default {
+ fields: [
+ {
+ key: 'accountSid',
+ label: 'Project ID',
+ type: 'string' as const,
+ required: true,
+ readOnly: false,
+ value: null,
+ placeholder: null,
+ description:
+ 'Log into your SignalWire account and find the Project ID',
+ clickToCopy: false,
+ },
+ {
+ key: 'authToken',
+ label: 'API Token',
+ type: 'string' as const,
+ required: true,
+ readOnly: false,
+ value: null,
+ placeholder: null,
+ description: 'API Token in the respective project',
+ clickToCopy: false,
+ },
+ {
+ key: 'spaceRegion',
+ label: 'SignalWire Region',
+ type: 'dropdown' as const,
+ required: true,
+ readOnly: false,
+ value: '',
+ placeholder: null,
+ description: 'Most people should choose the default, "US"',
+ clickToCopy: false,
+ options: [
+ {
+ label: 'US',
+ value: '',
+ },
+ {
+ label: 'EU',
+ value: 'eu-',
+ },
+ ],
+ },
+ {
+ key: 'spaceName',
+ label: 'Space Name',
+ type: 'string' as const,
+ required: true,
+ readOnly: false,
+ value: null,
+ placeholder: null,
+ description: 'Name of your SignalWire space that contains the project',
+ clickToCopy: false,
+ },
+ ],
+
+ verifyCredentials,
+ isStillVerified,
+};
diff --git a/packages/backend/src/apps/signalwire/auth/is-still-verified.ts b/packages/backend/src/apps/signalwire/auth/is-still-verified.ts
new file mode 100644
index 00000000..5a2fe2ae
--- /dev/null
+++ b/packages/backend/src/apps/signalwire/auth/is-still-verified.ts
@@ -0,0 +1,10 @@
+import { IGlobalVariable } from '@automatisch/types';
+import verifyCredentials from './verify-credentials';
+
+const isStillVerified = async ($: IGlobalVariable) => {
+ await verifyCredentials($);
+
+ return true;
+};
+
+export default isStillVerified;
diff --git a/packages/backend/src/apps/signalwire/auth/verify-credentials.ts b/packages/backend/src/apps/signalwire/auth/verify-credentials.ts
new file mode 100644
index 00000000..fbbf5ddc
--- /dev/null
+++ b/packages/backend/src/apps/signalwire/auth/verify-credentials.ts
@@ -0,0 +1,11 @@
+import { IGlobalVariable } from '@automatisch/types';
+
+const verifyCredentials = async ($: IGlobalVariable) => {
+ const { data } = await $.http.get(`/api/laml/2010-04-01/Accounts/${$.auth.data.accountSid}`);
+
+ await $.auth.set({
+ screenName: `${data.friendly_name} (${$.auth.data.accountSid})`,
+ });
+};
+
+export default verifyCredentials;
diff --git a/packages/backend/src/apps/signalwire/common/add-auth-header.ts b/packages/backend/src/apps/signalwire/common/add-auth-header.ts
new file mode 100644
index 00000000..f26c2af2
--- /dev/null
+++ b/packages/backend/src/apps/signalwire/common/add-auth-header.ts
@@ -0,0 +1,27 @@
+import { TBeforeRequest } from '@automatisch/types';
+
+const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
+ const authData = $.auth.data || {};
+
+ requestConfig.headers['Content-Type'] = 'application/x-www-form-urlencoded';
+
+ if (
+ authData.accountSid &&
+ authData.authToken
+ ) {
+ requestConfig.auth = {
+ username: authData.accountSid as string,
+ password: authData.authToken as string,
+ };
+ }
+
+ if (authData.spaceName) {
+ const serverUrl = `https://${authData.spaceName}.${authData.spaceRegion}signalwire.com`;
+
+ requestConfig.baseURL = serverUrl as string;
+ }
+
+ return requestConfig;
+};
+
+export default addAuthHeader;
diff --git a/packages/backend/src/apps/signalwire/dynamic-data/index.ts b/packages/backend/src/apps/signalwire/dynamic-data/index.ts
new file mode 100644
index 00000000..d20d4347
--- /dev/null
+++ b/packages/backend/src/apps/signalwire/dynamic-data/index.ts
@@ -0,0 +1,3 @@
+import listIncomingPhoneNumbers from './list-incoming-phone-numbers';
+
+export default [listIncomingPhoneNumbers];
diff --git a/packages/backend/src/apps/signalwire/dynamic-data/list-incoming-phone-numbers/index.ts b/packages/backend/src/apps/signalwire/dynamic-data/list-incoming-phone-numbers/index.ts
new file mode 100644
index 00000000..969d2e45
--- /dev/null
+++ b/packages/backend/src/apps/signalwire/dynamic-data/list-incoming-phone-numbers/index.ts
@@ -0,0 +1,57 @@
+import { IGlobalVariable, IJSONObject } from '@automatisch/types';
+
+type TAggregatedResponse = {
+ data: IJSONObject[];
+ error?: IJSONObject;
+};
+
+type TResponse = {
+ incoming_phone_numbers: TIncomingPhoneNumber[];
+ next_page_uri: string;
+};
+
+type TIncomingPhoneNumber = {
+ capabilities: {
+ sms: boolean;
+ };
+ sid: string;
+ friendly_name: string;
+ phone_number: string;
+};
+
+export default {
+ name: 'List incoming phone numbers',
+ key: 'listIncomingPhoneNumbers',
+
+ async run($: IGlobalVariable) {
+ let requestPath = `/api/laml/2010-04-01/Accounts/${$.auth.data.accountSid}/IncomingPhoneNumbers`;
+
+ const aggregatedResponse: TAggregatedResponse = {
+ data: [],
+ };
+
+ do {
+ const { data } = await $.http.get(requestPath);
+
+ const smsCapableIncomingPhoneNumbers = data.incoming_phone_numbers
+ .filter((incomingPhoneNumber) => {
+ return incomingPhoneNumber.capabilities.sms;
+ })
+ .map((incomingPhoneNumber) => {
+ const friendlyName = incomingPhoneNumber.friendly_name;
+ const phoneNumber = incomingPhoneNumber.phone_number;
+ const name = [friendlyName, phoneNumber].filter(Boolean).join(' - ');
+
+ return {
+ value: phoneNumber,
+ name,
+ };
+ })
+ aggregatedResponse.data.push(...smsCapableIncomingPhoneNumbers)
+
+ requestPath = data.next_page_uri;
+ } while (requestPath);
+
+ return aggregatedResponse;
+ },
+};
diff --git a/packages/backend/src/apps/signalwire/index.d.ts b/packages/backend/src/apps/signalwire/index.d.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/backend/src/apps/signalwire/index.ts b/packages/backend/src/apps/signalwire/index.ts
new file mode 100644
index 00000000..658a9a90
--- /dev/null
+++ b/packages/backend/src/apps/signalwire/index.ts
@@ -0,0 +1,22 @@
+import defineApp from '../../helpers/define-app';
+import addAuthHeader from './common/add-auth-header';
+import auth from './auth';
+import triggers from './triggers';
+import actions from './actions';
+import dynamicData from './dynamic-data';
+
+export default defineApp({
+ name: 'SignalWire',
+ key: 'signalwire',
+ iconUrl: '{BASE_URL}/apps/signalwire/assets/favicon.svg',
+ authDocUrl: 'https://automatisch.io/docs/apps/signalwire/connection',
+ supportsConnections: true,
+ baseUrl: 'https://signalwire.com',
+ apiBaseUrl: '',
+ primaryColor: '044cf6',
+ beforeRequest: [addAuthHeader],
+ auth,
+ triggers,
+ actions,
+ dynamicData,
+});
diff --git a/packages/backend/src/apps/signalwire/triggers/index.ts b/packages/backend/src/apps/signalwire/triggers/index.ts
new file mode 100644
index 00000000..04e1504d
--- /dev/null
+++ b/packages/backend/src/apps/signalwire/triggers/index.ts
@@ -0,0 +1,3 @@
+import receiveSms from './receive-sms';
+
+export default [receiveSms];
diff --git a/packages/backend/src/apps/signalwire/triggers/receive-sms/fetch-messages.ts b/packages/backend/src/apps/signalwire/triggers/receive-sms/fetch-messages.ts
new file mode 100644
index 00000000..425c5b20
--- /dev/null
+++ b/packages/backend/src/apps/signalwire/triggers/receive-sms/fetch-messages.ts
@@ -0,0 +1,27 @@
+import { IGlobalVariable, IJSONObject } from '@automatisch/types';
+
+const fetchMessages = async ($: IGlobalVariable) => {
+ const toNumber = $.step.parameters.toNumber as string;
+
+ let response;
+ let requestPath = `/api/laml/2010-04-01/Accounts/${$.auth.data.accountSid}/Messages?To=${toNumber}`;
+
+ do {
+ response = await $.http.get(requestPath);
+
+ response.data.messages.forEach((message: IJSONObject) => {
+ const dataItem = {
+ raw: message,
+ meta: {
+ internalId: message.date_sent as string,
+ },
+ };
+
+ $.pushTriggerItem(dataItem);
+ });
+
+ requestPath = response.data.next_page_uri;
+ } while (requestPath);
+};
+
+export default fetchMessages;
diff --git a/packages/backend/src/apps/signalwire/triggers/receive-sms/index.ts b/packages/backend/src/apps/signalwire/triggers/receive-sms/index.ts
new file mode 100644
index 00000000..896244fa
--- /dev/null
+++ b/packages/backend/src/apps/signalwire/triggers/receive-sms/index.ts
@@ -0,0 +1,33 @@
+import defineTrigger from '../../../../helpers/define-trigger';
+import fetchMessages from './fetch-messages';
+
+export default defineTrigger({
+ name: 'Receive SMS',
+ key: 'receiveSms',
+ pollInterval: 15,
+ description: 'Triggers when a new SMS is received.',
+ arguments: [
+ {
+ label: 'To Number',
+ key: 'toNumber',
+ type: 'dropdown',
+ required: true,
+ description:
+ 'The number to receive the SMS on. It should be a SignalWire number in your project.',
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listIncomingPhoneNumbers',
+ },
+ ],
+ },
+ },
+ ],
+
+ async run($) {
+ await fetchMessages($);
+ },
+});
diff --git a/packages/backend/src/graphql/mutations/create-flow.ts b/packages/backend/src/graphql/mutations/create-flow.ts
index 30c0968f..7243d8c5 100644
--- a/packages/backend/src/graphql/mutations/create-flow.ts
+++ b/packages/backend/src/graphql/mutations/create-flow.ts
@@ -17,7 +17,9 @@ const createFlow = async (
const connectionId = params?.input?.connectionId;
const appKey = params?.input?.triggerAppKey;
- await App.findOneByKey(appKey);
+ if (appKey) {
+ await App.findOneByKey(appKey);
+ }
const flow = await context.currentUser.$relatedQuery('flows').insert({
name: 'Name your flow',
diff --git a/packages/backend/src/models/app.ts b/packages/backend/src/models/app.ts
index d50d3426..2755c6a7 100644
--- a/packages/backend/src/models/app.ts
+++ b/packages/backend/src/models/app.ts
@@ -40,6 +40,8 @@ class App {
static async checkAppAndAction(appKey: string, actionKey: string): Promise {
const app = await this.findOneByKey(appKey);
+ if (!actionKey) return;
+
const hasAction = app.actions?.find(action => action.key === actionKey);
if (!hasAction) {
@@ -50,6 +52,8 @@ class App {
static async checkAppAndTrigger(appKey: string, triggerKey: string): Promise {
const app = await this.findOneByKey(appKey);
+ if (!triggerKey) return;
+
const hasTrigger = app.triggers?.find(trigger => trigger.key === triggerKey);
if (!hasTrigger) {
diff --git a/packages/docs/pages/.vitepress/config.js b/packages/docs/pages/.vitepress/config.js
index 3a16f115..59f53567 100644
--- a/packages/docs/pages/.vitepress/config.js
+++ b/packages/docs/pages/.vitepress/config.js
@@ -133,6 +133,16 @@ export default defineConfig({
{ text: 'Connection', link: '/apps/scheduler/connection' },
],
},
+ {
+ text: 'SignalWire',
+ collapsible: true,
+ collapsed: true,
+ items: [
+ { text: 'Triggers', link: '/apps/signalwire/triggers' },
+ { text: 'Actions', link: '/apps/signalwire/actions' },
+ { text: 'Connection', link: '/apps/signalwire/connection' },
+ ],
+ },
{
text: 'Slack',
collapsible: true,
diff --git a/packages/docs/pages/apps/signalwire/actions.md b/packages/docs/pages/apps/signalwire/actions.md
new file mode 100644
index 00000000..bc89edd2
--- /dev/null
+++ b/packages/docs/pages/apps/signalwire/actions.md
@@ -0,0 +1,12 @@
+---
+favicon: /favicons/signalwire.svg
+items:
+ - name: Send an SMS
+ desc: Sends an SMS
+---
+
+
+
+
diff --git a/packages/docs/pages/apps/signalwire/connection.md b/packages/docs/pages/apps/signalwire/connection.md
new file mode 100644
index 00000000..20a61005
--- /dev/null
+++ b/packages/docs/pages/apps/signalwire/connection.md
@@ -0,0 +1,16 @@
+# SignalWire
+
+:::info
+This page explains the steps you need to follow to set up a SignalWire connection in Automatisch. If any of the steps are outdated, please let us know!
+:::
+
+1. Go to the SignalWire API page in your respective project (https://{space}.signalwire.com/credentials)
+2. Copy **Project ID** and paste it to the **Project ID** field on the
+ Automatisch connection creation page.
+3. Create/Copy **API Token** and paste it to the **API Token** field on the
+ Automatisch connection creation page.
+4. Select your **Region** (US for most users).
+5. Provide your **Space Name** from the URL and paste it to the **Space NAME** field on the
+ Automatisch connection creation page.
+6. Click **Submit** button on Automatisch.
+7. Now you can start using the new SignalWire connection!
diff --git a/packages/docs/pages/apps/signalwire/triggers.md b/packages/docs/pages/apps/signalwire/triggers.md
new file mode 100644
index 00000000..42083cc8
--- /dev/null
+++ b/packages/docs/pages/apps/signalwire/triggers.md
@@ -0,0 +1,12 @@
+---
+favicon: /favicons/signalwire.svg
+items:
+ - name: Receive SMS
+ desc: Triggers when a new SMS is received.
+---
+
+
+
+
diff --git a/packages/docs/pages/build-integrations/examples.md b/packages/docs/pages/build-integrations/examples.md
index 308be03f..3acb1c80 100644
--- a/packages/docs/pages/build-integrations/examples.md
+++ b/packages/docs/pages/build-integrations/examples.md
@@ -33,6 +33,7 @@ The build integrations section is best understood when read from beginning to en
- [DeepL](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/deepl/auth/index.ts)
- [Twilio](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twilio/auth/index.ts)
+- [SignalWire](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/signalwire/auth/index.ts)
- [SMTP](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/smtp/auth/index.ts)
### Without authentication
@@ -60,6 +61,7 @@ If you are developing a webhook-based trigger, you need to ensure that the webho
- [Search tweets - Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/triggers/search-tweets/index.ts)
- [New issues - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-issues/index.ts)
- [Receive SMS - Twilio](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twilio/triggers/receive-sms/index.ts)
+- [Receive SMS - SignalWire](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/signalwire/triggers/receive-sms/index.ts)
- [New photos - Flickr](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/flickr/triggers/new-photos/index.ts)
### Pagination with ascending order
diff --git a/packages/docs/pages/guide/available-apps.md b/packages/docs/pages/guide/available-apps.md
index 5874ca0c..33b9bb2f 100644
--- a/packages/docs/pages/guide/available-apps.md
+++ b/packages/docs/pages/guide/available-apps.md
@@ -17,6 +17,7 @@ Following integrations are currently supported by Automatisch.
- [RSS](/apps/rss/triggers)
- [Salesforce](/apps/salesforce/triggers)
- [Scheduler](/apps/scheduler/triggers)
+- [SignalWire](/apps/signalwire/triggers)
- [Slack](/apps/slack/actions)
- [SMTP](/apps/smtp/actions)
- [Stripe](/apps/stripe/triggers)
diff --git a/packages/docs/pages/public/favicons/signalwire.svg b/packages/docs/pages/public/favicons/signalwire.svg
new file mode 100644
index 00000000..1dda2037
--- /dev/null
+++ b/packages/docs/pages/public/favicons/signalwire.svg
@@ -0,0 +1 @@
+
\ No newline at end of file