From c61b2f5cc87d609ec96d64aa380ada2c83216425 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Fri, 20 Dec 2024 17:36:16 +0000 Subject: [PATCH 01/19] Add env var injection to AS_DEV --- .../aselo_development_custom/action.yml | 5 ++++ .../webhooks/taskrouterCallback.protected.ts | 28 +++++++++++++++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/.github/actions/custom-actions/aselo_development_custom/action.yml b/.github/actions/custom-actions/aselo_development_custom/action.yml index 5b1baa2d..41835a16 100644 --- a/.github/actions/custom-actions/aselo_development_custom/action.yml +++ b/.github/actions/custom-actions/aselo_development_custom/action.yml @@ -254,4 +254,9 @@ runs: - name: Add MODICA_TEST_SEND_MESSAGE_URL run: echo "MODICA_TEST_SEND_MESSAGE_URL=${{ env.MODICA_TEST_SEND_MESSAGE_URL }}" >> .env + shell: bash + + + - name: Add DELEGATE_WEBHOOK_URL + run: echo "DELEGATE_WEBHOOK_URL=https://hrm-development.tl.techmatters.org/lambda/twilio/account-scoped" >> .env shell: bash \ No newline at end of file diff --git a/functions/webhooks/taskrouterCallback.protected.ts b/functions/webhooks/taskrouterCallback.protected.ts index 3b0db208..6c726ca1 100644 --- a/functions/webhooks/taskrouterCallback.protected.ts +++ b/functions/webhooks/taskrouterCallback.protected.ts @@ -31,6 +31,12 @@ const LISTENERS_FOLDER = 'taskrouterListeners/'; type EnvVars = { TWILIO_WORKSPACE_SID: string; CHAT_SERVICE_SID: string; + DELEGATE_WEBHOOK_URL: string; + ACCOUNT_SID: string; +}; + +type EventFieldsWithRequest = EventFields & { + request: { headers: Record }; }; /** @@ -45,13 +51,13 @@ const getListeners = (): [string, TaskrouterListener][] => { const runTaskrouterListeners = async ( context: Context, - event: EventFields, + event: EventFieldsWithRequest, callback: ServerlessCallback, ) => { const listeners = getListeners(); - await Promise.all( - listeners + await Promise.all([ + ...listeners .filter(([, listener]) => listener.shouldHandle(event)) .map(async ([path, listener]) => { console.debug( @@ -66,12 +72,24 @@ const runTaskrouterListeners = async ( `===== Successfully executed listener at ${path} for event: ${event.EventType}, task: ${event.TaskSid} =====`, ); }), - ); + async () => { + if (context.DELEGATE_WEBHOOK_URL) { + return fetch(`${context.DELEGATE_WEBHOOK_URL}/${context.ACCOUNT_SID}${context.PATH}`, { + method: 'POST', + headers: { + 'X-Original-Webhook-Url': `https//:${context.DOMAIN_NAME}${context.PATH}`, + ...event.request.headers, + }, + }); + } + return Promise.resolve(); + }, + ]); }; export const handler = async ( context: Context, - event: EventFields, + event: EventFieldsWithRequest, callback: ServerlessCallback, ) => { const response = responseWithCors(); From 34851dc3e519a1943e82a2fd24d1dff3740e83c5 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Mon, 23 Dec 2024 08:43:03 +0000 Subject: [PATCH 02/19] Logging --- functions/webhooks/taskrouterCallback.protected.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/functions/webhooks/taskrouterCallback.protected.ts b/functions/webhooks/taskrouterCallback.protected.ts index 6c726ca1..64113b0f 100644 --- a/functions/webhooks/taskrouterCallback.protected.ts +++ b/functions/webhooks/taskrouterCallback.protected.ts @@ -74,7 +74,9 @@ const runTaskrouterListeners = async ( }), async () => { if (context.DELEGATE_WEBHOOK_URL) { - return fetch(`${context.DELEGATE_WEBHOOK_URL}/${context.ACCOUNT_SID}${context.PATH}`, { + const delegateUrl = `${context.DELEGATE_WEBHOOK_URL}/${context.ACCOUNT_SID}${context.PATH}`; + console.info('Forwarding event to delegate webhook:', delegateUrl); + return fetch(delegateUrl, { method: 'POST', headers: { 'X-Original-Webhook-Url': `https//:${context.DOMAIN_NAME}${context.PATH}`, From 50ef56acb1eb7b15ec14e11d285e9b6085737482 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Mon, 23 Dec 2024 09:14:49 +0000 Subject: [PATCH 03/19] Logging --- .../webhooks/taskrouterCallback.protected.ts | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/functions/webhooks/taskrouterCallback.protected.ts b/functions/webhooks/taskrouterCallback.protected.ts index 64113b0f..cfc9c6b1 100644 --- a/functions/webhooks/taskrouterCallback.protected.ts +++ b/functions/webhooks/taskrouterCallback.protected.ts @@ -56,8 +56,20 @@ const runTaskrouterListeners = async ( ) => { const listeners = getListeners(); - await Promise.all([ - ...listeners + if (context.DELEGATE_WEBHOOK_URL) { + const delegateUrl = `${context.DELEGATE_WEBHOOK_URL}/${context.ACCOUNT_SID}${context.PATH}`; + console.info('Forwarding event to delegate webhook:', delegateUrl); + // Fire and forget + fetch(delegateUrl, { + method: 'POST', + headers: { + 'X-Original-Webhook-Url': `https//:${context.DOMAIN_NAME}${context.PATH}`, + ...event.request.headers, + }, + }); + } + await Promise.all( + listeners .filter(([, listener]) => listener.shouldHandle(event)) .map(async ([path, listener]) => { console.debug( @@ -72,21 +84,7 @@ const runTaskrouterListeners = async ( `===== Successfully executed listener at ${path} for event: ${event.EventType}, task: ${event.TaskSid} =====`, ); }), - async () => { - if (context.DELEGATE_WEBHOOK_URL) { - const delegateUrl = `${context.DELEGATE_WEBHOOK_URL}/${context.ACCOUNT_SID}${context.PATH}`; - console.info('Forwarding event to delegate webhook:', delegateUrl); - return fetch(delegateUrl, { - method: 'POST', - headers: { - 'X-Original-Webhook-Url': `https//:${context.DOMAIN_NAME}${context.PATH}`, - ...event.request.headers, - }, - }); - } - return Promise.resolve(); - }, - ]); + ); }; export const handler = async ( From 5f352a353b193b143fa0e8b7bc709c15360cdcd0 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Mon, 23 Dec 2024 09:19:28 +0000 Subject: [PATCH 04/19] Remove redundant flex flow SIDs from AS_DEV context to reduce context size --- .../aselo_development_custom/action.yml | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/.github/actions/custom-actions/aselo_development_custom/action.yml b/.github/actions/custom-actions/aselo_development_custom/action.yml index 41835a16..a7c5f80c 100644 --- a/.github/actions/custom-actions/aselo_development_custom/action.yml +++ b/.github/actions/custom-actions/aselo_development_custom/action.yml @@ -74,11 +74,6 @@ runs: with: ssm_parameter: "FACEBOOK_PAGE_ACCESS_TOKEN_105220114492262_Aselo-Development" env_variable_name: "FACEBOOK_PAGE_ACCESS_TOKEN" - - name: Set helpline Instagram Flex Flow SID - uses: "marvinpinto/action-inject-ssm-secrets@latest" - with: - ssm_parameter: "DEV_TWILIO_AS_INSTAGRAM_FLEX_FLOW_SID" - env_variable_name: "INSTAGRAM_FLEX_FLOW_SID" - name: Set helpline Instagram Studio Flow SID uses: "marvinpinto/action-inject-ssm-secrets@latest" with: @@ -90,11 +85,6 @@ runs: ssm_parameter: "/development/instagram/${{inputs.account-sid}}/messaging_mode" env_variable_name: "INSTAGRAM_TWILIO_MESSAGING_MODE" # Line environment variables - - name: Set helpline Line Flex Flow SID - uses: "marvinpinto/action-inject-ssm-secrets@latest" - with: - ssm_parameter: "DEV_TWILIO_AS_LINE_FLEX_FLOW_SID" - env_variable_name: "LINE_FLEX_FLOW_SID" - name: Set Line Channel Secret uses: "marvinpinto/action-inject-ssm-secrets@latest" with: @@ -126,11 +116,6 @@ runs: with: ssm_parameter: "/development/modica/${{inputs.account-sid}}/app_password" env_variable_name: "MODICA_APP_PASSWORD" - - name: Set Modica Flex Flow Sid - uses: "marvinpinto/action-inject-ssm-secrets@latest" - with: - ssm_parameter: "/development/twilio/${{inputs.account-sid}}/modica_flex_flow_sid" - env_variable_name: "MODICA_FLEX_FLOW_SID" - name: Set helpline Modica Studio Flow SID uses: "marvinpinto/action-inject-ssm-secrets@latest" with: @@ -203,18 +188,12 @@ runs: - name: Add FACEBOOK_PAGE_ACCESS_TOKEN run: echo "FACEBOOK_PAGE_ACCESS_TOKEN=${{ env.FACEBOOK_PAGE_ACCESS_TOKEN }}" >> .env shell: bash - - name: Add INSTAGRAM_FLEX_FLOW_SID - run: echo "INSTAGRAM_FLEX_FLOW_SID=${{ env.INSTAGRAM_FLEX_FLOW_SID }}" >> .env - shell: bash - name: Add INSTAGRAM_STUDIO_FLOW_SID run: echo "INSTAGRAM_STUDIO_FLOW_SID=${{ env.INSTAGRAM_STUDIO_FLOW_SID }}" >> .env shell: bash - name: Add INSTAGRAM_TWILIO_MESSAGING_MODE run: echo "INSTAGRAM_TWILIO_MESSAGING_MODE=${{ env.INSTAGRAM_TWILIO_MESSAGING_MODE }}" >> .env shell: bash - - name: Add LINE_FLEX_FLOW_SID - run: echo "LINE_FLEX_FLOW_SID=${{ env.LINE_FLEX_FLOW_SID }}" >> .env - shell: bash - name: Add LINE_CHANNEL_SECRET run: echo "LINE_CHANNEL_SECRET=${{ env.LINE_CHANNEL_SECRET }}" >> .env shell: bash @@ -233,9 +212,6 @@ runs: - name: Add MODICA_APP_PASSWORD run: echo "MODICA_APP_PASSWORD=${{ env.MODICA_APP_PASSWORD }}" >> .env shell: bash - - name: Add MODICA_FLEX_FLOW_SID - run: echo "MODICA_FLEX_FLOW_SID=${{ env.MODICA_FLEX_FLOW_SID }}" >> .env - shell: bash - name: Add MODICA_STUDIO_FLOW_SID run: echo "MODICA_STUDIO_FLOW_SID=${{ env.MODICA_STUDIO_FLOW_SID }}" >> .env shell: bash From cc2765bbc55887597435acc88abe6c7220614c9f Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Mon, 23 Dec 2024 17:18:59 +0000 Subject: [PATCH 05/19] Fix original webhook URL --- functions/webhooks/taskrouterCallback.protected.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/webhooks/taskrouterCallback.protected.ts b/functions/webhooks/taskrouterCallback.protected.ts index cfc9c6b1..4ca6f0ce 100644 --- a/functions/webhooks/taskrouterCallback.protected.ts +++ b/functions/webhooks/taskrouterCallback.protected.ts @@ -63,7 +63,7 @@ const runTaskrouterListeners = async ( fetch(delegateUrl, { method: 'POST', headers: { - 'X-Original-Webhook-Url': `https//:${context.DOMAIN_NAME}${context.PATH}`, + 'X-Original-Webhook-Url': `https://${context.DOMAIN_NAME}${context.PATH}`, ...event.request.headers, }, }); From c236a15ad09021216dcdd71a4adf27eda586eded Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Mon, 23 Dec 2024 17:34:29 +0000 Subject: [PATCH 06/19] Include body in delegated webhook call --- functions/webhooks/taskrouterCallback.protected.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/functions/webhooks/taskrouterCallback.protected.ts b/functions/webhooks/taskrouterCallback.protected.ts index 4ca6f0ce..95166727 100644 --- a/functions/webhooks/taskrouterCallback.protected.ts +++ b/functions/webhooks/taskrouterCallback.protected.ts @@ -66,6 +66,7 @@ const runTaskrouterListeners = async ( 'X-Original-Webhook-Url': `https://${context.DOMAIN_NAME}${context.PATH}`, ...event.request.headers, }, + body: JSON.stringify(event), }); } await Promise.all( From a105153b5842a517075301951df12ed49d20c0bf Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Mon, 23 Dec 2024 17:59:57 +0000 Subject: [PATCH 07/19] Go back to awaiting delegated call --- .../webhooks/taskrouterCallback.protected.ts | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/functions/webhooks/taskrouterCallback.protected.ts b/functions/webhooks/taskrouterCallback.protected.ts index 95166727..d1da83b1 100644 --- a/functions/webhooks/taskrouterCallback.protected.ts +++ b/functions/webhooks/taskrouterCallback.protected.ts @@ -55,22 +55,24 @@ const runTaskrouterListeners = async ( callback: ServerlessCallback, ) => { const listeners = getListeners(); - - if (context.DELEGATE_WEBHOOK_URL) { - const delegateUrl = `${context.DELEGATE_WEBHOOK_URL}/${context.ACCOUNT_SID}${context.PATH}`; - console.info('Forwarding event to delegate webhook:', delegateUrl); - // Fire and forget - fetch(delegateUrl, { - method: 'POST', - headers: { - 'X-Original-Webhook-Url': `https://${context.DOMAIN_NAME}${context.PATH}`, - ...event.request.headers, - }, - body: JSON.stringify(event), - }); - } - await Promise.all( - listeners + await Promise.all([ + async () => { + if (context.DELEGATE_WEBHOOK_URL) { + const delegateUrl = `${context.DELEGATE_WEBHOOK_URL}/${context.ACCOUNT_SID}${context.PATH}`; + console.info('Forwarding event to delegate webhook:', delegateUrl); + // Fire and forget + return fetch(delegateUrl, { + method: 'POST', + headers: { + 'X-Original-Webhook-Url': `https://${context.DOMAIN_NAME}${context.PATH}`, + ...event.request.headers, + }, + body: JSON.stringify(event), + }); + } + return Promise.resolve(); + }, + ...listeners .filter(([, listener]) => listener.shouldHandle(event)) .map(async ([path, listener]) => { console.debug( @@ -85,7 +87,7 @@ const runTaskrouterListeners = async ( `===== Successfully executed listener at ${path} for event: ${event.EventType}, task: ${event.TaskSid} =====`, ); }), - ); + ]); }; export const handler = async ( From 96268f6c4088819f80c94b431ca2dc21552c0838 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Mon, 23 Dec 2024 18:52:11 +0000 Subject: [PATCH 08/19] Logging --- functions/webhooks/taskrouterCallback.protected.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/functions/webhooks/taskrouterCallback.protected.ts b/functions/webhooks/taskrouterCallback.protected.ts index d1da83b1..1ae2819c 100644 --- a/functions/webhooks/taskrouterCallback.protected.ts +++ b/functions/webhooks/taskrouterCallback.protected.ts @@ -57,6 +57,7 @@ const runTaskrouterListeners = async ( const listeners = getListeners(); await Promise.all([ async () => { + console.debug('Checking forwarding event to delegate webhook'); if (context.DELEGATE_WEBHOOK_URL) { const delegateUrl = `${context.DELEGATE_WEBHOOK_URL}/${context.ACCOUNT_SID}${context.PATH}`; console.info('Forwarding event to delegate webhook:', delegateUrl); From 4791e2decdfb8ffa174138d2ba9ce3d1f475f1c5 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Mon, 23 Dec 2024 19:04:23 +0000 Subject: [PATCH 09/19] Logging --- functions/webhooks/taskrouterCallback.protected.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/functions/webhooks/taskrouterCallback.protected.ts b/functions/webhooks/taskrouterCallback.protected.ts index 1ae2819c..2cbd35cf 100644 --- a/functions/webhooks/taskrouterCallback.protected.ts +++ b/functions/webhooks/taskrouterCallback.protected.ts @@ -56,7 +56,7 @@ const runTaskrouterListeners = async ( ) => { const listeners = getListeners(); await Promise.all([ - async () => { + () => { console.debug('Checking forwarding event to delegate webhook'); if (context.DELEGATE_WEBHOOK_URL) { const delegateUrl = `${context.DELEGATE_WEBHOOK_URL}/${context.ACCOUNT_SID}${context.PATH}`; @@ -84,11 +84,11 @@ const runTaskrouterListeners = async ( } catch (err) { console.error(`===== Listener at ${path} has failed, aborting =====`, err); } - console.info( - `===== Successfully executed listener at ${path} for event: ${event.EventType}, task: ${event.TaskSid} =====`, - ); }), ]); + console.info( + `===== Successfully executed all listeners for event: ${event.EventType}, task: ${event.TaskSid} =====`, + ); }; export const handler = async ( From 1d920bf5c000cfaf5afd6b35e90c6671293b982c Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Mon, 23 Dec 2024 20:38:41 +0000 Subject: [PATCH 10/19] Fixed promise await for forwarding delegate --- .../webhooks/taskrouterCallback.protected.ts | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/functions/webhooks/taskrouterCallback.protected.ts b/functions/webhooks/taskrouterCallback.protected.ts index 2cbd35cf..11e9fc2b 100644 --- a/functions/webhooks/taskrouterCallback.protected.ts +++ b/functions/webhooks/taskrouterCallback.protected.ts @@ -55,24 +55,22 @@ const runTaskrouterListeners = async ( callback: ServerlessCallback, ) => { const listeners = getListeners(); + const delegatePromise: Promise = Promise.resolve(); + if (context.DELEGATE_WEBHOOK_URL) { + const delegateUrl = `${context.DELEGATE_WEBHOOK_URL}/${context.ACCOUNT_SID}${context.PATH}`; + console.info('Forwarding event to delegate webhook:', delegateUrl); + // Fire and forget + return fetch(delegateUrl, { + method: 'POST', + headers: { + 'X-Original-Webhook-Url': `https://${context.DOMAIN_NAME}${context.PATH}`, + ...event.request.headers, + }, + body: JSON.stringify(event), + }); + } await Promise.all([ - () => { - console.debug('Checking forwarding event to delegate webhook'); - if (context.DELEGATE_WEBHOOK_URL) { - const delegateUrl = `${context.DELEGATE_WEBHOOK_URL}/${context.ACCOUNT_SID}${context.PATH}`; - console.info('Forwarding event to delegate webhook:', delegateUrl); - // Fire and forget - return fetch(delegateUrl, { - method: 'POST', - headers: { - 'X-Original-Webhook-Url': `https://${context.DOMAIN_NAME}${context.PATH}`, - ...event.request.headers, - }, - body: JSON.stringify(event), - }); - } - return Promise.resolve(); - }, + delegatePromise, ...listeners .filter(([, listener]) => listener.shouldHandle(event)) .map(async ([path, listener]) => { @@ -89,6 +87,7 @@ const runTaskrouterListeners = async ( console.info( `===== Successfully executed all listeners for event: ${event.EventType}, task: ${event.TaskSid} =====`, ); + return null; }; export const handler = async ( From 18d79b8319cced349b187ba89b90d628eaa2ec24 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Mon, 23 Dec 2024 20:52:52 +0000 Subject: [PATCH 11/19] Logging --- functions/webhooks/taskrouterCallback.protected.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/webhooks/taskrouterCallback.protected.ts b/functions/webhooks/taskrouterCallback.protected.ts index 11e9fc2b..e9aaa426 100644 --- a/functions/webhooks/taskrouterCallback.protected.ts +++ b/functions/webhooks/taskrouterCallback.protected.ts @@ -58,7 +58,7 @@ const runTaskrouterListeners = async ( const delegatePromise: Promise = Promise.resolve(); if (context.DELEGATE_WEBHOOK_URL) { const delegateUrl = `${context.DELEGATE_WEBHOOK_URL}/${context.ACCOUNT_SID}${context.PATH}`; - console.info('Forwarding event to delegate webhook:', delegateUrl); + console.info('Forwarding event to delegate webhook:', delegateUrl, event); // Fire and forget return fetch(delegateUrl, { method: 'POST', From 913234e214c76dfd6c4234704c02a48b50258271 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Mon, 23 Dec 2024 20:53:39 +0000 Subject: [PATCH 12/19] Logging --- functions/webhooks/taskrouterCallback.protected.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/functions/webhooks/taskrouterCallback.protected.ts b/functions/webhooks/taskrouterCallback.protected.ts index e9aaa426..14b52379 100644 --- a/functions/webhooks/taskrouterCallback.protected.ts +++ b/functions/webhooks/taskrouterCallback.protected.ts @@ -59,6 +59,7 @@ const runTaskrouterListeners = async ( if (context.DELEGATE_WEBHOOK_URL) { const delegateUrl = `${context.DELEGATE_WEBHOOK_URL}/${context.ACCOUNT_SID}${context.PATH}`; console.info('Forwarding event to delegate webhook:', delegateUrl, event); + console.info('Forwarding event to delegate webhook:', JSON.stringify(event)); // Fire and forget return fetch(delegateUrl, { method: 'POST', From 86438fe83e9343d9d4cddddaab6d16bcdd2d0666 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Mon, 23 Dec 2024 21:12:43 +0000 Subject: [PATCH 13/19] Split event from request before forwarding --- functions/webhooks/taskrouterCallback.protected.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/functions/webhooks/taskrouterCallback.protected.ts b/functions/webhooks/taskrouterCallback.protected.ts index 14b52379..c62cfe30 100644 --- a/functions/webhooks/taskrouterCallback.protected.ts +++ b/functions/webhooks/taskrouterCallback.protected.ts @@ -51,21 +51,21 @@ const getListeners = (): [string, TaskrouterListener][] => { const runTaskrouterListeners = async ( context: Context, - event: EventFieldsWithRequest, + { request, ...event }: EventFieldsWithRequest, callback: ServerlessCallback, ) => { const listeners = getListeners(); - const delegatePromise: Promise = Promise.resolve(); + let delegatePromise: Promise = Promise.resolve(); if (context.DELEGATE_WEBHOOK_URL) { const delegateUrl = `${context.DELEGATE_WEBHOOK_URL}/${context.ACCOUNT_SID}${context.PATH}`; console.info('Forwarding event to delegate webhook:', delegateUrl, event); console.info('Forwarding event to delegate webhook:', JSON.stringify(event)); // Fire and forget - return fetch(delegateUrl, { + delegatePromise = fetch(delegateUrl, { method: 'POST', headers: { 'X-Original-Webhook-Url': `https://${context.DOMAIN_NAME}${context.PATH}`, - ...event.request.headers, + ...request.headers, }, body: JSON.stringify(event), }); From 7a51bbf9063f1e2c00708582873989f8f5b14ed2 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Tue, 24 Dec 2024 17:26:52 +0000 Subject: [PATCH 14/19] Test with empty body --- functions/webhooks/taskrouterCallback.protected.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/webhooks/taskrouterCallback.protected.ts b/functions/webhooks/taskrouterCallback.protected.ts index c62cfe30..372c4155 100644 --- a/functions/webhooks/taskrouterCallback.protected.ts +++ b/functions/webhooks/taskrouterCallback.protected.ts @@ -67,7 +67,7 @@ const runTaskrouterListeners = async ( 'X-Original-Webhook-Url': `https://${context.DOMAIN_NAME}${context.PATH}`, ...request.headers, }, - body: JSON.stringify(event), + body: '{}', // JSON.stringify(event), }); } await Promise.all([ From 479327eb498be184bced85f5550eba6f5e20169b Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Tue, 24 Dec 2024 17:34:11 +0000 Subject: [PATCH 15/19] Add correct contect type --- functions/webhooks/taskrouterCallback.protected.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/functions/webhooks/taskrouterCallback.protected.ts b/functions/webhooks/taskrouterCallback.protected.ts index 372c4155..ecb1c86d 100644 --- a/functions/webhooks/taskrouterCallback.protected.ts +++ b/functions/webhooks/taskrouterCallback.protected.ts @@ -66,8 +66,9 @@ const runTaskrouterListeners = async ( headers: { 'X-Original-Webhook-Url': `https://${context.DOMAIN_NAME}${context.PATH}`, ...request.headers, + 'Content-Type': 'application/json', }, - body: '{}', // JSON.stringify(event), + body: JSON.stringify(event), }); } await Promise.all([ From 7fc72e43af85ff372e698d9343782861c0afeea9 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Tue, 24 Dec 2024 17:46:01 +0000 Subject: [PATCH 16/19] Log out headers --- functions/webhooks/taskrouterCallback.protected.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/functions/webhooks/taskrouterCallback.protected.ts b/functions/webhooks/taskrouterCallback.protected.ts index ecb1c86d..d3483288 100644 --- a/functions/webhooks/taskrouterCallback.protected.ts +++ b/functions/webhooks/taskrouterCallback.protected.ts @@ -60,15 +60,16 @@ const runTaskrouterListeners = async ( const delegateUrl = `${context.DELEGATE_WEBHOOK_URL}/${context.ACCOUNT_SID}${context.PATH}`; console.info('Forwarding event to delegate webhook:', delegateUrl, event); console.info('Forwarding event to delegate webhook:', JSON.stringify(event)); + console.info('Forwarding headers to delegate webhook:', JSON.stringify(request.headers)); // Fire and forget delegatePromise = fetch(delegateUrl, { method: 'POST', headers: { - 'X-Original-Webhook-Url': `https://${context.DOMAIN_NAME}${context.PATH}`, ...request.headers, + 'X-Original-Webhook-Url': `https://${context.DOMAIN_NAME}${context.PATH}`, 'Content-Type': 'application/json', }, - body: JSON.stringify(event), + // body: JSON.stringify(event), }); } await Promise.all([ From 96cf7ff00a716dc3e808c1980cb1e4004cbb6213 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Tue, 24 Dec 2024 18:06:43 +0000 Subject: [PATCH 17/19] Filter forwarded headers --- .../webhooks/taskrouterCallback.protected.ts | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/functions/webhooks/taskrouterCallback.protected.ts b/functions/webhooks/taskrouterCallback.protected.ts index d3483288..15cf4fa2 100644 --- a/functions/webhooks/taskrouterCallback.protected.ts +++ b/functions/webhooks/taskrouterCallback.protected.ts @@ -58,18 +58,22 @@ const runTaskrouterListeners = async ( let delegatePromise: Promise = Promise.resolve(); if (context.DELEGATE_WEBHOOK_URL) { const delegateUrl = `${context.DELEGATE_WEBHOOK_URL}/${context.ACCOUNT_SID}${context.PATH}`; - console.info('Forwarding event to delegate webhook:', delegateUrl, event); - console.info('Forwarding event to delegate webhook:', JSON.stringify(event)); - console.info('Forwarding headers to delegate webhook:', JSON.stringify(request.headers)); + const forwardedHeaderEntries = Object.entries(request.headers).filter( + ([key]) => key.toLowerCase().startsWith('x-') || key.toLowerCase().startsWith('t-'), + ); + const delegateHeaders = { + ...Object.fromEntries(forwardedHeaderEntries), + 'X-Original-Webhook-Url': `https://${context.DOMAIN_NAME}${context.PATH}`, + 'Content-Type': 'application/json', + }; + console.info('Forwarding to delegate webhook:', delegateUrl); + console.info('event:', event); + console.debug('headers:', delegateHeaders); // Fire and forget delegatePromise = fetch(delegateUrl, { method: 'POST', - headers: { - ...request.headers, - 'X-Original-Webhook-Url': `https://${context.DOMAIN_NAME}${context.PATH}`, - 'Content-Type': 'application/json', - }, - // body: JSON.stringify(event), + headers: delegateHeaders, + body: JSON.stringify(event), }); } await Promise.all([ From 49a95cf68df6cf18eecc3f7a28761da412a33693 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Fri, 27 Dec 2024 08:18:42 +0000 Subject: [PATCH 18/19] disable HRM contact creation --- .../taskrouterListeners/createHrmContactListener.private.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/functions/taskrouterListeners/createHrmContactListener.private.ts b/functions/taskrouterListeners/createHrmContactListener.private.ts index 34e87cc2..cdd2ef3b 100644 --- a/functions/taskrouterListeners/createHrmContactListener.private.ts +++ b/functions/taskrouterListeners/createHrmContactListener.private.ts @@ -22,12 +22,14 @@ import { Context } from '@twilio-labs/serverless-runtime-types/types'; import { EventFields, EventType, - RESERVATION_ACCEPTED, + // RESERVATION_ACCEPTED, TaskrouterListener, } from '@tech-matters/serverless-helpers/taskrouter'; import { HrmContact, PrepopulateForm } from '../hrm/populateHrmContactFormFromTask'; -export const eventTypes: EventType[] = [RESERVATION_ACCEPTED]; +export const eventTypes: EventType[] = [ + /* RESERVATION_ACCEPTED */ +]; type EnvVars = { TWILIO_WORKSPACE_SID: string; From 17a402a06b8eab15c84d1468c5d88985024993d3 Mon Sep 17 00:00:00 2001 From: Stephen Hand Date: Fri, 27 Dec 2024 19:45:45 +0000 Subject: [PATCH 19/19] remove HRM contact creation --- .../hrm/populateHrmContactFormFromTask.ts | 479 ------------------ .../createHrmContactListener.private.ts | 193 ------- 2 files changed, 672 deletions(-) delete mode 100644 functions/hrm/populateHrmContactFormFromTask.ts delete mode 100644 functions/taskrouterListeners/createHrmContactListener.private.ts diff --git a/functions/hrm/populateHrmContactFormFromTask.ts b/functions/hrm/populateHrmContactFormFromTask.ts deleted file mode 100644 index 24def61e..00000000 --- a/functions/hrm/populateHrmContactFormFromTask.ts +++ /dev/null @@ -1,479 +0,0 @@ -/** - * Copyright (C) 2021-2023 Technology Matters - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see https://www.gnu.org/licenses/. - */ - -import { capitalize } from 'lodash'; -import { startOfDay, format } from 'date-fns'; - -type MapperFunction = (options: string[]) => (value: string) => string; - -// When we move this into the flex repo we can depend on hrm-form-definitions for these types & enums -enum FormInputType { - Input = 'input', - SearchInput = 'search-input', - NumericInput = 'numeric-input', - Email = 'email', - RadioInput = 'radio-input', - ListboxMultiselect = 'listbox-multiselect', - Select = 'select', - DependentSelect = 'dependent-select', - Checkbox = 'checkbox', - MixedCheckbox = 'mixed-checkbox', - Textarea = 'textarea', - DateInput = 'date-input', - TimeInput = 'time-input', - FileUpload = 'file-upload', - Button = 'button', - CopyTo = 'copy-to', - CustomContactComponent = 'custom-contact-component', -} - -type FormItemDefinition = { - name: string; - unknownOption?: string; - options?: { value: string }[]; - initialChecked?: boolean; - initializeWithCurrent?: boolean; -} & ( - | { - type: Exclude; - defaultOption?: string; - } - | { - type: FormInputType.DependentSelect; - defaultOption: { - value: string; - }; - } -); - -type PrepopulateKeys = { - preEngagement: { - ChildInformationTab: string[]; - CallerInformationTab: string[]; - CaseInformationTab: string[]; - }; - survey: { ChildInformationTab: string[]; CallerInformationTab: string[] }; -}; - -type ChannelTypes = - | 'voice' - | 'sms' - | 'facebook' - | 'messenger' - | 'whatsapp' - | 'web' - | 'telegram' - | 'instagram' - | 'line' - | 'modica'; - -const callTypes = { - child: 'Child calling about self', - caller: 'Someone calling about a child', -}; - -type HrmContactRawJson = { - definitionVersion?: string; - callType: typeof callTypes[keyof typeof callTypes]; - childInformation: Record; - callerInformation: Record; - caseInformation: Record; - categories: Record; - contactlessTask: { - channel: ChannelTypes; - date: string; - time: string; - createdOnBehalfOf: `WK${string}` | ''; - [key: string]: string | boolean; - }; -}; - -export type HrmContact = { - id: string; - accountSid?: `AC${string}`; - twilioWorkerId?: `WK${string}`; - number: string; - conversationDuration: number; - csamReports: unknown[]; - referrals?: unknown[]; - conversationMedia?: unknown[]; - createdAt: string; - createdBy: string; - helpline: string; - taskId: `WT${string}` | null; - profileId?: string; - identifierId?: string; - channel: ChannelTypes | 'default'; - updatedBy: string; - updatedAt?: string; - finalizedAt?: string; - rawJson: HrmContactRawJson; - timeOfContact: string; - queueName: string; - channelSid: string; - serviceSid: string; - caseId?: string; -}; - -type FormValue = string | string[] | boolean | null; - -// This hardcoded logic should be moved into the form definition JSON or some other configuration -const MANDATORY_CHATBOT_FIELDS = ['age', 'gender', 'ethnicity']; - -const CUSTOM_MAPPERS: Record = { - age: - (ageOptions: string[]) => - (age: string): string => { - const ageInt = parseInt(age, 10); - - const maxAge = ageOptions.find((e) => e.includes('>')); - - if (maxAge) { - const maxAgeInt = parseInt(maxAge.replace('>', ''), 10); - - if (ageInt >= 0 && ageInt <= maxAgeInt) { - return ageOptions.find((o) => parseInt(o, 10) === ageInt) || 'Unknown'; - } - - if (ageInt > maxAgeInt) return maxAge; - } else { - console.error('Pre populate form error: no maxAge option provided.'); - } - - return 'Unknown'; - }, -}; - -/** - * Utility functions to create initial state from definition - * @param {FormItemDefinition} def Definition for a single input of a Form - */ -const getInitialValue = (def: FormItemDefinition): FormValue => { - switch (def.type) { - case FormInputType.Input: - case FormInputType.NumericInput: - case FormInputType.Email: - case FormInputType.Textarea: - case FormInputType.FileUpload: - return ''; - case FormInputType.DateInput: { - if (def.initializeWithCurrent) { - return format(startOfDay(new Date()), 'yyyy-MM-dd'); - } - return ''; - } - case FormInputType.TimeInput: { - if (def.initializeWithCurrent) { - return format(new Date(), 'HH:mm'); - } - - return ''; - } - case FormInputType.RadioInput: - return def.defaultOption ?? ''; - case FormInputType.ListboxMultiselect: - return []; - case FormInputType.Select: - if (def.defaultOption) return def.defaultOption; - return def.options && def.options[0] ? def.options[0].value : null; - case FormInputType.DependentSelect: - return def.defaultOption?.value; - case FormInputType.CopyTo: - case FormInputType.Checkbox: - return Boolean(def.initialChecked); - case 'mixed-checkbox': - return def.initialChecked === undefined ? 'mixed' : def.initialChecked; - default: - return null; - } -}; - -const mapGenericOption = (options: string[]) => (value: string) => { - const validOption = options.find((e) => e.toLowerCase() === value.toLowerCase()); - - if (!validOption) { - return 'Unknown'; - } - - return validOption; -}; - -const getUnknownOption = (key: string, definition: FormItemDefinition[]) => { - const inputDef = definition.find((e) => e.name === key); - - // inputDef.options check needed whilst we use an el cheapo copy of the type, once we share the flex type it won't be needed - if (inputDef?.type === 'select' && inputDef.options) { - const unknownOption = inputDef.unknownOption - ? inputDef.options.find((e) => e.value === inputDef.unknownOption) - : inputDef.options.find((e) => e.value === 'Unknown'); - if (unknownOption && unknownOption.value) return unknownOption.value; - - console.error(`getUnknownOption couldn't determine a valid unknown option for key ${key}.`); - } - - return 'Unknown'; -}; - -/** - * Given a key and a form definition, grabs the input with name that equals the key and return the options values, or empty array. - */ -const getSelectOptions = (key: string) => (definition: FormItemDefinition[]) => { - const inputDef = definition.find((e) => e.name === key); - // inputDef.options check needed whilst we use an el cheapo copy of the type, once we share the flex type it won't be needed - if (inputDef?.type === 'select' && inputDef.options) { - return inputDef.options.map((e) => e.value) || []; - } - - console.error(`getSelectOptions called with key ${key} but is a non-select input type.`); - return []; -}; - -const getAnswerOrUnknown = ( - answers: any, - key: string, - definition: FormItemDefinition[], - mapperFunction: MapperFunction = mapGenericOption, -) => { - // This keys must be set with 'Unknown' value even if there's no answer - const isRequiredKey = key === 'age' || key === 'gender'; - - // This prevents setting redux state with the 'Unknown' value for a property that is not asked by the pre-survey - if (!isRequiredKey && !answers[key]) return null; - - const itemDefinition = definition.find((e) => e.name === key); - - // This prevents setting redux state with the 'Unknown' value for a property that is not present on the definition - if (!itemDefinition) { - console.error(`${key} does not exist in the current definition`); - return null; - } - - if (itemDefinition.type === 'select') { - const unknown = getUnknownOption(key, definition); - const isUnknownAnswer = !answers[key] || answers[key] === unknown; - - if (isUnknownAnswer) return unknown; - - const options = getSelectOptions(key)(definition); - const result = mapperFunction(options)(answers[key]); - - return result === 'Unknown' ? unknown : result; - } - - return answers[key]; -}; - -const getValuesFromAnswers = ( - prepopulateKeys: Set, - tabFormDefinition: FormItemDefinition[], - answers: any, -): Record => { - // Get values from task attributes - const { firstName, language } = answers; - - // Get the customizable values from the bot's memory if there's any value (defined in PrepopulateKeys.json) - const customizableValues = Array.from(prepopulateKeys).reduce((accum, key) => { - const value = getAnswerOrUnknown( - answers, - key, - tabFormDefinition, - CUSTOM_MAPPERS[key] || mapGenericOption, - ); - return value ? { ...accum, [key]: value } : accum; - }, {}); - - return { - ...(firstName && { firstName }), - ...(language && { language: capitalize(language) }), - ...customizableValues, - }; -}; - -const getValuesFromPreEngagementData = ( - prepopulateKeySet: Set, - tabFormDefinition: FormItemDefinition[], - preEngagementData: Record, -) => { - // Get values from task attributes - const values: Record = {}; - const prepopulateKeys = Array.from(prepopulateKeySet); - tabFormDefinition.forEach((field: FormItemDefinition) => { - if (prepopulateKeys.indexOf(field.name) > -1) { - if (['mixed-checkbox', 'checkbox'].includes(field.type)) { - const fieldValue = preEngagementData[field.name]?.toLowerCase(); - if (fieldValue === 'yes') { - values[field.name] = true; - } else if (fieldValue === 'no' || field.type === 'checkbox') { - values[field.name] = false; - } - return; - } - values[field.name] = preEngagementData[field.name] || ''; - } - }); - return values; -}; - -const loadedConfigJsons: Record = {}; - -const loadConfigJson = async (formDefinitionRootUrl: URL, section: string): Promise => { - if (!loadedConfigJsons[section]) { - const url = `${formDefinitionRootUrl}/${section}.json`; - const response = await fetch(url); - loadedConfigJsons[section] = response.json(); - } - return loadedConfigJsons[section]; -}; - -const populateInitialValues = async (contact: HrmContact, formDefinitionRootUrl: URL) => { - const tabNamesAndRawJsonSections: [string, Record][] = [ - ['CaseInformationTab', contact.rawJson.caseInformation], - ['ChildInformationTab', contact.rawJson.childInformation], - ['CallerInformationTab', contact.rawJson.callerInformation], - ]; - - const defintionsAndJsons: [FormItemDefinition[], Record][] = await Promise.all( - tabNamesAndRawJsonSections.map(async ([tabbedFormsSection, rawJsonSection]) => [ - await loadConfigJson(formDefinitionRootUrl, `tabbedForms/${tabbedFormsSection}`), - rawJsonSection, - ]), - ); - for (const [tabFormDefinition, rawJson] of defintionsAndJsons) { - for (const formItemDefinition of tabFormDefinition) { - rawJson[formItemDefinition.name] = getInitialValue(formItemDefinition); - } - } - const helplineInformation = await loadConfigJson(formDefinitionRootUrl, 'HelplineInformation'); - const defaultHelplineOption = ( - helplineInformation.helplines.find((helpline: any) => helpline.default) || - helplineInformation.helplines[0] - ).value; - Object.assign(contact.rawJson.contactlessTask, { - date: getInitialValue({ - type: FormInputType.DateInput, - initializeWithCurrent: true, - name: 'date', - }), - time: getInitialValue({ - type: FormInputType.TimeInput, - initializeWithCurrent: true, - name: 'time', - }), - helpline: defaultHelplineOption, - }); -}; - -const populateContactSection = async ( - target: Record, - valuesToPopulate: Record, - keys: Set, - formDefinitionRootUrl: URL, - tabbedFormsSection: 'CaseInformationTab' | 'ChildInformationTab' | 'CallerInformationTab', - converter: ( - keys: Set, - formTabDefinition: FormItemDefinition[], - values: Record, - ) => Record, -) => { - console.debug('Populating', tabbedFormsSection); - console.debug('Keys', Array.from(keys)); - console.debug('Using Values', valuesToPopulate); - - if (keys.size > 0) { - const childInformationTabDefinition = await loadConfigJson( - formDefinitionRootUrl, - `tabbedForms/${tabbedFormsSection}`, - ); - Object.assign(target, converter(keys, childInformationTabDefinition, valuesToPopulate)); - } -}; - -export const populateHrmContactFormFromTask = async ( - taskAttributes: Record, - contact: HrmContact, - formDefinitionRootUrl: URL, -): Promise => { - const { memory, preEngagementData, firstName, language } = taskAttributes; - const answers = { ...memory, firstName, language }; - await populateInitialValues(contact, formDefinitionRootUrl); - if (!answers && !preEngagementData) return contact; - const { preEngagement: preEngagementKeys, survey: surveyKeys }: PrepopulateKeys = - await loadConfigJson(formDefinitionRootUrl, 'PrepopulateKeys'); - - const isValidSurvey = Boolean(answers?.aboutSelf); // determines if the memory has valid values or if it was aborted - const isAboutSelf = answers.aboutSelf === 'Yes'; - if (isValidSurvey) { - // eslint-disable-next-line no-param-reassign - contact.rawJson.callType = isAboutSelf ? callTypes.child : callTypes.caller; - } - if (preEngagementData) { - await populateContactSection( - contact.rawJson.caseInformation, - preEngagementData, - new Set(preEngagementKeys.CaseInformationTab), - formDefinitionRootUrl, - 'CaseInformationTab', - getValuesFromPreEngagementData, - ); - - if (!isValidSurvey || isAboutSelf) { - await populateContactSection( - contact.rawJson.childInformation, - preEngagementData, - new Set(preEngagementKeys.ChildInformationTab), - formDefinitionRootUrl, - 'ChildInformationTab', - getValuesFromPreEngagementData, - ); - } else { - await populateContactSection( - contact.rawJson.callerInformation, - preEngagementData, - new Set(preEngagementKeys.CallerInformationTab), - formDefinitionRootUrl, - 'CallerInformationTab', - getValuesFromPreEngagementData, - ); - } - } - - if (isValidSurvey) { - if (isAboutSelf) { - await populateContactSection( - contact.rawJson.childInformation, - answers, - new Set([...MANDATORY_CHATBOT_FIELDS, ...surveyKeys.ChildInformationTab]), - formDefinitionRootUrl, - 'ChildInformationTab', - getValuesFromAnswers, - ); - } else { - await populateContactSection( - contact.rawJson.callerInformation, - answers, - new Set([...MANDATORY_CHATBOT_FIELDS, ...surveyKeys.CallerInformationTab]), - formDefinitionRootUrl, - 'CallerInformationTab', - getValuesFromAnswers, - ); - } - } - return contact; -}; - -export type PrepopulateForm = { - populateHrmContactFormFromTask: typeof populateHrmContactFormFromTask; -}; diff --git a/functions/taskrouterListeners/createHrmContactListener.private.ts b/functions/taskrouterListeners/createHrmContactListener.private.ts deleted file mode 100644 index cdd2ef3b..00000000 --- a/functions/taskrouterListeners/createHrmContactListener.private.ts +++ /dev/null @@ -1,193 +0,0 @@ -/** - * Copyright (C) 2021-2023 Technology Matters - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see https://www.gnu.org/licenses/. - */ - -/* eslint-disable global-require */ -/* eslint-disable import/no-dynamic-require */ -import '@twilio-labs/serverless-runtime-types'; -import { Context } from '@twilio-labs/serverless-runtime-types/types'; - -import { - EventFields, - EventType, - // RESERVATION_ACCEPTED, - TaskrouterListener, -} from '@tech-matters/serverless-helpers/taskrouter'; -import { HrmContact, PrepopulateForm } from '../hrm/populateHrmContactFormFromTask'; - -export const eventTypes: EventType[] = [ - /* RESERVATION_ACCEPTED */ -]; - -type EnvVars = { - TWILIO_WORKSPACE_SID: string; - CHAT_SERVICE_SID: string; - HRM_STATIC_KEY: string; -}; - -// Temporarily copied to this repo, will share the flex types when we move them into the same repo - -const BLANK_CONTACT: HrmContact = { - id: '', - timeOfContact: new Date().toISOString(), - taskId: null, - helpline: '', - rawJson: { - childInformation: {}, - callerInformation: {}, - caseInformation: {}, - callType: '', - contactlessTask: { - channel: 'web', - date: '', - time: '', - createdOnBehalfOf: '', - helpline: '', - }, - categories: {}, - }, - channelSid: '', - serviceSid: '', - channel: 'default', - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - queueName: '', - number: '', - conversationDuration: 0, - csamReports: [], - conversationMedia: [], -}; - -/** - * Checks the event type to determine if the listener should handle the event or not. - * If it returns true, the taskrouter will invoke this listener. - */ -export const shouldHandle = ({ - TaskAttributes: taskAttributesString, - TaskSid: taskSid, - EventType: eventType, -}: EventFields) => { - if (!eventTypes.includes(eventType)) return false; - - const { isContactlessTask, transferTargetType } = JSON.parse(taskAttributesString ?? '{}'); - - if (isContactlessTask) { - console.debug(`Task ${taskSid} is a contactless task, contact was already created in Flex.`); - return false; - } - - if (transferTargetType) { - console.debug( - `Task ${taskSid} was created to receive a ${transferTargetType} transfer. The original contact will be used so a new one will not be created.`, - ); - return false; - } - return true; -}; - -export const handleEvent = async ( - { getTwilioClient, HRM_STATIC_KEY, TWILIO_WORKSPACE_SID }: Context, - { TaskAttributes: taskAttributesString, TaskSid: taskSid, WorkerSid: workerSid }: EventFields, -) => { - const taskAttributes = taskAttributesString ? JSON.parse(taskAttributesString) : {}; - const { channelSid } = taskAttributes; - - const client = getTwilioClient(); - const serviceConfig = await client.flexApi.configuration.get().fetch(); - - const { - definitionVersion, - hrm_base_url: hrmBaseUrl, - hrm_api_version: hrmApiVersion, - form_definitions_version_url: configFormDefinitionsVersionUrl, - assets_bucket_url: assetsBucketUrl, - helpline_code: helplineCode, - channelType, - customChannelType, - feature_flags: { enable_backend_hrm_contact_creation: enableBackendHrmContactCreation }, - } = serviceConfig.attributes; - const formDefinitionsVersionUrl = - configFormDefinitionsVersionUrl || `${assetsBucketUrl}/form-definitions/${helplineCode}/v1`; - if (!enableBackendHrmContactCreation) { - console.debug( - `enable_backend_hrm_contact_creation is not set, the contact associated with task ${taskSid} will be created from Flex.`, - ); - return; - } - console.debug('Creating HRM contact for task', taskSid); - const hrmBaseAccountUrl = `${hrmBaseUrl}/${hrmApiVersion}/accounts/${serviceConfig.accountSid}`; - - const newContact: HrmContact = { - ...BLANK_CONTACT, - channel: (customChannelType || channelType) as HrmContact['channel'], - rawJson: { - definitionVersion, - ...BLANK_CONTACT.rawJson, - }, - twilioWorkerId: workerSid as HrmContact['twilioWorkerId'], - taskId: taskSid as HrmContact['taskId'], - channelSid: channelSid ?? '', - serviceSid: (channelSid && serviceConfig.chatServiceInstanceSid) ?? '', - // We set createdBy to the workerSid because the contact is 'created' by the worker who accepts the task - createdBy: workerSid as HrmContact['createdBy'], - }; - - const prepopulatePath = Runtime.getFunctions()['hrm/populateHrmContactFormFromTask'].path; - const { populateHrmContactFormFromTask } = require(prepopulatePath) as PrepopulateForm; - const populatedContact = await populateHrmContactFormFromTask( - taskAttributes, - newContact, - formDefinitionsVersionUrl, - ); - const options: RequestInit = { - method: 'POST', - body: JSON.stringify(populatedContact), - headers: { - 'Content-Type': 'application/json', - Authorization: `Basic ${HRM_STATIC_KEY}`, - }, - }; - const response = await fetch(`${hrmBaseAccountUrl}/contacts`, options); - if (!response.ok) { - console.error( - `Failed to create HRM contact for task ${taskSid} - status: ${response.status} - ${response.statusText}`, - await response.text(), - ); - return; - } - const { id }: HrmContact = await response.json(); - console.info(`Created HRM contact with id ${id} for task ${taskSid}`); - - const taskContext = client.taskrouter.v1.workspaces.get(TWILIO_WORKSPACE_SID).tasks.get(taskSid); - const currentTaskAttributes = (await taskContext.fetch()).attributes; // Less chance of race conditions if we fetch the task attributes again, still not the best... - const updatedAttributes = { - ...JSON.parse(currentTaskAttributes), - contactId: id.toString(), - }; - await taskContext.update({ attributes: JSON.stringify(updatedAttributes) }); -}; - -/** - * The taskrouter callback expects that all taskrouter listeners return - * a default object of type TaskrouterListener. - */ -const createHrmContactListener: TaskrouterListener = { - shouldHandle, - handleEvent, -}; - -export default createHrmContactListener;