From a3d50e276611477c5c094271b429498f03254c9e Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Mon, 18 Sep 2023 22:41:29 +0200 Subject: [PATCH] feat(vonage): Introduce Vonage API receive and send message integrations --- .../backend/src/apps/vonage/actions/index.ts | 3 + .../apps/vonage/actions/send-message/index.ts | 82 +++++++++++++++++++ .../src/apps/vonage/assets/favicon.svg | 4 + .../backend/src/apps/vonage/auth/index.ts | 44 ++++++++++ .../src/apps/vonage/auth/is-still-verified.ts | 9 ++ .../apps/vonage/auth/verify-credentials.ts | 13 +++ packages/backend/src/apps/vonage/index.d.ts | 0 packages/backend/src/apps/vonage/index.ts | 18 ++++ .../backend/src/apps/vonage/triggers/index.ts | 3 + .../vonage/triggers/receive-message/index.ts | 63 ++++++++++++++ .../controllers/webhooks/handler-by-app.ts | 34 ++++++++ packages/backend/src/routes/webhooks.ts | 56 +++++++++---- 12 files changed, 311 insertions(+), 18 deletions(-) create mode 100644 packages/backend/src/apps/vonage/actions/index.ts create mode 100644 packages/backend/src/apps/vonage/actions/send-message/index.ts create mode 100644 packages/backend/src/apps/vonage/assets/favicon.svg create mode 100644 packages/backend/src/apps/vonage/auth/index.ts create mode 100644 packages/backend/src/apps/vonage/auth/is-still-verified.ts create mode 100644 packages/backend/src/apps/vonage/auth/verify-credentials.ts create mode 100644 packages/backend/src/apps/vonage/index.d.ts create mode 100644 packages/backend/src/apps/vonage/index.ts create mode 100644 packages/backend/src/apps/vonage/triggers/index.ts create mode 100644 packages/backend/src/apps/vonage/triggers/receive-message/index.ts create mode 100644 packages/backend/src/controllers/webhooks/handler-by-app.ts diff --git a/packages/backend/src/apps/vonage/actions/index.ts b/packages/backend/src/apps/vonage/actions/index.ts new file mode 100644 index 00000000..37aeb338 --- /dev/null +++ b/packages/backend/src/apps/vonage/actions/index.ts @@ -0,0 +1,3 @@ +import sendMessage from './send-message'; + +export default [sendMessage]; diff --git a/packages/backend/src/apps/vonage/actions/send-message/index.ts b/packages/backend/src/apps/vonage/actions/send-message/index.ts new file mode 100644 index 00000000..7489ffba --- /dev/null +++ b/packages/backend/src/apps/vonage/actions/send-message/index.ts @@ -0,0 +1,82 @@ +import defineAction from '../../../../helpers/define-action'; + +export default defineAction({ + name: 'Send Message', + key: 'sendMessage', + description: 'Send a message to a number.', + arguments: [ + { + label: 'Message Type', + key: 'messageType', + type: 'string' as const, + required: true, + description: 'The type of message to send. e.g. text', + variables: true, + }, + { + label: 'Channel', + key: 'channel', + type: 'string' as const, + required: true, + description: + 'The channel to send the message through. e.g. sms, whatsapp', + variables: true, + }, + { + label: 'From Number', + key: 'fromNumber', + type: 'string' as const, + required: true, + description: + 'The number to send the message from. Include country code. Example: 15551234567', + variables: true, + }, + { + label: 'To Number', + key: 'toNumber', + type: 'string' as const, + required: true, + description: + 'The number to send the message to. Include country code. Example: 15551234567', + variables: true, + }, + { + label: 'Message', + key: 'message', + type: 'string' as const, + required: true, + description: 'The message to send.', + variables: true, + }, + ], + + async run($) { + const messageType = $.step.parameters.messageType as string; + const channel = $.step.parameters.channel as string; + const messageBody = $.step.parameters.message as string; + const fromNumber = ($.step.parameters.fromNumber as string).trim(); + const toNumber = ($.step.parameters.toNumber as string).trim(); + + const basicAuthToken = Buffer.from( + `${$.auth.data.apiKey}:${$.auth.data.apiSecret}` + ).toString('base64'); + + const headers = { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: `Basic ${basicAuthToken}`, + }; + + const payload = { + message_type: messageType, + text: messageBody, + to: toNumber, + from: fromNumber, + channel, + }; + + const response = await $.http.post('/messages', payload, { headers }); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/vonage/assets/favicon.svg b/packages/backend/src/apps/vonage/assets/favicon.svg new file mode 100644 index 00000000..9e9c03a4 --- /dev/null +++ b/packages/backend/src/apps/vonage/assets/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/backend/src/apps/vonage/auth/index.ts b/packages/backend/src/apps/vonage/auth/index.ts new file mode 100644 index 00000000..b3473a66 --- /dev/null +++ b/packages/backend/src/apps/vonage/auth/index.ts @@ -0,0 +1,44 @@ +import verifyCredentials from './verify-credentials'; +import isStillVerified from './is-still-verified'; + +export default { + fields: [ + { + key: 'screenName', + label: 'Screen Name', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Screen name of your connection to be used on Automatisch UI.', + clickToCopy: false, + }, + { + key: 'apiKey', + label: 'API Key', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'API Key from Vonage.', + clickToCopy: false, + }, + { + key: 'apiSecret', + label: 'API Secret', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'API Secret from Vonage.', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/vonage/auth/is-still-verified.ts b/packages/backend/src/apps/vonage/auth/is-still-verified.ts new file mode 100644 index 00000000..66bb963e --- /dev/null +++ b/packages/backend/src/apps/vonage/auth/is-still-verified.ts @@ -0,0 +1,9 @@ +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/vonage/auth/verify-credentials.ts b/packages/backend/src/apps/vonage/auth/verify-credentials.ts new file mode 100644 index 00000000..e7ff9739 --- /dev/null +++ b/packages/backend/src/apps/vonage/auth/verify-credentials.ts @@ -0,0 +1,13 @@ +import { IGlobalVariable } from '@automatisch/types'; + +const verifyCredentials = async ($: IGlobalVariable) => { + await $.http.get( + `https://rest.nexmo.com/account/get-balance?api_key=${$.auth.data.apiKey}&api_secret=${$.auth.data.apiSecret}` + ); + + await $.auth.set({ + screenName: $.auth.data.screenName, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/vonage/index.d.ts b/packages/backend/src/apps/vonage/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/backend/src/apps/vonage/index.ts b/packages/backend/src/apps/vonage/index.ts new file mode 100644 index 00000000..18bfb080 --- /dev/null +++ b/packages/backend/src/apps/vonage/index.ts @@ -0,0 +1,18 @@ +import defineApp from '../../helpers/define-app'; +import auth from './auth'; +import triggers from './triggers'; +import actions from './actions'; + +export default defineApp({ + name: 'Vonage', + key: 'vonage', + iconUrl: '{BASE_URL}/apps/vonage/assets/favicon.svg', + authDocUrl: 'https://automatisch.io/docs/apps/vonage/connection', + supportsConnections: true, + baseUrl: 'https://vonage.com', + apiBaseUrl: 'https://messages-sandbox.nexmo.com/v1', + primaryColor: '000000', + auth, + triggers, + actions, +}); diff --git a/packages/backend/src/apps/vonage/triggers/index.ts b/packages/backend/src/apps/vonage/triggers/index.ts new file mode 100644 index 00000000..57a2289e --- /dev/null +++ b/packages/backend/src/apps/vonage/triggers/index.ts @@ -0,0 +1,3 @@ +import receiveMessage from './receive-message'; + +export default [receiveMessage]; diff --git a/packages/backend/src/apps/vonage/triggers/receive-message/index.ts b/packages/backend/src/apps/vonage/triggers/receive-message/index.ts new file mode 100644 index 00000000..333ef822 --- /dev/null +++ b/packages/backend/src/apps/vonage/triggers/receive-message/index.ts @@ -0,0 +1,63 @@ +import defineTrigger from '../../../../helpers/define-trigger'; +import isEmpty from 'lodash/isEmpty'; + +export default defineTrigger({ + name: 'Receive message - Sandbox', + key: 'receiveMessage', + type: 'webhook', + description: + 'Triggers when a message is received from Vonage sandbox number. (+14157386102)', + arguments: [ + { + label: 'From Number', + key: 'fromNumber', + type: 'string' as const, + required: true, + description: + 'The number from which the message was sent. (e.g. 491234567899)', + variables: true, + }, + ], + + async testRun($) { + const lastExecutionStep = await $.getLastExecutionStep(); + + if (!isEmpty(lastExecutionStep?.dataOut)) { + $.pushTriggerItem({ + raw: lastExecutionStep.dataOut, + meta: { + internalId: '', + }, + }); + } else { + const sampleData = { + to: '14157386102', + from: $.step.parameters.fromNumber as string, + text: 'Tell me how will be the weather tomorrow for Berlin?', + channel: 'whatsapp', + profile: { + name: 'Sample User', + }, + timestamp: '2023-09-18T19:52:36Z', + message_type: 'text', + message_uuid: '318960cc-16ff-4f66-8397-45574333a435', + context_status: 'none', + }; + + $.pushTriggerItem({ + raw: sampleData, + meta: { + internalId: '', + }, + }); + } + }, + + async registerHook() { + // void + }, + + async unregisterHook() { + // void + }, +}); diff --git a/packages/backend/src/controllers/webhooks/handler-by-app.ts b/packages/backend/src/controllers/webhooks/handler-by-app.ts new file mode 100644 index 00000000..415337d1 --- /dev/null +++ b/packages/backend/src/controllers/webhooks/handler-by-app.ts @@ -0,0 +1,34 @@ +import { Response } from 'express'; +import { IRequest } from '@automatisch/types'; + +import Step from '../../models/step'; +import logger from '../../helpers/logger'; +import handler from '../../helpers/webhook-handler'; + +export default async (request: IRequest, response: Response) => { + const computedRequestPayload = { + headers: request.headers, + body: request.body, + query: request.query, + params: request.params, + }; + logger.debug(`Handling incoming webhook request at ${request.originalUrl}.`); + logger.debug(JSON.stringify(computedRequestPayload, null, 2)); + + const triggerSteps = await Step.query().where({ + type: 'trigger', + app_key: 'vonage', + key: 'receiveMessage', + }); + + if (triggerSteps.length === 0) return response.sendStatus(404); + + for (const triggerStep of triggerSteps) { + const flow = await triggerStep.$relatedQuery('flow'); + if (flow.status !== 'published') continue; + + await handler(triggerStep.flowId, request, response); + } + + response.sendStatus(204); +}; diff --git a/packages/backend/src/routes/webhooks.ts b/packages/backend/src/routes/webhooks.ts index fc24ca2a..765557d4 100644 --- a/packages/backend/src/routes/webhooks.ts +++ b/packages/backend/src/routes/webhooks.ts @@ -1,32 +1,45 @@ -import express, { Response, Router, NextFunction, RequestHandler } from 'express'; +import express, { + Response, + Router, + NextFunction, + RequestHandler, +} from 'express'; import multer from 'multer'; import { IRequest } from '@automatisch/types'; import appConfig from '../config/app'; import webhookHandlerByFlowId from '../controllers/webhooks/handler-by-flow-id'; import webhookHandlerByConnectionIdAndRefValue from '../controllers/webhooks/handler-by-connection-id-and-ref-value'; +import webhookHandlerByApp from '../controllers/webhooks/handler-by-app'; const router = Router(); const upload = multer(); router.use(upload.none()); -router.use(express.text({ - limit: appConfig.requestBodySizeLimit, - verify(req, res, buf) { - (req as IRequest).rawBody = buf; - }, -})); +router.use( + express.text({ + limit: appConfig.requestBodySizeLimit, + verify(req, res, buf) { + (req as IRequest).rawBody = buf; + }, + }) +); -const exposeError = (handler: RequestHandler) => async (req: IRequest, res: Response, next: NextFunction) => { - try { - await handler(req, res, next); - } catch (err) { - next(err); - } -} +const exposeError = + (handler: RequestHandler) => + async (req: IRequest, res: Response, next: NextFunction) => { + try { + await handler(req, res, next); + } catch (err) { + next(err); + } + }; -function createRouteHandler(path: string, handler: (req: IRequest, res: Response, next: NextFunction) => void) { +function createRouteHandler( + path: string, + handler: (req: IRequest, res: Response, next: NextFunction) => void +) { const wrappedHandler = exposeError(handler); router @@ -35,11 +48,18 @@ function createRouteHandler(path: string, handler: (req: IRequest, res: Response .put(wrappedHandler) .patch(wrappedHandler) .post(wrappedHandler); -}; +} -createRouteHandler('/connections/:connectionId/:refValue', webhookHandlerByConnectionIdAndRefValue); -createRouteHandler('/connections/:connectionId', webhookHandlerByConnectionIdAndRefValue); +createRouteHandler( + '/connections/:connectionId/:refValue', + webhookHandlerByConnectionIdAndRefValue +); +createRouteHandler( + '/connections/:connectionId', + webhookHandlerByConnectionIdAndRefValue +); createRouteHandler('/flows/:flowId', webhookHandlerByFlowId); createRouteHandler('/:flowId', webhookHandlerByFlowId); +createRouteHandler('/apps/vonage', webhookHandlerByApp); export default router;