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