diff --git a/functions/interaction/interactionChannelParticipants.private.ts b/functions/interaction/interactionChannelParticipants.private.ts new file mode 100644 index 00000000..34a9964d --- /dev/null +++ b/functions/interaction/interactionChannelParticipants.private.ts @@ -0,0 +1,82 @@ +import { Context } from '@twilio-labs/serverless-runtime-types/types'; +import { + InteractionChannelParticipantInstance, + InteractionChannelParticipantListInstance, + InteractionChannelParticipantStatus, +} from 'twilio/lib/rest/flexApi/v1/interaction/interactionChannel/interactionChannelParticipant'; + +const transitionAgentParticipants = async ( + client: ReturnType['getTwilioClient']>, + twilioWorkspaceSid: string, + taskSid: string, + targetStatus: InteractionChannelParticipantStatus, + interactionChannelParticipantSid?: string, +): Promise => { + console.log('==== transitionAgentParticipants ===='); + + const task = await client.taskrouter.workspaces + .get(twilioWorkspaceSid) + .tasks.get(taskSid) + .fetch(); + const { flexInteractionSid, flexInteractionChannelSid } = JSON.parse(task.attributes); + + if (!flexInteractionSid || !flexInteractionChannelSid) { + console.warn( + "transitionAgentParticipants called with a task without a flexInteractionSid or flexInteractionChannelSid set in it's attributes - is it being called with a Programmable Chat task?", + task.attributes, + ); + return { + errorType: 'Validation', + errorMessage: + "ValidationError: Task specified must have a flexInteractionSid and flexInteractionChannelSid set in it's attributes", + }; + } + const interactionParticipantListInstance: InteractionChannelParticipantListInstance = + client.flexApi.v1.interaction + .get(flexInteractionSid) + .channels.get(flexInteractionChannelSid).participants; + const interactionAgentParticipants = (await interactionParticipantListInstance.list()).filter( + (p) => + p.type === 'agent' && + (p.sid === interactionChannelParticipantSid || !interactionChannelParticipantSid), + ); + + if (interactionAgentParticipants.length === 0) { + return []; + } + + const results = await Promise.allSettled( + interactionAgentParticipants.map((p) => { + console.log(`Transitioning agent participant ${p.sid} to ${targetStatus}`); + Object.entries(p).forEach(([k, v]) => { + try { + console.log(`${k}: ${typeof v === 'object' ? JSON.stringify(v) : v}`); + } catch (e) { + console.log(`${k}: ${v}`); + } + }); + console.log('routing_properties', JSON.stringify((p as any).routing_properties)); + console.log('routingProperties', JSON.stringify((p as any).routingProperties)); + + return p.update({ status: targetStatus }); + }), + ); + const failures: PromiseRejectedResult[] = results.filter( + (r) => r.status === 'rejected', + ) as PromiseRejectedResult[]; + failures.forEach((f) => console.warn(f.reason)); + // This is a bit of a hack. Conversations which have been transferred between agents should have all the previous agents removed as participants of the interaction + // However if they haven't for any reason, they are in a state where their status cannot be transitioned, presumably because they are no longer active in the conversation. + // I can't see a good way to detect these in the API, so we assume if any of the agents are successfully transitioned, the 'current' agent has been transitioned and the operation can be considered successful. + // There are probably edge cases where this assumption isn't valid, but this is itself working around an edge case so we would be into edge cases of edge cases there. + if (failures.length < interactionAgentParticipants.length) { + return results + .filter((r) => r.status === 'fulfilled') + .map((r) => (r as PromiseFulfilledResult).value.sid); + } + return { errorType: 'Exception', errorMessage: failures[0].reason }; +}; + +export type InteractionChannelParticipants = { + transitionAgentParticipants: typeof transitionAgentParticipants; +}; diff --git a/functions/interaction/transitionAgentParticipants.ts b/functions/interaction/transitionAgentParticipants.ts index 6a81bf6b..e0243e2a 100644 --- a/functions/interaction/transitionAgentParticipants.ts +++ b/functions/interaction/transitionAgentParticipants.ts @@ -23,10 +23,8 @@ import { responseWithCors, success, } from '@tech-matters/serverless-helpers'; -import { - InteractionChannelParticipantInstance, - InteractionChannelParticipantStatus, -} from 'twilio/lib/rest/flexApi/v1/interaction/interactionChannel/interactionChannelParticipant'; +import { InteractionChannelParticipantStatus } from 'twilio/lib/rest/flexApi/v1/interaction/interactionChannel/interactionChannelParticipant'; +import { InteractionChannelParticipants } from './interactionChannelParticipants.private'; type EnvVars = { TWILIO_WORKSPACE_SID: string; @@ -39,79 +37,6 @@ type Body = { request: { cookies: {}; headers: {} }; }; -const transitionAgentParticipants = async ( - client: ReturnType['getTwilioClient']>, - twilioWorkspaceSid: string, - taskSid: string, - targetStatus: InteractionChannelParticipantStatus, - interactionChannelParticipantSid?: string, -): Promise => { - console.log('==== transitionAgentParticipants ===='); - - const task = await client.taskrouter.workspaces - .get(twilioWorkspaceSid) - .tasks.get(taskSid) - .fetch(); - const { flexInteractionSid, flexInteractionChannelSid } = JSON.parse(task.attributes); - - if (!flexInteractionSid || !flexInteractionChannelSid) { - console.warn( - "transitionAgentParticipants called with a task without a flexInteractionSid or flexInteractionChannelSid set in it's attributes - is it being called with a Programmable Chat task?", - task.attributes, - ); - return { - errorType: 'Validation', - errorMessage: - "ValidationError: Task specified must have a flexInteractionSid and flexInteractionChannelSid set in it's attributes", - }; - } - const interactionParticipantContext = client.flexApi.v1.interaction - .get(flexInteractionSid) - .channels.get(flexInteractionChannelSid).participants; - const interactionAgentParticipants = (await interactionParticipantContext.list()).filter( - (p) => - p.type === 'agent' && - (p.sid === interactionChannelParticipantSid || !interactionChannelParticipantSid), - ); - - if (interactionAgentParticipants.length === 0) { - return []; - } - - const results = await Promise.allSettled( - interactionAgentParticipants.map((p) => { - console.log(`Transitioning agent participant ${p.sid} to ${targetStatus}`); - Object.entries(p).forEach(([k, v]) => { - try { - console.log(`${k}: ${typeof v === 'object' ? JSON.stringify(v) : v}`); - } catch (e) { - console.log(`${k}: ${v}`); - } - }); - console.log('routing_properties', JSON.stringify((p as any).routing_properties)); - console.log('routingProperties', JSON.stringify((p as any).routingProperties)); - - return p.update({ status: targetStatus }); - }), - ); - const failures: PromiseRejectedResult[] = results.filter( - (r) => r.status === 'rejected', - ) as PromiseRejectedResult[]; - failures.forEach((f) => console.warn(f.reason)); - // This is a bit of a hack. Conversations which have been transferred between agents should have all the previous agents removed as participants of the interaction - // However if they haven't for any reason, they are in a state where their status cannot be transitioned, presumably because they are no longer active in the conversation. - // I can't see a good way to detect these in the API, so we assume if any of the agents are successfully transitioned, the 'current' agent has been transitioned and the operation can be considered successful. - // There are probably edge cases where this assumption isn't valid, but this is itself working around an edge case so we would be into edge cases of edge cases there. - if (failures.length < interactionAgentParticipants.length) { - return results - .filter((r) => r.status === 'fulfilled') - .map((r) => (r as PromiseFulfilledResult).value.sid); - } - return { errorType: 'Exception', errorMessage: failures[0].reason }; -}; - -export default transitionAgentParticipants; - /** * This function looks up a Flex interaction & interaction channel using the attributes of the provided Task. * It will then transition any participants in the interaction channel of type 'agent' to the pspecified state. @@ -123,6 +48,10 @@ export const handler = TokenValidator( console.log('==== transitionAgentParticipants ===='); const response = responseWithCors(); const resolve = bindResolve(callback)(response); + + const { path } = Runtime.getFunctions()['interaction/interactionChannelParticipants']; + // eslint-disable-next-line prefer-destructuring,global-require,import/no-dynamic-require + const { transitionAgentParticipants }: InteractionChannelParticipants = require(path); try { const result = await transitionAgentParticipants( context.getTwilioClient(), @@ -149,5 +78,3 @@ export const handler = TokenValidator( } }, ); - -export type TransitionAgentParticipants = typeof transitionAgentParticipants; diff --git a/functions/taskrouterListeners/transfersListener.private.ts b/functions/taskrouterListeners/transfersListener.private.ts index 336a2dd5..76266814 100644 --- a/functions/taskrouterListeners/transfersListener.private.ts +++ b/functions/taskrouterListeners/transfersListener.private.ts @@ -29,7 +29,7 @@ import { TASK_QUEUE_ENTERED, } from '@tech-matters/serverless-helpers/taskrouter'; import type { TransferMeta, ChatTransferTaskAttributes } from '../transfer/helpers.private'; -import { TransitionAgentParticipants } from '../interaction/transitionAgentParticipants'; +import { InteractionChannelParticipants } from '../interaction/interactionChannelParticipants.private'; export const eventTypes: EventType[] = [ RESERVATION_ACCEPTED, @@ -193,9 +193,9 @@ export const handleEvent = async (context: Context, event: EventFields) * If conversation, remove original participant from conversation. */ - const { path } = Runtime.getFunctions()['interaction/transitionAgentParticipants']; + const { path } = Runtime.getFunctions()['interaction/interactionChannelParticipants']; // eslint-disable-next-line prefer-destructuring,global-require,import/no-dynamic-require - const transitionAgentParticipants: TransitionAgentParticipants = require(path); + const { transitionAgentParticipants }: InteractionChannelParticipants = require(path); await transitionAgentParticipants( context.getTwilioClient(), context.TWILIO_WORKSPACE_SID, @@ -231,9 +231,9 @@ export const handleEvent = async (context: Context, event: EventFields) * If conversation, remove original participant from conversation. */ try { - const { path } = Runtime.getFunctions()['interaction/transitionAgentParticipants']; + const { path } = Runtime.getFunctions()['interaction/interactionChannelParticipants']; // eslint-disable-next-line prefer-destructuring,global-require,import/no-dynamic-require - const transitionAgentParticipants: TransitionAgentParticipants = require(path); + const { transitionAgentParticipants }: InteractionChannelParticipants = require(path); await transitionAgentParticipants( context.getTwilioClient(), context.TWILIO_WORKSPACE_SID,