From ec135dd740d79baacea96163603bb1cbcf62dda3 Mon Sep 17 00:00:00 2001 From: mythilytm Date: Thu, 1 May 2025 12:17:34 -0400 Subject: [PATCH 01/22] log configurations for queues and workflows --- functions/webhooks/assignSwitchboarding.ts | 133 +++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 functions/webhooks/assignSwitchboarding.ts diff --git a/functions/webhooks/assignSwitchboarding.ts b/functions/webhooks/assignSwitchboarding.ts new file mode 100644 index 00000000..8407e791 --- /dev/null +++ b/functions/webhooks/assignSwitchboarding.ts @@ -0,0 +1,133 @@ +/** + * 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 '@twilio-labs/serverless-runtime-types'; +import { validator } from 'twilio-flex-token-validator'; +import { Context, ServerlessCallback } from '@twilio-labs/serverless-runtime-types/types'; +import { + responseWithCors, + bindResolve, + error400, + error500, + success, + functionValidator as TokenValidator, + error403, +} from '@tech-matters/serverless-helpers'; + +type EnvVars = { + TWILIO_WORKSPACE_SID: string; + ACCOUNT_SID: string; + AUTH_TOKEN: string; +}; + +export type Body = { + originalQueueSid?: string; + request: { cookies: {}; headers: {} }; + Token: string; +}; + +export type TokenValidatorResponse = { worker_sid?: string; roles?: string[] }; + +const isSupervisor = (tokenResult: TokenValidatorResponse) => + Array.isArray(tokenResult.roles) && tokenResult.roles.includes('supervisor'); + +export const handler = TokenValidator( + async (context: Context, event: Body, callback: ServerlessCallback) => { + const response = responseWithCors(); + const resolve = bindResolve(callback)(response); + + const accountSid = context.ACCOUNT_SID; + const authToken = context.AUTH_TOKEN; + const { Token: token } = event; + + // Check for token presence + if (!token) { + console.error('Token is missing in the request.'); + resolve(error400('token')); + return; + } + + try { + // Validate the token + const tokenResult: TokenValidatorResponse = await validator( + token as string, + accountSid, + authToken, + ); + + // Check if the user has supervisor role + const isSupervisorToken = isSupervisor(tokenResult); + console.log(`Is Supervisor Token: ${isSupervisorToken}`); + + if (!isSupervisorToken) { + console.error('Unauthorized access attempt by non-supervisor.'); + resolve( + error403(`Unauthorized: endpoint not open to non supervisors. ${isSupervisorToken}`), + ); + return; + } + + const { originalQueueSid } = event; + + // Initialize Twilio TaskRouter client + const taskRouterClient = context + .getTwilioClient() + .taskrouter.workspaces(context.TWILIO_WORKSPACE_SID); + + // Get list of queues and workflows + const queues = await taskRouterClient.taskQueues.list(); + + const switchboardQueue = queues.find((queue) => queue.friendlyName === 'Switchboard Queue'); + if (!switchboardQueue) { + console.error('Switchboard Queue not found.'); + resolve(error400('Switchboard Queue not found')); + return; + } + const originalQueue = queues.find((queue) => queue.sid === originalQueueSid); + if (!originalQueue) { + console.error('Original Queue not found.'); + resolve(error400('Original Queue not found')); + return; + } + console.log(`>>> Original Queue: ${originalQueue.friendlyName}, SID: ${originalQueue.sid}`); + + const workflows = await taskRouterClient.workflows.list(); + const masterWorkflow = workflows.find( + (workflow) => workflow.friendlyName === 'Master Workflow', + ); + const transferWorkflow = workflows.find( + (workflow) => workflow.friendlyName === 'Queue Transfers Workflow', + ); + + if (!masterWorkflow || !transferWorkflow) { + console.error(`Workflow not found: ${masterWorkflow}, ${transferWorkflow}`); + resolve(error400('Workflow not found')); + return; + } + + const masterConfiguration = JSON.stringify(masterWorkflow.configuration, null, 2); + const transferConfiguration = JSON.stringify(transferWorkflow.configuration, null, 2); + + console.log(`>>> Master Workflow Configuration: ${masterConfiguration}`); + console.log(`>>> Transfer Workflow Configuration: ${transferConfiguration}`); + + const result = 'Switchboarding mode enabled'; + resolve(success(result)); + } catch (err: any) { + console.error('Error in handler:', err); + resolve(error500(err)); + } + }, +); From fc6c7c82fee512319d8ac70014c15d785bf9c519 Mon Sep 17 00:00:00 2001 From: mythilytm Date: Thu, 1 May 2025 12:41:45 -0400 Subject: [PATCH 02/22] log configurations for queues and workflows --- functions/{webhooks => }/assignSwitchboarding.ts | 1 + 1 file changed, 1 insertion(+) rename functions/{webhooks => }/assignSwitchboarding.ts (98%) diff --git a/functions/webhooks/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts similarity index 98% rename from functions/webhooks/assignSwitchboarding.ts rename to functions/assignSwitchboarding.ts index 8407e791..a45a5565 100644 --- a/functions/webhooks/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -110,6 +110,7 @@ export const handler = TokenValidator( const transferWorkflow = workflows.find( (workflow) => workflow.friendlyName === 'Queue Transfers Workflow', ); + console.log(`>>> Workflows: ${JSON.stringify(workflows, null, 2)}`); if (!masterWorkflow || !transferWorkflow) { console.error(`Workflow not found: ${masterWorkflow}, ${transferWorkflow}`); From 7795223865b3275af45acb78789ed102decdf5d3 Mon Sep 17 00:00:00 2001 From: mythilytm Date: Thu, 1 May 2025 19:34:09 -0400 Subject: [PATCH 03/22] log configurations for workflows --- functions/assignSwitchboarding.ts | 38 +++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index a45a5565..5634af2d 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -104,20 +104,44 @@ export const handler = TokenValidator( console.log(`>>> Original Queue: ${originalQueue.friendlyName}, SID: ${originalQueue.sid}`); const workflows = await taskRouterClient.workflows.list(); + + // Log each workflow individually with key properties + console.log('>>> Workflows Information:'); + workflows.forEach((workflow, index) => { + console.log(`\n>>> Workflow ${index + 1}:`); + console.log(`>>> Friendly Name: ${workflow.friendlyName}`); + console.log(`>>> Assignment Callback URL: ${workflow.assignmentCallbackUrl || 'N/A'}`); + console.log(`>>> Task Reservation Timeout: ${workflow.taskReservationTimeout}`); + try { + // Parse configuration to log it in a readable way + const config = JSON.parse(workflow.configuration); + console.log( + `>>> Configuration Summary: ${JSON.stringify(config, null, 2).substring(0, 500)}...`, + ); + } catch (e) { + console.log(`>>> Configuration (raw): ${workflow.configuration}`); + } + }); + + // Check for Master Workflow const masterWorkflow = workflows.find( (workflow) => workflow.friendlyName === 'Master Workflow', ); - const transferWorkflow = workflows.find( - (workflow) => workflow.friendlyName === 'Queue Transfers Workflow', - ); - console.log(`>>> Workflows: ${JSON.stringify(workflows, null, 2)}`); + if (!masterWorkflow) { + console.error('Master Workflow not found'); + resolve(error400('Master Workflow not found parameter not provided')); + return; + } - if (!masterWorkflow || !transferWorkflow) { - console.error(`Workflow not found: ${masterWorkflow}, ${transferWorkflow}`); - resolve(error400('Workflow not found')); + // Check for Transfer Workflow + const transferWorkflow = workflows.find((workflow) => workflow.friendlyName === 'Transfers'); + if (!transferWorkflow) { + console.error('Transfers Workflow not found'); + resolve(error400('Transfers Workflow not found parameter not provided')); return; } + // Both workflows found, proceed const masterConfiguration = JSON.stringify(masterWorkflow.configuration, null, 2); const transferConfiguration = JSON.stringify(transferWorkflow.configuration, null, 2); From c97343bb7fc1503ad7ae6e69deebc634351ae276 Mon Sep 17 00:00:00 2001 From: mythilytm Date: Thu, 1 May 2025 19:36:02 -0400 Subject: [PATCH 04/22] log configurations for workflows --- functions/assignSwitchboarding.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index 5634af2d..ae9ebede 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -127,23 +127,23 @@ export const handler = TokenValidator( const masterWorkflow = workflows.find( (workflow) => workflow.friendlyName === 'Master Workflow', ); - if (!masterWorkflow) { - console.error('Master Workflow not found'); - resolve(error400('Master Workflow not found parameter not provided')); - return; - } + // if (!masterWorkflow) { + // console.error('Master Workflow not found'); + // resolve(error400('Master Workflow not found parameter not provided')); + // return; + // } - // Check for Transfer Workflow + // // Check for Transfer Workflow const transferWorkflow = workflows.find((workflow) => workflow.friendlyName === 'Transfers'); - if (!transferWorkflow) { - console.error('Transfers Workflow not found'); - resolve(error400('Transfers Workflow not found parameter not provided')); - return; - } + // if (!transferWorkflow) { + // console.error('Transfers Workflow not found'); + // resolve(error400('Transfers Workflow not found parameter not provided')); + // return; + // } // Both workflows found, proceed - const masterConfiguration = JSON.stringify(masterWorkflow.configuration, null, 2); - const transferConfiguration = JSON.stringify(transferWorkflow.configuration, null, 2); + const masterConfiguration = JSON.stringify(masterWorkflow?.configuration, null, 2); + const transferConfiguration = JSON.stringify(transferWorkflow?.configuration, null, 2); console.log(`>>> Master Workflow Configuration: ${masterConfiguration}`); console.log(`>>> Transfer Workflow Configuration: ${transferConfiguration}`); From 153a3a4221cd68e65f5cf81f5dba3934f728ae72 Mon Sep 17 00:00:00 2001 From: mythilytm Date: Thu, 1 May 2025 23:05:57 -0400 Subject: [PATCH 05/22] add switchboarding listener for transfers, operations logic --- functions/assignSwitchboarding.ts | 213 ++++++++++++++---- .../switchboardingListener.private.ts | 185 +++++++++++++++ 2 files changed, 350 insertions(+), 48 deletions(-) create mode 100644 functions/taskrouterListeners/switchboardingListener.private.ts diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index ae9ebede..fa3c308e 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -34,15 +34,78 @@ type EnvVars = { export type Body = { originalQueueSid?: string; + operation?: 'enable' | 'disable' | 'status'; request: { cookies: {}; headers: {} }; Token: string; }; export type TokenValidatorResponse = { worker_sid?: string; roles?: string[] }; +type SwitchboardingState = { + isEnabled: boolean; + originalQueueSid?: string; + originalQueueName?: string; + enabledBy?: string; + enabledAt?: string; +}; + +const switchboardingState: SwitchboardingState = { + isEnabled: false, +}; + const isSupervisor = (tokenResult: TokenValidatorResponse) => Array.isArray(tokenResult.roles) && tokenResult.roles.includes('supervisor'); +/** + * Adds a filter to the workflow configuration to redirect calls from originalQueue to switchboardQueue + * except for calls that have been transferred (to avoid bouncing) + */ +function addSwitchboardingFilter( + config: any, + originalQueueSid: string, + switchboardQueueSid: string, +): any { + // Clone the configuration to avoid modifying the original + const updatedConfig = JSON.parse(JSON.stringify(config)); + + // Add a new filter at the top of the filter chain to redirect to switchboard + // This filter should check if: + // 1. The task is targeting the original queue + // 2. The task is not a transfer (check transferMeta or other attributes) + const switchboardingFilter = { + filter_friendly_name: 'Switchboarding Active Filter', + expression: `task.taskQueueSid == "${originalQueueSid}" AND !task.transferMeta`, + targets: [ + { + queue: switchboardQueueSid, + expression: 'worker.available == true', // Only route to available workers + priority: 100, // High priority + skip_if: 'task.transferMeta', // Skip if it's a transfer + }, + ], + }; + + // Insert the new filter at the beginning of the task_routing.filters array + updatedConfig.task_routing.filters.unshift(switchboardingFilter); + + return updatedConfig; +} + +/** + * Removes the switchboarding filter from the workflow configuration + */ +function removeSwitchboardingFilter(config: any): any { + // Clone the configuration to avoid modifying the original + const updatedConfig = JSON.parse(JSON.stringify(config)); + + // Remove the switchboarding filter (identified by its friendly name) + updatedConfig.task_routing.filters = updatedConfig.task_routing.filters.filter( + (filter: any) => filter.filter_friendly_name !== 'Switchboarding Active Filter', + ); + + return updatedConfig; +} + export const handler = TokenValidator( async (context: Context, event: Body, callback: ServerlessCallback) => { const response = responseWithCors(); @@ -52,7 +115,6 @@ export const handler = TokenValidator( const authToken = context.AUTH_TOKEN; const { Token: token } = event; - // Check for token presence if (!token) { console.error('Token is missing in the request.'); resolve(error400('token')); @@ -60,14 +122,12 @@ export const handler = TokenValidator( } try { - // Validate the token const tokenResult: TokenValidatorResponse = await validator( token as string, accountSid, authToken, ); - // Check if the user has supervisor role const isSupervisorToken = isSupervisor(tokenResult); console.log(`Is Supervisor Token: ${isSupervisorToken}`); @@ -79,14 +139,12 @@ export const handler = TokenValidator( return; } - const { originalQueueSid } = event; + const { originalQueueSid, operation = 'status' } = event; - // Initialize Twilio TaskRouter client const taskRouterClient = context .getTwilioClient() .taskrouter.workspaces(context.TWILIO_WORKSPACE_SID); - // Get list of queues and workflows const queues = await taskRouterClient.taskQueues.list(); const switchboardQueue = queues.find((queue) => queue.friendlyName === 'Switchboard Queue'); @@ -95,61 +153,120 @@ export const handler = TokenValidator( resolve(error400('Switchboard Queue not found')); return; } + + if (operation === 'status') { + console.log('Returning switchboarding status'); + resolve(success(switchboardingState)); + return; + } + + if (!originalQueueSid) { + console.error('Original Queue SID is required for enable/disable operations.'); + resolve(error400('Original Queue SID is required')); + return; + } + const originalQueue = queues.find((queue) => queue.sid === originalQueueSid); if (!originalQueue) { console.error('Original Queue not found.'); resolve(error400('Original Queue not found')); return; } + console.log( + `>>> Switchboard Queue: ${switchboardQueue.friendlyName}, SID: ${switchboardQueue.sid}`, + ); console.log(`>>> Original Queue: ${originalQueue.friendlyName}, SID: ${originalQueue.sid}`); const workflows = await taskRouterClient.workflows.list(); - // Log each workflow individually with key properties - console.log('>>> Workflows Information:'); - workflows.forEach((workflow, index) => { - console.log(`\n>>> Workflow ${index + 1}:`); - console.log(`>>> Friendly Name: ${workflow.friendlyName}`); - console.log(`>>> Assignment Callback URL: ${workflow.assignmentCallbackUrl || 'N/A'}`); - console.log(`>>> Task Reservation Timeout: ${workflow.taskReservationTimeout}`); - try { - // Parse configuration to log it in a readable way - const config = JSON.parse(workflow.configuration); - console.log( - `>>> Configuration Summary: ${JSON.stringify(config, null, 2).substring(0, 500)}...`, - ); - } catch (e) { - console.log(`>>> Configuration (raw): ${workflow.configuration}`); - } - }); - - // Check for Master Workflow const masterWorkflow = workflows.find( (workflow) => workflow.friendlyName === 'Master Workflow', ); - // if (!masterWorkflow) { - // console.error('Master Workflow not found'); - // resolve(error400('Master Workflow not found parameter not provided')); - // return; - // } - - // // Check for Transfer Workflow - const transferWorkflow = workflows.find((workflow) => workflow.friendlyName === 'Transfers'); - // if (!transferWorkflow) { - // console.error('Transfers Workflow not found'); - // resolve(error400('Transfers Workflow not found parameter not provided')); - // return; - // } - - // Both workflows found, proceed - const masterConfiguration = JSON.stringify(masterWorkflow?.configuration, null, 2); - const transferConfiguration = JSON.stringify(transferWorkflow?.configuration, null, 2); - - console.log(`>>> Master Workflow Configuration: ${masterConfiguration}`); - console.log(`>>> Transfer Workflow Configuration: ${transferConfiguration}`); - - const result = 'Switchboarding mode enabled'; - resolve(success(result)); + + if (!masterWorkflow) { + console.error('Master Workflow not found'); + resolve(error400('Master Workflow not found')); + return; + } + console.log( + `>>> Master Workflow: ${masterWorkflow.friendlyName}, SID: ${masterWorkflow.sid}`, + ); + + if (operation === 'enable') { + console.log('Enabling switchboarding mode...'); + if ( + switchboardingState.isEnabled && + switchboardingState.originalQueueSid === originalQueueSid + ) { + console.log('Switchboarding is already enabled for this queue.'); + resolve( + success({ + message: 'Switchboarding is already active for this queue', + state: switchboardingState, + }), + ); + return; + } + + const masterConfig = JSON.parse(masterWorkflow.configuration); + + const updatedMasterConfig = addSwitchboardingFilter( + masterConfig, + originalQueue.sid, + switchboardQueue.sid, + ); + + await taskRouterClient.workflows(masterWorkflow.sid).update({ + configuration: JSON.stringify(updatedMasterConfig), + }); + + console.log('Switchboarding mode enabled'); + resolve( + success({ + message: 'Switchboarding mode enabled', + state: { + isEnabled: true, + originalQueueSid, + originalQueueName: originalQueue.friendlyName, + enabledBy: tokenResult.worker_sid, + enabledAt: new Date().toISOString(), + }, + }), + ); + } else if (operation === 'disable') { + console.log('Disabling switchboarding mode...'); + if (!switchboardingState.isEnabled) { + console.log('Switchboarding is not currently enabled.'); + resolve( + success({ + message: 'Switchboarding is not currently active', + state: switchboardingState, + }), + ); + return; + } + + const masterConfig = JSON.parse(masterWorkflow.configuration); + const updatedMasterConfig = removeSwitchboardingFilter(masterConfig); + + await taskRouterClient.workflows(masterWorkflow.sid).update({ + configuration: JSON.stringify(updatedMasterConfig), + }); + + console.log('Switchboarding mode disabled'); + resolve( + success({ + message: 'Switchboarding mode disabled', + state: { + isEnabled: false, + originalQueueSid: undefined, + originalQueueName: undefined, + enabledBy: undefined, + enabledAt: undefined, + }, + }), + ); + } } catch (err: any) { console.error('Error in handler:', err); resolve(error500(err)); diff --git a/functions/taskrouterListeners/switchboardingListener.private.ts b/functions/taskrouterListeners/switchboardingListener.private.ts new file mode 100644 index 00000000..fc6f41c4 --- /dev/null +++ b/functions/taskrouterListeners/switchboardingListener.private.ts @@ -0,0 +1,185 @@ +/** + * 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 '@twilio-labs/serverless-runtime-types'; +import { Context } from '@twilio-labs/serverless-runtime-types/types'; + +import { + TaskrouterListener, + EventFields, + EventType, + TASK_CREATED, + TASK_QUEUE_ENTERED, + TASK_QUEUE_MOVED, + TASK_UPDATED, +} from '@tech-matters/serverless-helpers/taskrouter'; + +export const eventTypes: EventType[] = [ + TASK_CREATED, + TASK_QUEUE_ENTERED, + TASK_QUEUE_MOVED, + TASK_UPDATED, +]; + +type EnvVars = { + TWILIO_WORKSPACE_SID: string; +}; + +/** + * Add a flag to tasks that are being handled by the switchboarding system + * This ensures we can identify these tasks later for proper routing + */ +const markTaskForSwitchboarding = async ( + context: Context, + taskSid: string, + attributes: any, +) => { + const client = context.getTwilioClient(); + const updatedAttributes = { + ...attributes, + switchboardingHandled: true, + switchboardingTimestamp: new Date().toISOString(), + }; + + await client.taskrouter.v1.workspaces + .get(context.TWILIO_WORKSPACE_SID) + .tasks.get(taskSid) + .update({ attributes: JSON.stringify(updatedAttributes) }); + + console.log(`Task ${taskSid} marked for switchboarding`); +}; + +/** + * Check if the task is being transferred back to the original queue + * This is important for allowing supervisors to transfer calls back + * to the original queue without having them bounce to switchboard + */ +const isTransferBackToOriginal = (taskAttributes: any, queueSid: string): boolean => { + // Check if the task was previously handled by switchboarding + if (!taskAttributes.switchboardingHandled) { + return false; + } + + // Check if this is a transfer operation + if (!taskAttributes.transferMeta) { + return false; + } + + // Check if the queue is the original queue that had switchboarding enabled + // In a real implementation, we would fetch this from a persistent storage + // but for now, we'll use the queueSid parameter that was passed in + console.log(`Checking if queue ${queueSid} is the original queue for switchboarding exemption`); + + // For now, any transfer from a switchboarded task will be exempt + // In a more complete implementation, we would check that this specific queue + // was the original queue that had switchboarding enabled + return true; +}; + +/** + * 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 = (event: EventFields) => eventTypes.includes(event.EventType); + +export const handleEvent = async (context: Context, event: EventFields) => { + try { + const { EventType: eventType, TaskSid: taskSid, TaskAttributes: taskAttributesString } = event; + + console.log(`===== Executing SwitchboardingListener for event: ${eventType} =====`); + + // Parse the task attributes + const taskAttributes = JSON.parse(taskAttributesString); + + // Get queue SID from task attributes instead of from event directly + const taskQueueSid = taskAttributes.taskQueueSid || taskAttributes.task_queue_sid; + + // Log key attributes for debugging + console.log(`Task ${taskSid} entering queue ${taskQueueSid || 'unknown'}`); + console.log( + `Task attributes: ${JSON.stringify({ + callSid: taskAttributes.call_sid, + direction: taskAttributes.direction, + transferMeta: taskAttributes.transferMeta, + switchboardingHandled: taskAttributes.switchboardingHandled, + })}`, + ); + + // If this is a new task entering the Switchboard Queue and isn't already marked + if ( + (eventType === TASK_QUEUE_ENTERED || eventType === TASK_QUEUE_MOVED) && + !taskAttributes.switchboardingHandled + ) { + // Fetch the queue to check if it's the switchboard queue + const client = context.getTwilioClient(); + + // If we have a queue SID in the attributes, use it to look up the queue + if (taskQueueSid) { + const queue = await client.taskrouter.v1.workspaces + .get(context.TWILIO_WORKSPACE_SID) + .taskQueues(taskQueueSid) + .fetch(); + + if (queue.friendlyName === 'Switchboard Queue') { + await markTaskForSwitchboarding(context, taskSid, taskAttributes); + console.log('Task marked for switchboarding handling'); + } + } else { + console.log( + 'TaskQueueSid not found in task attributes, cannot determine if this is a switchboard queue', + ); + } + } + + // Handle transfers specifically to prevent bouncing back to switchboard + if (eventType === TASK_UPDATED) { + // For task updates, check if this is a transfer back to original queue + // Note: Since TaskQueueSid may not be directly available on task update events, + // we need to extract it from attributes or determine it from other properties + const targetQueueSid = + taskAttributes.taskQueueSid || taskAttributes.task_queue_sid || taskAttributes.targetSid; + + if (targetQueueSid && isTransferBackToOriginal(taskAttributes, targetQueueSid)) { + // If this is a transfer back to original queue, mark it as a special transfer + // to prevent it from bouncing back to the switchboard queue + const client = context.getTwilioClient(); + const updatedAttributes = { + ...taskAttributes, + switchboardingTransferExempt: true, + switchboardingTransferTimestamp: new Date().toISOString(), + }; + + await client.taskrouter.v1.workspaces + .get(context.TWILIO_WORKSPACE_SID) + .tasks.get(taskSid) + .update({ attributes: JSON.stringify(updatedAttributes) }); + + console.log(`Task ${taskSid} marked as exempt from switchboarding redirection`); + } + } + } catch (err) { + console.error('Error in SwitchboardingListener:', err); + } +}; + +/** + * The taskrouter callback expects that all taskrouter listeners return + * a default object of type TaskrouterListener. + */ +export default { + shouldHandle, + handleEvent, +} as TaskrouterListener; From e015abcce56acb97f1041ddb44c196852e8ac34a Mon Sep 17 00:00:00 2001 From: mythilytm Date: Mon, 12 May 2025 14:05:31 -0400 Subject: [PATCH 06/22] debug switchboarding execution flow --- functions/assignSwitchboarding.ts | 156 ++++++++++++++++++++++-------- 1 file changed, 118 insertions(+), 38 deletions(-) diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index fa3c308e..e3880a27 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -30,6 +30,7 @@ type EnvVars = { TWILIO_WORKSPACE_SID: string; ACCOUNT_SID: string; AUTH_TOKEN: string; + SYNC_SERVICE_SID: string; }; export type Body = { @@ -49,10 +50,61 @@ type SwitchboardingState = { enabledAt?: string; }; -const switchboardingState: SwitchboardingState = { +// Sync document constants +const SWITCHBOARD_DOCUMENT_NAME = 'switchboard-state'; +const DEFAULT_SWITCHBOARD_STATE: SwitchboardingState = { isEnabled: false, }; +/** + * Get or create the switchboard document in Twilio Sync + */ +async function getSwitchboardStateDocument(client: any, syncServiceSid: string): Promise { + try { + return client.sync.services(syncServiceSid).documents(SWITCHBOARD_DOCUMENT_NAME).fetch(); + } catch (error: any) { + // If document doesn't exist, create it + if (error.status === 404) { + return client.sync.services(syncServiceSid).documents.create({ + uniqueName: SWITCHBOARD_DOCUMENT_NAME, + data: DEFAULT_SWITCHBOARD_STATE, + ttl: 48 * 60 * 60, // 48 hours + }); + } + throw error; + } +} + +/** + * Get current switchboarding state + */ +async function getSwitchboardState( + client: any, + syncServiceSid: string, +): Promise { + const document = await getSwitchboardStateDocument(client, syncServiceSid); + return document.data; +} + +/** + * Update switchboarding state + */ +async function updateSwitchboardState( + client: any, + syncServiceSid: string, + state: Partial, +): Promise { + const document = await getSwitchboardStateDocument(client, syncServiceSid); + const currentState = document.data; + const updatedState = { ...currentState, ...state }; + + await client.sync.services(syncServiceSid).documents(SWITCHBOARD_DOCUMENT_NAME).update({ + data: updatedState, + }); + + return updatedState; +} + const isSupervisor = (tokenResult: TokenValidatorResponse) => Array.isArray(tokenResult.roles) && tokenResult.roles.includes('supervisor'); @@ -108,6 +160,7 @@ function removeSwitchboardingFilter(config: any): any { export const handler = TokenValidator( async (context: Context, event: Body, callback: ServerlessCallback) => { + console.log('>>> 1. FUNCTION ENTRY: Starting switchboarding handler'); const response = responseWithCors(); const resolve = bindResolve(callback)(response); @@ -116,12 +169,13 @@ export const handler = TokenValidator( const { Token: token } = event; if (!token) { - console.error('Token is missing in the request.'); + console.error('>>> 1b ERROR: Token is missing in the request'); resolve(error400('token')); return; } try { + console.log('>>> 1a: Validating token'); const tokenResult: TokenValidatorResponse = await validator( token as string, accountSid, @@ -129,10 +183,10 @@ export const handler = TokenValidator( ); const isSupervisorToken = isSupervisor(tokenResult); - console.log(`Is Supervisor Token: ${isSupervisorToken}`); + console.log(`>>> 1a: Is Supervisor Token: ${isSupervisorToken}`); if (!isSupervisorToken) { - console.error('Unauthorized access attempt by non-supervisor.'); + console.error('>>> 1c ERROR: Unauthorized access attempt by non-supervisor'); resolve( error403(`Unauthorized: endpoint not open to non supervisors. ${isSupervisorToken}`), ); @@ -140,43 +194,54 @@ export const handler = TokenValidator( } const { originalQueueSid, operation = 'status' } = event; + console.log(`>>> 2. OPERATION: Request operation is ${operation}`); - const taskRouterClient = context - .getTwilioClient() - .taskrouter.workspaces(context.TWILIO_WORKSPACE_SID); + const client = context.getTwilioClient(); + const syncServiceSid = context.SYNC_SERVICE_SID; + console.log( + `>>> 2a. STATE MANAGEMENT: Setting up Twilio clients with SyncServiceSid: ${syncServiceSid}`, + ); + const taskRouterClient = client.taskrouter.workspaces(context.TWILIO_WORKSPACE_SID); + console.log('>>> 3. Fetching TaskRouter queues'); const queues = await taskRouterClient.taskQueues.list(); const switchboardQueue = queues.find((queue) => queue.friendlyName === 'Switchboard Queue'); if (!switchboardQueue) { - console.error('Switchboard Queue not found.'); + console.error('>>> 3b. QUEUES ERROR: Switchboard Queue not found'); resolve(error400('Switchboard Queue not found')); return; } + console.log(`>>> 3a. Found Switchboard Queue with SID: ${switchboardQueue.sid}`); if (operation === 'status') { - console.log('Returning switchboarding status'); + console.log('>>> 4. STATUS: Retrieving current switchboarding status'); + const switchboardingState = await getSwitchboardState(client, syncServiceSid); + console.log(`>>> 4a. STATUS: Current state - isEnabled: ${switchboardingState.isEnabled}`); resolve(success(switchboardingState)); return; } if (!originalQueueSid) { - console.error('Original Queue SID is required for enable/disable operations.'); + console.error('>>> 5b ERROR: Original Queue SID is required for enable/disable operations'); resolve(error400('Original Queue SID is required')); return; } const originalQueue = queues.find((queue) => queue.sid === originalQueueSid); if (!originalQueue) { - console.error('Original Queue not found.'); + console.error('>>> 5c ERROR: Original Queue not found'); resolve(error400('Original Queue not found')); return; } console.log( - `>>> Switchboard Queue: ${switchboardQueue.friendlyName}, SID: ${switchboardQueue.sid}`, + `>>> 5a. Switchboard Queue: ${switchboardQueue.friendlyName}, SID: ${switchboardQueue.sid}`, + ); + console.log( + `>>> 5a. Original Queue: ${originalQueue.friendlyName}, SID: ${originalQueue.sid}`, ); - console.log(`>>> Original Queue: ${originalQueue.friendlyName}, SID: ${originalQueue.sid}`); + console.log('>>> 6. WORKFLOWS: Fetching TaskRouter workflows'); const workflows = await taskRouterClient.workflows.list(); const masterWorkflow = workflows.find( @@ -184,21 +249,20 @@ export const handler = TokenValidator( ); if (!masterWorkflow) { - console.error('Master Workflow not found'); + console.error('>>> 6b. WORKFLOWS ERROR: Master Workflow not found'); resolve(error400('Master Workflow not found')); return; } - console.log( - `>>> Master Workflow: ${masterWorkflow.friendlyName}, SID: ${masterWorkflow.sid}`, - ); + console.log(`>>> 6a. WORKFLOWS: Found Master Workflow with SID: ${masterWorkflow.sid}`); if (operation === 'enable') { - console.log('Enabling switchboarding mode...'); + console.log('>>> 7. ENABLE: Enabling switchboarding mode'); + const switchboardingState = await getSwitchboardState(client, syncServiceSid); if ( switchboardingState.isEnabled && switchboardingState.originalQueueSid === originalQueueSid ) { - console.log('Switchboarding is already enabled for this queue.'); + console.log('>>> 7b. ENABLE: Switchboarding is already enabled for this queue'); resolve( success({ message: 'Switchboarding is already active for this queue', @@ -207,36 +271,44 @@ export const handler = TokenValidator( ); return; } + console.log('>>> 7a. ENABLE: Proceeding with switchboarding activation'); + console.log('>>> 8. CONFIG UPDATE: Parsing and updating Master Workflow configuration'); const masterConfig = JSON.parse(masterWorkflow.configuration); + console.log('>>> 8a. CONFIG UPDATE: Adding switchboarding filter to workflow'); const updatedMasterConfig = addSwitchboardingFilter( masterConfig, originalQueue.sid, switchboardQueue.sid, ); + console.log('>>> 8a. CONFIG UPDATE: Applying updated configuration to Master Workflow'); await taskRouterClient.workflows(masterWorkflow.sid).update({ configuration: JSON.stringify(updatedMasterConfig), }); - console.log('Switchboarding mode enabled'); + console.log('>>> 9. STATE UPDATE: Updating switchboarding state in Sync'); + const updatedState = await updateSwitchboardState(client, syncServiceSid, { + isEnabled: true, + originalQueueSid, + originalQueueName: originalQueue.friendlyName, + enabledBy: tokenResult.worker_sid, + enabledAt: new Date().toISOString(), + }); + + console.log('>>> 9a. STATE UPDATE: Switchboarding mode successfully enabled'); resolve( success({ message: 'Switchboarding mode enabled', - state: { - isEnabled: true, - originalQueueSid, - originalQueueName: originalQueue.friendlyName, - enabledBy: tokenResult.worker_sid, - enabledAt: new Date().toISOString(), - }, + state: updatedState, }), ); } else if (operation === 'disable') { - console.log('Disabling switchboarding mode...'); + console.log('>>> 7. DISABLE: Disabling switchboarding mode'); + const switchboardingState = await getSwitchboardState(client, syncServiceSid); if (!switchboardingState.isEnabled) { - console.log('Switchboarding is not currently enabled.'); + console.log('>>> 7c. DISABLE: Switchboarding is not currently enabled'); resolve( success({ message: 'Switchboarding is not currently active', @@ -245,31 +317,39 @@ export const handler = TokenValidator( ); return; } + console.log('>>> 7a. DISABLE: Proceeding with switchboarding deactivation'); + console.log('>>> 8. CONFIG UPDATE: Parsing and updating Master Workflow configuration'); const masterConfig = JSON.parse(masterWorkflow.configuration); + console.log('>>> 8a. CONFIG UPDATE: Removing switchboarding filter from workflow'); const updatedMasterConfig = removeSwitchboardingFilter(masterConfig); + console.log('>>> 8a. CONFIG UPDATE: Applying updated configuration to Master Workflow'); await taskRouterClient.workflows(masterWorkflow.sid).update({ configuration: JSON.stringify(updatedMasterConfig), }); - console.log('Switchboarding mode disabled'); + console.log('>>> 9. STATE UPDATE: Updating switchboarding state in Sync'); + const updatedState = await updateSwitchboardState(client, syncServiceSid, { + isEnabled: false, + originalQueueSid: undefined, + originalQueueName: undefined, + enabledBy: undefined, + enabledAt: undefined, + }); + + console.log('>>> 9a. STATE UPDATE: Switchboarding mode successfully disabled'); resolve( success({ message: 'Switchboarding mode disabled', - state: { - isEnabled: false, - originalQueueSid: undefined, - originalQueueName: undefined, - enabledBy: undefined, - enabledAt: undefined, - }, + state: updatedState, }), ); } } catch (err: any) { - console.error('Error in handler:', err); + console.error('>>> Error in switchboarding handler:', err); resolve(error500(err)); } + console.log('>>> Switchboarding handler completed'); }, ); From 88c4673cad4e5ba86b9cd22d1f471734323add71 Mon Sep 17 00:00:00 2001 From: mythilytm Date: Mon, 12 May 2025 14:23:24 -0400 Subject: [PATCH 07/22] debug switchboarding execution flow --- functions/assignSwitchboarding.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index e3880a27..ad89ab04 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -83,7 +83,16 @@ async function getSwitchboardState( syncServiceSid: string, ): Promise { const document = await getSwitchboardStateDocument(client, syncServiceSid); - return document.data; + + const state = document.data || {}; + + return { + isEnabled: state.isEnabled === undefined ? false : state.isEnabled, + originalQueueSid: state.originalQueueSid, + originalQueueName: state.originalQueueName, + enabledBy: state.enabledBy, + enabledAt: state.enabledAt, + }; } /** @@ -194,7 +203,7 @@ export const handler = TokenValidator( } const { originalQueueSid, operation = 'status' } = event; - console.log(`>>> 2. OPERATION: Request operation is ${operation}`); + console.log(`>>> 2. EVENT: ${JSON.stringify(event)}`); const client = context.getTwilioClient(); const syncServiceSid = context.SYNC_SERVICE_SID; @@ -217,7 +226,12 @@ export const handler = TokenValidator( if (operation === 'status') { console.log('>>> 4. STATUS: Retrieving current switchboarding status'); const switchboardingState = await getSwitchboardState(client, syncServiceSid); - console.log(`>>> 4a. STATUS: Current state - isEnabled: ${switchboardingState.isEnabled}`); + console.log( + `>>> 4a. STATUS: Current state - isEnabled: ${ + switchboardingState.isEnabled === undefined ? false : switchboardingState.isEnabled + }`, + ); + console.log('>>> 4b. STATUS: Full switchboard state:', JSON.stringify(switchboardingState)); resolve(success(switchboardingState)); return; } From 972d20ded1247855ab649c63f5bfb2a8613c1e2b Mon Sep 17 00:00:00 2001 From: mythilytm Date: Mon, 12 May 2025 14:37:18 -0400 Subject: [PATCH 08/22] debug switchboarding execution flow --- functions/assignSwitchboarding.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index ad89ab04..1d63824c 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -202,8 +202,8 @@ export const handler = TokenValidator( return; } - const { originalQueueSid, operation = 'status' } = event; - console.log(`>>> 2. EVENT: ${JSON.stringify(event)}`); + const { originalQueueSid, operation } = event; + console.log(`>>> 2. event:, operation: ${operation}, originalQueueSid: ${originalQueueSid}`); const client = context.getTwilioClient(); const syncServiceSid = context.SYNC_SERVICE_SID; From 4a97a7e38b5f28ac8617acb6db332c6633af01e7 Mon Sep 17 00:00:00 2001 From: mythilytm Date: Mon, 12 May 2025 20:06:34 -0400 Subject: [PATCH 09/22] move update sync state to serverless --- functions/assignSwitchboarding.ts | 57 ++++++++++++++++--------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index 1d63824c..b3f8e212 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -35,7 +35,7 @@ type EnvVars = { export type Body = { originalQueueSid?: string; - operation?: 'enable' | 'disable' | 'status'; + operation: 'enable' | 'disable' | 'status'; request: { cookies: {}; headers: {} }; Token: string; }; @@ -43,17 +43,20 @@ export type Body = { export type TokenValidatorResponse = { worker_sid?: string; roles?: string[] }; type SwitchboardingState = { - isEnabled: boolean; - originalQueueSid?: string; - originalQueueName?: string; - enabledBy?: string; - enabledAt?: string; + isSwitchboardingActive: boolean; // Using the frontend naming for consistency + queueSid?: string; + queueName?: string; + supervisorWorkerSid?: string; + startTime?: string; }; -// Sync document constants const SWITCHBOARD_DOCUMENT_NAME = 'switchboard-state'; const DEFAULT_SWITCHBOARD_STATE: SwitchboardingState = { - isEnabled: false, + isSwitchboardingActive: false, + queueSid: undefined, + queueName: undefined, + startTime: undefined, + supervisorWorkerSid: undefined, }; /** @@ -87,11 +90,11 @@ async function getSwitchboardState( const state = document.data || {}; return { - isEnabled: state.isEnabled === undefined ? false : state.isEnabled, - originalQueueSid: state.originalQueueSid, - originalQueueName: state.originalQueueName, - enabledBy: state.enabledBy, - enabledAt: state.enabledAt, + isSwitchboardingActive: state.isSwitchboardingActive === undefined ? false : state.isSwitchboardingActive, + queueSid: state.queueSid, + queueName: state.queueName, + startTime: state.startTime, + supervisorWorkerSid: state.supervisorWorkerSid, }; } @@ -228,7 +231,7 @@ export const handler = TokenValidator( const switchboardingState = await getSwitchboardState(client, syncServiceSid); console.log( `>>> 4a. STATUS: Current state - isEnabled: ${ - switchboardingState.isEnabled === undefined ? false : switchboardingState.isEnabled + switchboardingState.isSwitchboardingActive === undefined ? false : switchboardingState.isSwitchboardingActive }`, ); console.log('>>> 4b. STATUS: Full switchboard state:', JSON.stringify(switchboardingState)); @@ -273,8 +276,8 @@ export const handler = TokenValidator( console.log('>>> 7. ENABLE: Enabling switchboarding mode'); const switchboardingState = await getSwitchboardState(client, syncServiceSid); if ( - switchboardingState.isEnabled && - switchboardingState.originalQueueSid === originalQueueSid + switchboardingState.isSwitchboardingActive && + switchboardingState.queueSid === originalQueueSid ) { console.log('>>> 7b. ENABLE: Switchboarding is already enabled for this queue'); resolve( @@ -304,11 +307,11 @@ export const handler = TokenValidator( console.log('>>> 9. STATE UPDATE: Updating switchboarding state in Sync'); const updatedState = await updateSwitchboardState(client, syncServiceSid, { - isEnabled: true, - originalQueueSid, - originalQueueName: originalQueue.friendlyName, - enabledBy: tokenResult.worker_sid, - enabledAt: new Date().toISOString(), + isSwitchboardingActive: true, + queueSid: originalQueueSid, + queueName: originalQueue.friendlyName, + supervisorWorkerSid: tokenResult.worker_sid, + startTime: new Date().toISOString(), }); console.log('>>> 9a. STATE UPDATE: Switchboarding mode successfully enabled'); @@ -321,7 +324,7 @@ export const handler = TokenValidator( } else if (operation === 'disable') { console.log('>>> 7. DISABLE: Disabling switchboarding mode'); const switchboardingState = await getSwitchboardState(client, syncServiceSid); - if (!switchboardingState.isEnabled) { + if (!switchboardingState.isSwitchboardingActive) { console.log('>>> 7c. DISABLE: Switchboarding is not currently enabled'); resolve( success({ @@ -345,11 +348,11 @@ export const handler = TokenValidator( console.log('>>> 9. STATE UPDATE: Updating switchboarding state in Sync'); const updatedState = await updateSwitchboardState(client, syncServiceSid, { - isEnabled: false, - originalQueueSid: undefined, - originalQueueName: undefined, - enabledBy: undefined, - enabledAt: undefined, + isSwitchboardingActive: false, + queueSid: undefined, + queueName: undefined, + supervisorWorkerSid: undefined, + startTime: undefined, }); console.log('>>> 9a. STATE UPDATE: Switchboarding mode successfully disabled'); From 038acc287f1cab1e8aebcd4db6db7c7a0dc46723 Mon Sep 17 00:00:00 2001 From: mythilytm Date: Mon, 12 May 2025 20:31:56 -0400 Subject: [PATCH 10/22] debug error switchboarding --- functions/assignSwitchboarding.ts | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index b3f8e212..171270e7 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -90,7 +90,8 @@ async function getSwitchboardState( const state = document.data || {}; return { - isSwitchboardingActive: state.isSwitchboardingActive === undefined ? false : state.isSwitchboardingActive, + isSwitchboardingActive: + state.isSwitchboardingActive === undefined ? false : state.isSwitchboardingActive, queueSid: state.queueSid, queueName: state.queueName, startTime: state.startTime, @@ -231,7 +232,9 @@ export const handler = TokenValidator( const switchboardingState = await getSwitchboardState(client, syncServiceSid); console.log( `>>> 4a. STATUS: Current state - isEnabled: ${ - switchboardingState.isSwitchboardingActive === undefined ? false : switchboardingState.isSwitchboardingActive + switchboardingState.isSwitchboardingActive === undefined + ? false + : switchboardingState.isSwitchboardingActive }`, ); console.log('>>> 4b. STATUS: Full switchboard state:', JSON.stringify(switchboardingState)); @@ -365,7 +368,28 @@ export const handler = TokenValidator( } } catch (err: any) { console.error('>>> Error in switchboarding handler:', err); - resolve(error500(err)); + + // Enhanced error logging with details + console.error('>>> Error details:', { + message: err.message, + code: err.code, + status: err.status, + stack: err.stack, + }); + + // More specific error responses based on error type + if (err.message && err.message.includes('workflow')) { + console.error('>>> Workflow configuration error:', err); + } else if (err.message && err.message.includes('sync')) { + console.error('>>> Sync document error:', err); + } else if ( + err.status === 403 || + (err.message && err.message.toLowerCase().includes('permission')) + ) { + console.error('>>> Permission error:', err); + } else { + console.error('>>> Generic error:', err); + } } console.log('>>> Switchboarding handler completed'); }, From 96bc948571fdaf7493901fa6b3e5ac09e8e6fb06 Mon Sep 17 00:00:00 2001 From: mythilytm Date: Mon, 12 May 2025 20:42:08 -0400 Subject: [PATCH 11/22] debug error switchboarding - filter fix --- functions/assignSwitchboarding.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index 171270e7..89937a83 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -139,7 +139,7 @@ function addSwitchboardingFilter( // 2. The task is not a transfer (check transferMeta or other attributes) const switchboardingFilter = { filter_friendly_name: 'Switchboarding Active Filter', - expression: `task.taskQueueSid == "${originalQueueSid}" AND !task.transferMeta`, + expression: `task.taskQueueSid == "${originalQueueSid}" AND (task.transferMeta == null OR task.transferMeta == undefined)`, targets: [ { queue: switchboardQueueSid, From baa79065c39c2df041786d786b52a477af07cefe Mon Sep 17 00:00:00 2001 From: mythilytm Date: Mon, 12 May 2025 20:54:12 -0400 Subject: [PATCH 12/22] debug error switchboarding - filter fix --- functions/assignSwitchboarding.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index 89937a83..89db47cf 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -20,7 +20,6 @@ import { responseWithCors, bindResolve, error400, - error500, success, functionValidator as TokenValidator, error403, @@ -139,13 +138,13 @@ function addSwitchboardingFilter( // 2. The task is not a transfer (check transferMeta or other attributes) const switchboardingFilter = { filter_friendly_name: 'Switchboarding Active Filter', - expression: `task.taskQueueSid == "${originalQueueSid}" AND (task.transferMeta == null OR task.transferMeta == undefined)`, + expression: `task.taskQueueSid == "${originalQueueSid}" AND NOT(isnotnull(task.transferMeta))`, targets: [ { queue: switchboardQueueSid, expression: 'worker.available == true', // Only route to available workers priority: 100, // High priority - skip_if: 'task.transferMeta', // Skip if it's a transfer + skip_if: 'isnotnull(task.transferMeta)', // Skip if it's a transfer }, ], }; From 8b02ef7d0fe4178c4a2ae3b2b396adbffc0de1aa Mon Sep 17 00:00:00 2001 From: mythilytm Date: Mon, 12 May 2025 21:40:05 -0400 Subject: [PATCH 13/22] debug error switchboarding - filter fix --- functions/assignSwitchboarding.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index 89db47cf..cc1b7703 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -20,6 +20,7 @@ import { responseWithCors, bindResolve, error400, + error500, success, functionValidator as TokenValidator, error403, @@ -138,13 +139,13 @@ function addSwitchboardingFilter( // 2. The task is not a transfer (check transferMeta or other attributes) const switchboardingFilter = { filter_friendly_name: 'Switchboarding Active Filter', - expression: `task.taskQueueSid == "${originalQueueSid}" AND NOT(isnotnull(task.transferMeta))`, + expression: `task.taskQueueSid == "${originalQueueSid}" AND task.transferMeta == null`, targets: [ { queue: switchboardQueueSid, expression: 'worker.available == true', // Only route to available workers priority: 100, // High priority - skip_if: 'isnotnull(task.transferMeta)', // Skip if it's a transfer + skip_if: 'task.transferMeta != null', // Skip if it's a transfer }, ], }; From 8a911a6b8538186f76630706d9a4d935d136c7a7 Mon Sep 17 00:00:00 2001 From: mythilytm Date: Mon, 12 May 2025 22:15:31 -0400 Subject: [PATCH 14/22] debug error switchboarding - remove skipif --- functions/assignSwitchboarding.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index cc1b7703..a029ac57 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -145,7 +145,6 @@ function addSwitchboardingFilter( queue: switchboardQueueSid, expression: 'worker.available == true', // Only route to available workers priority: 100, // High priority - skip_if: 'task.transferMeta != null', // Skip if it's a transfer }, ], }; From 6a3e700cd90d5be85dee6d6611cf8c38ae671c1e Mon Sep 17 00:00:00 2001 From: mythilytm Date: Mon, 12 May 2025 22:34:15 -0400 Subject: [PATCH 15/22] debug error switchboarding - filter --- functions/assignSwitchboarding.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index a029ac57..2647b06c 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -134,17 +134,28 @@ function addSwitchboardingFilter( const updatedConfig = JSON.parse(JSON.stringify(config)); // Add a new filter at the top of the filter chain to redirect to switchboard - // This filter should check if: - // 1. The task is targeting the original queue - // 2. The task is not a transfer (check transferMeta or other attributes) + // This improved filter will: + // 1. Identify tasks that should go to the original queue + // 2. Mark them with attributes needed for switchboarding + // 3. Route them to the switchboard queue with proper attributes + // 4. Exclude tasks that are transfers or already handled const switchboardingFilter = { filter_friendly_name: 'Switchboarding Active Filter', - expression: `task.taskQueueSid == "${originalQueueSid}" AND task.transferMeta == null`, + expression: + '!has(task.transferMeta) AND !has(task.switchboardingHandled) AND !has(task.switchboardingTransferExempt)', targets: [ { queue: switchboardQueueSid, expression: 'worker.available == true', // Only route to available workers priority: 100, // High priority + // For tasks that would normally go to the original queue, redirect them + target_expression: `DEFAULT_TARGET_QUEUE_SID == '${originalQueueSid}'`, + // Add task attributes to help the Switchboard Workflow process it correctly + task_attributes: { + originalQueueSid, + needsSwitchboarding: true, + taskQueueSid: switchboardQueueSid, + }, }, ], }; From 926b88246ac16b56467a4a0c79af26a277dc24a7 Mon Sep 17 00:00:00 2001 From: mythilytm Date: Mon, 12 May 2025 22:42:51 -0400 Subject: [PATCH 16/22] debug error switchboarding - filter --- functions/assignSwitchboarding.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index 2647b06c..8c63d206 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -140,7 +140,7 @@ function addSwitchboardingFilter( // 3. Route them to the switchboard queue with proper attributes // 4. Exclude tasks that are transfers or already handled const switchboardingFilter = { - filter_friendly_name: 'Switchboarding Active Filter', + filter_friendly_name: 'Switchboarding Workflow', expression: '!has(task.transferMeta) AND !has(task.switchboardingHandled) AND !has(task.switchboardingTransferExempt)', targets: [ From 0f51fefa8613354f0330145ead075d1d9def6406 Mon Sep 17 00:00:00 2001 From: mythilytm Date: Mon, 12 May 2025 22:49:45 -0400 Subject: [PATCH 17/22] debug error switchboarding - filter --- functions/assignSwitchboarding.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index 8c63d206..27e59256 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -140,9 +140,9 @@ function addSwitchboardingFilter( // 3. Route them to the switchboard queue with proper attributes // 4. Exclude tasks that are transfers or already handled const switchboardingFilter = { - filter_friendly_name: 'Switchboarding Workflow', + filter_friendly_name: 'Switchboard Workflow', expression: - '!has(task.transferMeta) AND !has(task.switchboardingHandled) AND !has(task.switchboardingTransferExempt)', + 'task.transferMeta == null AND task.switchboardingHandled == null AND task.switchboardingTransferExempt == null', targets: [ { queue: switchboardQueueSid, @@ -160,9 +160,15 @@ function addSwitchboardingFilter( ], }; + // log the corrent filters + console.log('>>> Current filters:', updatedConfig.task_routing.filters); + // Insert the new filter at the beginning of the task_routing.filters array updatedConfig.task_routing.filters.unshift(switchboardingFilter); + // log the updated filters + console.log('>>> Updated filters:', updatedConfig.task_routing.filters); + return updatedConfig; } From dec916485a040626df10680ee404573e91146c81 Mon Sep 17 00:00:00 2001 From: mythilytm Date: Mon, 12 May 2025 23:34:43 -0400 Subject: [PATCH 18/22] transfers the tasks for either states --- functions/assignSwitchboarding.ts | 156 +++++++++++++++++++++++++++++- 1 file changed, 155 insertions(+), 1 deletion(-) diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index 27e59256..a29afd14 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -172,6 +172,109 @@ function addSwitchboardingFilter( return updatedConfig; } +/** + * Finds all tasks in a queue that are in a specific status + */ +async function findTasksInQueue( + client: any, + workspaceSid: string, + queueSid: string, + status: string = 'pending', +): Promise { + try { + console.log(`>>> Finding ${status} tasks in queue ${queueSid}`); + // Get all tasks with the specific status in the queue + const tasks = await client.taskrouter.workspaces(workspaceSid).tasks.list({ + assignmentStatus: status, + taskQueueSid: queueSid, + }); + + console.log(`>>> Found ${tasks.length} ${status} tasks in queue ${queueSid}`); + return tasks; + } catch (err) { + console.error(`>>> Error finding ${status} tasks in queue:`, err); + throw err; + } +} + +/** + * Moves a task from one queue to another + */ +async function moveTaskToQueue( + client: any, + workspaceSid: string, + taskSid: string, + targetQueueSid: string, + additionalAttributes: Record = {}, +): Promise { + try { + console.log(`>>> Moving task ${taskSid} to queue ${targetQueueSid}`); + + // First get the task to get its current attributes + const task = await client.taskrouter.workspaces(workspaceSid).tasks(taskSid).fetch(); + const currentAttributes = JSON.parse(task.attributes); + + // Merge in any additional attributes + const updatedAttributes = { + ...currentAttributes, + ...additionalAttributes, + taskQueueSid: targetQueueSid, + }; + + // Update the task with new attributes and move it to the new queue + await client.taskrouter + .workspaces(workspaceSid) + .tasks(taskSid) + .update({ + attributes: JSON.stringify(updatedAttributes), + taskQueueSid: targetQueueSid, + }); + + console.log(`>>> Successfully moved task ${taskSid} to queue ${targetQueueSid}`); + } catch (err) { + console.error('>>> Error moving task to queue:', err); + throw err; + } +} + +/** + * Moves waiting tasks from source queue to target queue + */ +async function moveWaitingTasks( + client: any, + workspaceSid: string, + sourceQueueSid: string, + targetQueueSid: string, + additionalAttributes: Record = {}, +): Promise { + try { + // Find all waiting tasks in the source queue + const waitingTasks = await findTasksInQueue(client, workspaceSid, sourceQueueSid, 'pending'); + + if (waitingTasks.length === 0) { + console.log(`>>> No waiting tasks found in queue ${sourceQueueSid} to move`); + return 0; + } + + console.log( + `>>> Moving ${waitingTasks.length} waiting tasks from ${sourceQueueSid} to ${targetQueueSid}`, + ); + + // Create an array of promises for all task moves (to process them in parallel) + const movePromises = waitingTasks.map((task) => + moveTaskToQueue(client, workspaceSid, task.sid, targetQueueSid, additionalAttributes), + ); + + // Wait for all move operations to complete in parallel + await Promise.all(movePromises); + + return waitingTasks.length; + } catch (err) { + console.error('>>> Error moving waiting tasks:', err); + throw err; + } +} + /** * Removes the switchboarding filter from the workflow configuration */ @@ -181,7 +284,7 @@ function removeSwitchboardingFilter(config: any): any { // Remove the switchboarding filter (identified by its friendly name) updatedConfig.task_routing.filters = updatedConfig.task_routing.filters.filter( - (filter: any) => filter.filter_friendly_name !== 'Switchboarding Active Filter', + (filter: any) => filter.filter_friendly_name !== 'Switchboard Workflow', ); return updatedConfig; @@ -324,6 +427,25 @@ export const handler = TokenValidator( configuration: JSON.stringify(updatedMasterConfig), }); + // Move any waiting tasks from the original queue to the switchboard queue + console.log('>>> 8b. TASKS: Moving waiting tasks from original queue to switchboard queue'); + try { + const movedCount = await moveWaitingTasks( + client, + context.TWILIO_WORKSPACE_SID, + originalQueueSid, + switchboardQueue.sid, + { + originalQueueSid, + needsSwitchboarding: true, + }, + ); + console.log(`>>> 8c. TASKS: Successfully moved ${movedCount} tasks to switchboard queue`); + } catch (moveErr) { + console.error('>>> 8d. TASKS ERROR: Failed to move waiting tasks:', moveErr); + // Continue with enabling switchboarding even if moving tasks fails + } + console.log('>>> 9. STATE UPDATE: Updating switchboarding state in Sync'); const updatedState = await updateSwitchboardState(client, syncServiceSid, { isSwitchboardingActive: true, @@ -365,6 +487,38 @@ export const handler = TokenValidator( configuration: JSON.stringify(updatedMasterConfig), }); + // Get the original queue SID from the current state + const originalQueueSid = switchboardingState.queueSid; + + // Move any waiting tasks from the switchboard queue back to the original queue + if (originalQueueSid) { + console.log( + '>>> 8b. TASKS: Moving waiting tasks from switchboard queue back to original queue', + ); + try { + const movedCount = await moveWaitingTasks( + client, + context.TWILIO_WORKSPACE_SID, + switchboardQueue.sid, + originalQueueSid, + { + needsSwitchboarding: false, + switchboardingHandled: true, + }, + ); + console.log( + `>>> 8c. TASKS: Successfully moved ${movedCount} tasks back to original queue`, + ); + } catch (moveErr) { + console.error('>>> 8d. TASKS ERROR: Failed to move waiting tasks:', moveErr); + // Continue with disabling switchboarding even if moving tasks fails + } + } else { + console.log( + '>>> 8b. TASKS: No original queue SID found in state, skipping task migration', + ); + } + console.log('>>> 9. STATE UPDATE: Updating switchboarding state in Sync'); const updatedState = await updateSwitchboardState(client, syncServiceSid, { isSwitchboardingActive: false, From 6e483e18358be4624e72565b8458ce3809c91bdc Mon Sep 17 00:00:00 2001 From: mythilytm Date: Mon, 12 May 2025 23:49:17 -0400 Subject: [PATCH 19/22] refactor switchboarding to allow task transfers --- functions/assignSwitchboarding.ts | 449 ++++++++++++++++-------------- 1 file changed, 234 insertions(+), 215 deletions(-) diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index a29afd14..0930571d 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -33,9 +33,11 @@ type EnvVars = { SYNC_SERVICE_SID: string; }; +export type OperationType = 'enable' | 'disable' | 'status'; + export type Body = { originalQueueSid?: string; - operation: 'enable' | 'disable' | 'status'; + operation: OperationType; request: { cookies: {}; headers: {} }; Token: string; }; @@ -179,20 +181,20 @@ async function findTasksInQueue( client: any, workspaceSid: string, queueSid: string, - status: string = 'pending', + assignmentStatus: string = 'pending', ): Promise { try { - console.log(`>>> Finding ${status} tasks in queue ${queueSid}`); + console.log(`>>> Finding ${assignmentStatus} tasks in queue ${queueSid}`); // Get all tasks with the specific status in the queue const tasks = await client.taskrouter.workspaces(workspaceSid).tasks.list({ - assignmentStatus: status, + assignmentStatus, taskQueueSid: queueSid, }); - console.log(`>>> Found ${tasks.length} ${status} tasks in queue ${queueSid}`); + console.log(`>>> Found ${tasks.length} ${assignmentStatus} tasks in queue ${queueSid}`); return tasks; } catch (err) { - console.error(`>>> Error finding ${status} tasks in queue:`, err); + console.error(`>>> Error finding ${assignmentStatus} tasks in queue:`, err); throw err; } } @@ -290,277 +292,294 @@ function removeSwitchboardingFilter(config: any): any { return updatedConfig; } +/** + * Handles the 'status' operation - returns current switchboarding state + */ +async function handleStatusOperation( + client: any, + syncServiceSid: string, + resolve: (response: any) => void, +) { + console.log('Getting current switchboarding status'); + const switchboardingState = await getSwitchboardState(client, syncServiceSid); + console.log(`Current state - isEnabled: ${switchboardingState.isSwitchboardingActive}`); + resolve(success(switchboardingState)); +} + +/** + * Handles the 'enable' operation - turns on switchboarding for a queue + */ +async function handleEnableOperation( + client: any, + syncServiceSid: string, + workspaceSid: string, + taskRouterClient: any, + originalQueue: any, + switchboardQueue: any, + masterWorkflow: any, + tokenResult: TokenValidatorResponse, + resolve: (response: any) => void, +) { + console.log('Enabling switchboarding mode'); + const switchboardingState = await getSwitchboardState(client, syncServiceSid); + + // Check if already enabled for this queue + if ( + switchboardingState.isSwitchboardingActive && + switchboardingState.queueSid === originalQueue.sid + ) { + console.log('Switchboarding is already enabled for this queue'); + resolve( + success({ + message: 'Switchboarding is already active for this queue', + state: switchboardingState, + }), + ); + return; + } + + // Update workflow configuration + console.log('Updating Master Workflow configuration'); + const masterConfig = JSON.parse(masterWorkflow.configuration); + const updatedMasterConfig = addSwitchboardingFilter( + masterConfig, + originalQueue.sid, + switchboardQueue.sid, + ); + + await taskRouterClient.workflows(masterWorkflow.sid).update({ + configuration: JSON.stringify(updatedMasterConfig), + }); + + // Move waiting tasks from original queue to switchboard queue + try { + console.log('Moving waiting tasks from original queue to switchboard queue'); + const movedCount = await moveWaitingTasks( + client, + workspaceSid, + originalQueue.sid, + switchboardQueue.sid, + { + originalQueueSid: originalQueue.sid, + needsSwitchboarding: true, + }, + ); + console.log(`Successfully moved ${movedCount} tasks to switchboard queue`); + } catch (moveErr) { + console.error('Failed to move waiting tasks:', moveErr); + // Continue with enabling switchboarding even if moving tasks fails + } + + // Update state in Sync + console.log('Updating switchboarding state'); + const updatedState = await updateSwitchboardState(client, syncServiceSid, { + isSwitchboardingActive: true, + queueSid: originalQueue.sid, + queueName: originalQueue.friendlyName, + supervisorWorkerSid: tokenResult.worker_sid, + startTime: new Date().toISOString(), + }); + + console.log('Switchboarding mode successfully enabled'); + resolve( + success({ + message: 'Switchboarding mode enabled', + state: updatedState, + }), + ); +} + +/** + * Handles the 'disable' operation - turns off switchboarding + */ +async function handleDisableOperation( + client: any, + syncServiceSid: string, + workspaceSid: string, + taskRouterClient: any, + switchboardQueue: any, + masterWorkflow: any, + resolve: (response: any) => void, +) { + console.log('Disabling switchboarding mode'); + const switchboardingState = await getSwitchboardState(client, syncServiceSid); + + // Check if already disabled + if (!switchboardingState.isSwitchboardingActive) { + console.log('Switchboarding is not currently enabled'); + resolve( + success({ + message: 'Switchboarding is not currently active', + state: switchboardingState, + }), + ); + return; + } + + // Update workflow configuration + console.log('Updating Master Workflow configuration'); + const masterConfig = JSON.parse(masterWorkflow.configuration); + const updatedMasterConfig = removeSwitchboardingFilter(masterConfig); + + await taskRouterClient.workflows(masterWorkflow.sid).update({ + configuration: JSON.stringify(updatedMasterConfig), + }); + + // Move waiting tasks back to original queue + const queueToRestoreTo = switchboardingState.queueSid; + if (queueToRestoreTo) { + try { + console.log('Moving waiting tasks from switchboard queue back to original queue'); + const movedCount = await moveWaitingTasks( + client, + workspaceSid, + switchboardQueue.sid, + queueToRestoreTo, + { + needsSwitchboarding: false, + switchboardingHandled: true, + }, + ); + console.log(`Successfully moved ${movedCount} tasks back to original queue`); + } catch (moveErr) { + console.error('Failed to move waiting tasks:', moveErr); + // Continue with disabling switchboarding even if moving tasks fails + } + } else { + console.log('No original queue SID found in state, skipping task migration'); + } + + // Update state in Sync + console.log('Updating switchboarding state'); + const updatedState = await updateSwitchboardState(client, syncServiceSid, { + isSwitchboardingActive: false, + queueSid: undefined, + queueName: undefined, + supervisorWorkerSid: undefined, + startTime: undefined, + }); + + console.log('Switchboarding mode successfully disabled'); + resolve( + success({ + message: 'Switchboarding mode disabled', + state: updatedState, + }), + ); +} + +/** + * Main handler function + */ export const handler = TokenValidator( async (context: Context, event: Body, callback: ServerlessCallback) => { - console.log('>>> 1. FUNCTION ENTRY: Starting switchboarding handler'); + console.log('Starting switchboarding handler'); const response = responseWithCors(); const resolve = bindResolve(callback)(response); - const accountSid = context.ACCOUNT_SID; - const authToken = context.AUTH_TOKEN; const { Token: token } = event; - if (!token) { - console.error('>>> 1b ERROR: Token is missing in the request'); + console.error('Token is missing in the request'); resolve(error400('token')); return; } try { - console.log('>>> 1a: Validating token'); + // Validate token and check for supervisor permissions const tokenResult: TokenValidatorResponse = await validator( token as string, - accountSid, - authToken, + context.ACCOUNT_SID, + context.AUTH_TOKEN, ); - const isSupervisorToken = isSupervisor(tokenResult); - console.log(`>>> 1a: Is Supervisor Token: ${isSupervisorToken}`); - - if (!isSupervisorToken) { - console.error('>>> 1c ERROR: Unauthorized access attempt by non-supervisor'); - resolve( - error403(`Unauthorized: endpoint not open to non supervisors. ${isSupervisorToken}`), - ); + if (!isSupervisor(tokenResult)) { + console.error('Unauthorized access attempt by non-supervisor'); + resolve(error403('Unauthorized: endpoint not open to non supervisors')); return; } const { originalQueueSid, operation } = event; - console.log(`>>> 2. event:, operation: ${operation}, originalQueueSid: ${originalQueueSid}`); + console.log(`Operation: ${operation}, OriginalQueueSid: ${originalQueueSid}`); + // Set up Twilio clients const client = context.getTwilioClient(); const syncServiceSid = context.SYNC_SERVICE_SID; - console.log( - `>>> 2a. STATE MANAGEMENT: Setting up Twilio clients with SyncServiceSid: ${syncServiceSid}`, - ); const taskRouterClient = client.taskrouter.workspaces(context.TWILIO_WORKSPACE_SID); - console.log('>>> 3. Fetching TaskRouter queues'); + // Get queues and find switchboard queue const queues = await taskRouterClient.taskQueues.list(); - const switchboardQueue = queues.find((queue) => queue.friendlyName === 'Switchboard Queue'); + if (!switchboardQueue) { - console.error('>>> 3b. QUEUES ERROR: Switchboard Queue not found'); + console.error('Switchboard Queue not found'); resolve(error400('Switchboard Queue not found')); return; } - console.log(`>>> 3a. Found Switchboard Queue with SID: ${switchboardQueue.sid}`); + // Handle status operation if (operation === 'status') { - console.log('>>> 4. STATUS: Retrieving current switchboarding status'); - const switchboardingState = await getSwitchboardState(client, syncServiceSid); - console.log( - `>>> 4a. STATUS: Current state - isEnabled: ${ - switchboardingState.isSwitchboardingActive === undefined - ? false - : switchboardingState.isSwitchboardingActive - }`, - ); - console.log('>>> 4b. STATUS: Full switchboard state:', JSON.stringify(switchboardingState)); - resolve(success(switchboardingState)); + await handleStatusOperation(client, syncServiceSid, resolve); return; } + // For enable/disable operations, originalQueueSid is required if (!originalQueueSid) { - console.error('>>> 5b ERROR: Original Queue SID is required for enable/disable operations'); + console.error('Original Queue SID is required for enable/disable operations'); resolve(error400('Original Queue SID is required')); return; } + // Find original queue const originalQueue = queues.find((queue) => queue.sid === originalQueueSid); if (!originalQueue) { - console.error('>>> 5c ERROR: Original Queue not found'); + console.error('Original Queue not found'); resolve(error400('Original Queue not found')); return; } - console.log( - `>>> 5a. Switchboard Queue: ${switchboardQueue.friendlyName}, SID: ${switchboardQueue.sid}`, - ); - console.log( - `>>> 5a. Original Queue: ${originalQueue.friendlyName}, SID: ${originalQueue.sid}`, - ); - console.log('>>> 6. WORKFLOWS: Fetching TaskRouter workflows'); + // Find Master Workflow const workflows = await taskRouterClient.workflows.list(); - const masterWorkflow = workflows.find( (workflow) => workflow.friendlyName === 'Master Workflow', ); if (!masterWorkflow) { - console.error('>>> 6b. WORKFLOWS ERROR: Master Workflow not found'); + console.error('Master Workflow not found'); resolve(error400('Master Workflow not found')); return; } - console.log(`>>> 6a. WORKFLOWS: Found Master Workflow with SID: ${masterWorkflow.sid}`); + // Handle enable/disable operations if (operation === 'enable') { - console.log('>>> 7. ENABLE: Enabling switchboarding mode'); - const switchboardingState = await getSwitchboardState(client, syncServiceSid); - if ( - switchboardingState.isSwitchboardingActive && - switchboardingState.queueSid === originalQueueSid - ) { - console.log('>>> 7b. ENABLE: Switchboarding is already enabled for this queue'); - resolve( - success({ - message: 'Switchboarding is already active for this queue', - state: switchboardingState, - }), - ); - return; - } - console.log('>>> 7a. ENABLE: Proceeding with switchboarding activation'); - - console.log('>>> 8. CONFIG UPDATE: Parsing and updating Master Workflow configuration'); - const masterConfig = JSON.parse(masterWorkflow.configuration); - - console.log('>>> 8a. CONFIG UPDATE: Adding switchboarding filter to workflow'); - const updatedMasterConfig = addSwitchboardingFilter( - masterConfig, - originalQueue.sid, - switchboardQueue.sid, - ); - - console.log('>>> 8a. CONFIG UPDATE: Applying updated configuration to Master Workflow'); - await taskRouterClient.workflows(masterWorkflow.sid).update({ - configuration: JSON.stringify(updatedMasterConfig), - }); - - // Move any waiting tasks from the original queue to the switchboard queue - console.log('>>> 8b. TASKS: Moving waiting tasks from original queue to switchboard queue'); - try { - const movedCount = await moveWaitingTasks( - client, - context.TWILIO_WORKSPACE_SID, - originalQueueSid, - switchboardQueue.sid, - { - originalQueueSid, - needsSwitchboarding: true, - }, - ); - console.log(`>>> 8c. TASKS: Successfully moved ${movedCount} tasks to switchboard queue`); - } catch (moveErr) { - console.error('>>> 8d. TASKS ERROR: Failed to move waiting tasks:', moveErr); - // Continue with enabling switchboarding even if moving tasks fails - } - - console.log('>>> 9. STATE UPDATE: Updating switchboarding state in Sync'); - const updatedState = await updateSwitchboardState(client, syncServiceSid, { - isSwitchboardingActive: true, - queueSid: originalQueueSid, - queueName: originalQueue.friendlyName, - supervisorWorkerSid: tokenResult.worker_sid, - startTime: new Date().toISOString(), - }); - - console.log('>>> 9a. STATE UPDATE: Switchboarding mode successfully enabled'); - resolve( - success({ - message: 'Switchboarding mode enabled', - state: updatedState, - }), + await handleEnableOperation( + client, + syncServiceSid, + context.TWILIO_WORKSPACE_SID, + taskRouterClient, + originalQueue, + switchboardQueue, + masterWorkflow, + tokenResult, + resolve, ); - } else if (operation === 'disable') { - console.log('>>> 7. DISABLE: Disabling switchboarding mode'); - const switchboardingState = await getSwitchboardState(client, syncServiceSid); - if (!switchboardingState.isSwitchboardingActive) { - console.log('>>> 7c. DISABLE: Switchboarding is not currently enabled'); - resolve( - success({ - message: 'Switchboarding is not currently active', - state: switchboardingState, - }), - ); - return; - } - console.log('>>> 7a. DISABLE: Proceeding with switchboarding deactivation'); - - console.log('>>> 8. CONFIG UPDATE: Parsing and updating Master Workflow configuration'); - const masterConfig = JSON.parse(masterWorkflow.configuration); - console.log('>>> 8a. CONFIG UPDATE: Removing switchboarding filter from workflow'); - const updatedMasterConfig = removeSwitchboardingFilter(masterConfig); - - console.log('>>> 8a. CONFIG UPDATE: Applying updated configuration to Master Workflow'); - await taskRouterClient.workflows(masterWorkflow.sid).update({ - configuration: JSON.stringify(updatedMasterConfig), - }); - - // Get the original queue SID from the current state - const originalQueueSid = switchboardingState.queueSid; - - // Move any waiting tasks from the switchboard queue back to the original queue - if (originalQueueSid) { - console.log( - '>>> 8b. TASKS: Moving waiting tasks from switchboard queue back to original queue', - ); - try { - const movedCount = await moveWaitingTasks( - client, - context.TWILIO_WORKSPACE_SID, - switchboardQueue.sid, - originalQueueSid, - { - needsSwitchboarding: false, - switchboardingHandled: true, - }, - ); - console.log( - `>>> 8c. TASKS: Successfully moved ${movedCount} tasks back to original queue`, - ); - } catch (moveErr) { - console.error('>>> 8d. TASKS ERROR: Failed to move waiting tasks:', moveErr); - // Continue with disabling switchboarding even if moving tasks fails - } - } else { - console.log( - '>>> 8b. TASKS: No original queue SID found in state, skipping task migration', - ); - } - - console.log('>>> 9. STATE UPDATE: Updating switchboarding state in Sync'); - const updatedState = await updateSwitchboardState(client, syncServiceSid, { - isSwitchboardingActive: false, - queueSid: undefined, - queueName: undefined, - supervisorWorkerSid: undefined, - startTime: undefined, - }); - - console.log('>>> 9a. STATE UPDATE: Switchboarding mode successfully disabled'); - resolve( - success({ - message: 'Switchboarding mode disabled', - state: updatedState, - }), + return; + } + if (operation === 'disable') { + await handleDisableOperation( + client, + syncServiceSid, + context.TWILIO_WORKSPACE_SID, + taskRouterClient, + switchboardQueue, + masterWorkflow, + resolve, ); + return; } } catch (err: any) { - console.error('>>> Error in switchboarding handler:', err); - - // Enhanced error logging with details - console.error('>>> Error details:', { - message: err.message, - code: err.code, - status: err.status, - stack: err.stack, - }); - - // More specific error responses based on error type - if (err.message && err.message.includes('workflow')) { - console.error('>>> Workflow configuration error:', err); - } else if (err.message && err.message.includes('sync')) { - console.error('>>> Sync document error:', err); - } else if ( - err.status === 403 || - (err.message && err.message.toLowerCase().includes('permission')) - ) { - console.error('>>> Permission error:', err); - } else { - console.error('>>> Generic error:', err); - } + console.error('Error in switchboarding handler:', err); + resolve(error500(err)); } - console.log('>>> Switchboarding handler completed'); + console.log('Switchboarding handler completed'); }, ); From 0ae4eadd0822ea9a7dc4265996c7a402b13e46db Mon Sep 17 00:00:00 2001 From: mythilytm Date: Tue, 13 May 2025 00:06:25 -0400 Subject: [PATCH 20/22] refactor switchboarding to allow task transfers --- functions/assignSwitchboarding.ts | 65 +++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index 0930571d..db5cb765 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -132,45 +132,34 @@ function addSwitchboardingFilter( originalQueueSid: string, switchboardQueueSid: string, ): any { - // Clone the configuration to avoid modifying the original const updatedConfig = JSON.parse(JSON.stringify(config)); - // Add a new filter at the top of the filter chain to redirect to switchboard - // This improved filter will: - // 1. Identify tasks that should go to the original queue - // 2. Mark them with attributes needed for switchboarding - // 3. Route them to the switchboard queue with proper attributes - // 4. Exclude tasks that are transfers or already handled + const filterName = `Switchboard Workflow - ${originalQueueSid}`; + console.log(`Adding switchboarding filter: ${filterName}`); + const switchboardingFilter = { - filter_friendly_name: 'Switchboard Workflow', + filter_friendly_name: filterName, expression: 'task.transferMeta == null AND task.switchboardingHandled == null AND task.switchboardingTransferExempt == null', targets: [ { queue: switchboardQueueSid, - expression: 'worker.available == true', // Only route to available workers - priority: 100, // High priority - // For tasks that would normally go to the original queue, redirect them + expression: 'worker.available == true', + priority: 100, target_expression: `DEFAULT_TARGET_QUEUE_SID == '${originalQueueSid}'`, - // Add task attributes to help the Switchboard Workflow process it correctly task_attributes: { originalQueueSid, needsSwitchboarding: true, taskQueueSid: switchboardQueueSid, + switchboardingActive: true, }, }, ], }; - // log the corrent filters - console.log('>>> Current filters:', updatedConfig.task_routing.filters); - // Insert the new filter at the beginning of the task_routing.filters array updatedConfig.task_routing.filters.unshift(switchboardingFilter); - // log the updated filters - console.log('>>> Updated filters:', updatedConfig.task_routing.filters); - return updatedConfig; } @@ -216,13 +205,35 @@ async function moveTaskToQueue( const task = await client.taskrouter.workspaces(workspaceSid).tasks(taskSid).fetch(); const currentAttributes = JSON.parse(task.attributes); - // Merge in any additional attributes + // Prepare switchboarding attributes based on whether moving to/from switchboard queue + const switchboardingAttributes = { + // If moving to the switchboard queue + ...(additionalAttributes.needsSwitchboarding + ? { + switchboardingActive: true, + switchboardingHandled: null, // Clear any previous handling flag + } + : {}), + // If moving from the switchboard queue back to original queue + ...(additionalAttributes.switchboardingHandled + ? { + switchboardingActive: false, + needsSwitchboarding: false, + switchboardingHandled: true, + } + : {}), + }; + + // Merge in all attributes const updatedAttributes = { ...currentAttributes, ...additionalAttributes, + ...switchboardingAttributes, taskQueueSid: targetQueueSid, }; + console.log(`>>> Task ${taskSid} attributes update:`, JSON.stringify(updatedAttributes)); + // Update the task with new attributes and move it to the new queue await client.taskrouter .workspaces(workspaceSid) @@ -284,9 +295,21 @@ function removeSwitchboardingFilter(config: any): any { // Clone the configuration to avoid modifying the original const updatedConfig = JSON.parse(JSON.stringify(config)); - // Remove the switchboarding filter (identified by its friendly name) + // Get the current filters for logging + console.log( + '>>> Current filters before removal:', + updatedConfig.task_routing.filters.map((filter: any) => filter.filter_friendly_name), + ); + + // Remove the switchboarding filters (identified by partial friendly name match) updatedConfig.task_routing.filters = updatedConfig.task_routing.filters.filter( - (filter: any) => filter.filter_friendly_name !== 'Switchboard Workflow', + (filter: any) => !filter.filter_friendly_name.startsWith('Switchboard Workflow'), + ); + + // Log the filters after removal + console.log( + '>>> Filters after removal:', + updatedConfig.task_routing.filters.map((filter: any) => filter.filter_friendly_name), ); return updatedConfig; From 22c565ecdd7f80a1c0a61fe153071d90ad89f314 Mon Sep 17 00:00:00 2001 From: mythilytm Date: Tue, 20 May 2025 16:50:53 -0400 Subject: [PATCH 21/22] clean up filter logic --- functions/assignSwitchboarding.ts | 60 +++++-------------------------- 1 file changed, 8 insertions(+), 52 deletions(-) diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index db5cb765..4b2f04e9 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -144,7 +144,7 @@ function addSwitchboardingFilter( targets: [ { queue: switchboardQueueSid, - expression: 'worker.available == true', + expression: '1==1', priority: 100, target_expression: `DEFAULT_TARGET_QUEUE_SID == '${originalQueueSid}'`, task_attributes: { @@ -157,7 +157,6 @@ function addSwitchboardingFilter( ], }; - // Insert the new filter at the beginning of the task_routing.filters array updatedConfig.task_routing.filters.unshift(switchboardingFilter); return updatedConfig; @@ -173,14 +172,11 @@ async function findTasksInQueue( assignmentStatus: string = 'pending', ): Promise { try { - console.log(`>>> Finding ${assignmentStatus} tasks in queue ${queueSid}`); - // Get all tasks with the specific status in the queue const tasks = await client.taskrouter.workspaces(workspaceSid).tasks.list({ assignmentStatus, taskQueueSid: queueSid, }); - console.log(`>>> Found ${tasks.length} ${assignmentStatus} tasks in queue ${queueSid}`); return tasks; } catch (err) { console.error(`>>> Error finding ${assignmentStatus} tasks in queue:`, err); @@ -189,7 +185,8 @@ async function findTasksInQueue( } /** - * Moves a task from one queue to another + * Moves a task from one queue to another + * Note that this is not working as intended */ async function moveTaskToQueue( client: any, @@ -199,19 +196,14 @@ async function moveTaskToQueue( additionalAttributes: Record = {}, ): Promise { try { - console.log(`>>> Moving task ${taskSid} to queue ${targetQueueSid}`); - - // First get the task to get its current attributes const task = await client.taskrouter.workspaces(workspaceSid).tasks(taskSid).fetch(); const currentAttributes = JSON.parse(task.attributes); - - // Prepare switchboarding attributes based on whether moving to/from switchboard queue const switchboardingAttributes = { // If moving to the switchboard queue ...(additionalAttributes.needsSwitchboarding ? { switchboardingActive: true, - switchboardingHandled: null, // Clear any previous handling flag + switchboardingHandled: null, } : {}), // If moving from the switchboard queue back to original queue @@ -232,8 +224,6 @@ async function moveTaskToQueue( taskQueueSid: targetQueueSid, }; - console.log(`>>> Task ${taskSid} attributes update:`, JSON.stringify(updatedAttributes)); - // Update the task with new attributes and move it to the new queue await client.taskrouter .workspaces(workspaceSid) @@ -243,9 +233,8 @@ async function moveTaskToQueue( taskQueueSid: targetQueueSid, }); - console.log(`>>> Successfully moved task ${taskSid} to queue ${targetQueueSid}`); } catch (err) { - console.error('>>> Error moving task to queue:', err); + console.error('Error moving task to queue:', err); throw err; } } @@ -265,25 +254,18 @@ async function moveWaitingTasks( const waitingTasks = await findTasksInQueue(client, workspaceSid, sourceQueueSid, 'pending'); if (waitingTasks.length === 0) { - console.log(`>>> No waiting tasks found in queue ${sourceQueueSid} to move`); + console.log(`No waiting tasks found in queue ${sourceQueueSid} to move`); return 0; } - console.log( - `>>> Moving ${waitingTasks.length} waiting tasks from ${sourceQueueSid} to ${targetQueueSid}`, - ); - - // Create an array of promises for all task moves (to process them in parallel) const movePromises = waitingTasks.map((task) => moveTaskToQueue(client, workspaceSid, task.sid, targetQueueSid, additionalAttributes), ); - - // Wait for all move operations to complete in parallel await Promise.all(movePromises); return waitingTasks.length; } catch (err) { - console.error('>>> Error moving waiting tasks:', err); + console.error('Error moving waiting tasks:', err); throw err; } } @@ -292,26 +274,12 @@ async function moveWaitingTasks( * Removes the switchboarding filter from the workflow configuration */ function removeSwitchboardingFilter(config: any): any { - // Clone the configuration to avoid modifying the original const updatedConfig = JSON.parse(JSON.stringify(config)); - // Get the current filters for logging - console.log( - '>>> Current filters before removal:', - updatedConfig.task_routing.filters.map((filter: any) => filter.filter_friendly_name), - ); - - // Remove the switchboarding filters (identified by partial friendly name match) updatedConfig.task_routing.filters = updatedConfig.task_routing.filters.filter( (filter: any) => !filter.filter_friendly_name.startsWith('Switchboard Workflow'), ); - // Log the filters after removal - console.log( - '>>> Filters after removal:', - updatedConfig.task_routing.filters.map((filter: any) => filter.filter_friendly_name), - ); - return updatedConfig; } @@ -343,15 +311,12 @@ async function handleEnableOperation( tokenResult: TokenValidatorResponse, resolve: (response: any) => void, ) { - console.log('Enabling switchboarding mode'); const switchboardingState = await getSwitchboardState(client, syncServiceSid); - // Check if already enabled for this queue if ( switchboardingState.isSwitchboardingActive && switchboardingState.queueSid === originalQueue.sid ) { - console.log('Switchboarding is already enabled for this queue'); resolve( success({ message: 'Switchboarding is already active for this queue', @@ -361,8 +326,6 @@ async function handleEnableOperation( return; } - // Update workflow configuration - console.log('Updating Master Workflow configuration'); const masterConfig = JSON.parse(masterWorkflow.configuration); const updatedMasterConfig = addSwitchboardingFilter( masterConfig, @@ -390,11 +353,9 @@ async function handleEnableOperation( console.log(`Successfully moved ${movedCount} tasks to switchboard queue`); } catch (moveErr) { console.error('Failed to move waiting tasks:', moveErr); - // Continue with enabling switchboarding even if moving tasks fails } - // Update state in Sync - console.log('Updating switchboarding state'); + // Update switchboard state in Twilio Sync const updatedState = await updateSwitchboardState(client, syncServiceSid, { isSwitchboardingActive: true, queueSid: originalQueue.sid, @@ -403,7 +364,6 @@ async function handleEnableOperation( startTime: new Date().toISOString(), }); - console.log('Switchboarding mode successfully enabled'); resolve( success({ message: 'Switchboarding mode enabled', @@ -473,7 +433,6 @@ async function handleDisableOperation( } // Update state in Sync - console.log('Updating switchboarding state'); const updatedState = await updateSwitchboardState(client, syncServiceSid, { isSwitchboardingActive: false, queueSid: undefined, @@ -482,7 +441,6 @@ async function handleDisableOperation( startTime: undefined, }); - console.log('Switchboarding mode successfully disabled'); resolve( success({ message: 'Switchboarding mode disabled', @@ -539,13 +497,11 @@ export const handler = TokenValidator( return; } - // Handle status operation if (operation === 'status') { await handleStatusOperation(client, syncServiceSid, resolve); return; } - // For enable/disable operations, originalQueueSid is required if (!originalQueueSid) { console.error('Original Queue SID is required for enable/disable operations'); resolve(error400('Original Queue SID is required')); From 161c27a134f2a452db4369536bdc59a44b14884c Mon Sep 17 00:00:00 2001 From: mythilytm Date: Tue, 20 May 2025 16:50:58 -0400 Subject: [PATCH 22/22] clean up filter logic --- functions/assignSwitchboarding.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/functions/assignSwitchboarding.ts b/functions/assignSwitchboarding.ts index 4b2f04e9..73235486 100644 --- a/functions/assignSwitchboarding.ts +++ b/functions/assignSwitchboarding.ts @@ -185,7 +185,7 @@ async function findTasksInQueue( } /** - * Moves a task from one queue to another + * Moves a task from one queue to another * Note that this is not working as intended */ async function moveTaskToQueue( @@ -232,7 +232,6 @@ async function moveTaskToQueue( attributes: JSON.stringify(updatedAttributes), taskQueueSid: targetQueueSid, }); - } catch (err) { console.error('Error moving task to queue:', err); throw err;