-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add transfer task functionality (#10)
* Added transferChatStart * Added transferChatResolve
- Loading branch information
Showing
5 changed files
with
879 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
import '@twilio-labs/serverless-runtime-types'; | ||
import { | ||
Context, | ||
ServerlessCallback, | ||
ServerlessFunctionSignature, | ||
} from '@twilio-labs/serverless-runtime-types/types'; | ||
import { | ||
responseWithCors, | ||
bindResolve, | ||
error400, | ||
error500, | ||
success, | ||
} from 'tech-matters-serverless-helpers'; | ||
|
||
const TokenValidator = require('twilio-flex-token-validator').functionValidator; | ||
|
||
type EnvVars = { | ||
TWILIO_WORKSPACE_SID: string; | ||
TWILIO_CHAT_TRANSFER_WORKFLOW_SID: string; | ||
CHAT_SERVICE_SID: string; | ||
}; | ||
|
||
export type Body = { | ||
closeSid?: string; | ||
keepSid?: string; | ||
memberToKick?: string; | ||
newStatus?: string; | ||
}; | ||
|
||
async function closeTask( | ||
context: Context<EnvVars>, | ||
sid: string, | ||
taskToCloseAttributes: any, | ||
newStatus: string, | ||
) { | ||
// set the channelSid and ProxySessionSID to a dummy value. This keeps the session alive | ||
const newTaskToCloseAttributes = { | ||
...taskToCloseAttributes, | ||
channelSid: 'CH00000000000000000000000000000000', | ||
proxySessionSID: 'KC00000000000000000000000000000000', | ||
}; | ||
|
||
const client = context.getTwilioClient(); | ||
|
||
await client.taskrouter | ||
.workspaces(context.TWILIO_WORKSPACE_SID) | ||
.tasks(sid) | ||
.update({ attributes: JSON.stringify(newTaskToCloseAttributes) }); | ||
|
||
// close the Task and set the proper status | ||
const closedTask = await client.taskrouter | ||
.workspaces(context.TWILIO_WORKSPACE_SID) | ||
.tasks(sid) | ||
.update({ | ||
assignmentStatus: 'completed', | ||
reason: 'task transferred', | ||
attributes: JSON.stringify({ | ||
...newTaskToCloseAttributes, | ||
transferMeta: { | ||
...newTaskToCloseAttributes.transferMeta, | ||
transferStatus: newStatus, | ||
}, | ||
}), | ||
}); | ||
|
||
return closedTask; | ||
} | ||
|
||
async function kickMember(context: Context<EnvVars>, memberToKick: string, chatChannel: string) { | ||
const client = context.getTwilioClient(); | ||
|
||
// kick out the counselor that is not required anymore | ||
if (memberToKick) { | ||
const memberKicked = await client.chat | ||
.services(context.CHAT_SERVICE_SID) | ||
.channels(chatChannel) | ||
.members(memberToKick) | ||
.remove(); | ||
|
||
return memberKicked; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
async function closeTaskAndKick(context: Context<EnvVars>, body: Required<Body>) { | ||
const client = context.getTwilioClient(); | ||
|
||
// retrieve attributes of the task to close | ||
const taskToClose = await client.taskrouter | ||
.workspaces(context.TWILIO_WORKSPACE_SID) | ||
.tasks(body.closeSid) | ||
.fetch(); | ||
const taskToCloseAttributes = JSON.parse(taskToClose.attributes); | ||
const { channelSid } = taskToCloseAttributes; | ||
|
||
const [closedTask] = await Promise.all([ | ||
closeTask(context, body.closeSid, taskToCloseAttributes, body.newStatus), | ||
kickMember(context, body.memberToKick, channelSid), | ||
]); | ||
|
||
return closedTask; | ||
} | ||
|
||
async function updateTaskToKeep(context: Context<EnvVars>, body: Required<Body>) { | ||
const client = context.getTwilioClient(); | ||
|
||
// retrieve attributes of the preserved task | ||
const taskToKeep = await client.taskrouter | ||
.workspaces(context.TWILIO_WORKSPACE_SID) | ||
.tasks(body.keepSid) | ||
.fetch(); | ||
|
||
const taskToKeepAttributes = JSON.parse(taskToKeep.attributes); | ||
|
||
// update the status of the task that is preserved | ||
const newTaskToKeepAttributes = { | ||
...taskToKeepAttributes, | ||
transferMeta: { | ||
...taskToKeepAttributes.transferMeta, | ||
transferStatus: body.newStatus, | ||
}, | ||
}; | ||
|
||
const keptTask = await client.taskrouter | ||
.workspaces(context.TWILIO_WORKSPACE_SID) | ||
.tasks(body.keepSid) | ||
.update({ attributes: JSON.stringify(newTaskToKeepAttributes) }); | ||
|
||
return keptTask; | ||
} | ||
|
||
export const handler: ServerlessFunctionSignature = TokenValidator( | ||
async (context: Context<EnvVars>, event: Body, callback: ServerlessCallback) => { | ||
const response = responseWithCors(); | ||
const resolve = bindResolve(callback)(response); | ||
|
||
const { closeSid, keepSid, memberToKick, newStatus } = event; | ||
|
||
try { | ||
if (closeSid === undefined) { | ||
resolve(error400('closeSid')); | ||
return; | ||
} | ||
if (keepSid === undefined) { | ||
resolve(error400('keepSid')); | ||
return; | ||
} | ||
if (memberToKick === undefined) { | ||
resolve(error400('memberToKick')); | ||
return; | ||
} | ||
if (newStatus === undefined) { | ||
resolve(error400('newStatus')); | ||
return; | ||
} | ||
|
||
const validBody = { closeSid, keepSid, memberToKick, newStatus }; | ||
|
||
const [closedTask, keptTask] = await Promise.all([ | ||
closeTaskAndKick(context, validBody), | ||
updateTaskToKeep(context, validBody), | ||
]); | ||
|
||
resolve(success({ closed: closedTask.sid, kept: keptTask.sid })); | ||
} catch (err) { | ||
resolve(error500(err)); | ||
} | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import '@twilio-labs/serverless-runtime-types'; | ||
import { | ||
Context, | ||
ServerlessCallback, | ||
ServerlessFunctionSignature, | ||
} from '@twilio-labs/serverless-runtime-types/types'; | ||
import { | ||
responseWithCors, | ||
bindResolve, | ||
error400, | ||
error500, | ||
success, | ||
} from 'tech-matters-serverless-helpers'; | ||
|
||
const TokenValidator = require('twilio-flex-token-validator').functionValidator; | ||
|
||
type EnvVars = { | ||
TWILIO_WORKSPACE_SID: string; | ||
TWILIO_CHAT_TRANSFER_WORKFLOW_SID: string; | ||
CHAT_SERVICE_SID: string; | ||
}; | ||
|
||
export type Body = { | ||
taskSid?: string; | ||
targetSid?: string; | ||
workerName?: string; | ||
mode?: string; | ||
memberToKick?: string; | ||
}; | ||
|
||
async function closeTask(context: Context<EnvVars>, sid: string, taskToCloseAttributes: any) { | ||
// set the channelSid and ProxySessionSID to a dummy value. This keeps the session alive | ||
const newTaskToCloseAttributes = { | ||
...taskToCloseAttributes, | ||
channelSid: 'CH00000000000000000000000000000000', | ||
proxySessionSID: 'KC00000000000000000000000000000000', | ||
}; | ||
|
||
const client = context.getTwilioClient(); | ||
|
||
await client.taskrouter | ||
.workspaces(context.TWILIO_WORKSPACE_SID) | ||
.tasks(sid) | ||
.update({ attributes: JSON.stringify(newTaskToCloseAttributes) }); | ||
|
||
// close the Task and set the proper status | ||
const closedTask = await client.taskrouter | ||
.workspaces(context.TWILIO_WORKSPACE_SID) | ||
.tasks(sid) | ||
.update({ | ||
assignmentStatus: 'completed', | ||
reason: 'task transferred', | ||
attributes: JSON.stringify(newTaskToCloseAttributes), | ||
}); | ||
|
||
return closedTask; | ||
} | ||
|
||
async function kickMember(context: Context<EnvVars>, memberToKick: string, chatChannel: string) { | ||
const client = context.getTwilioClient(); | ||
|
||
// kick out the counselor that is not required anymore | ||
if (memberToKick) { | ||
const memberKicked = await client.chat | ||
.services(context.CHAT_SERVICE_SID) | ||
.channels(chatChannel) | ||
.members(memberToKick) | ||
.remove(); | ||
|
||
return memberKicked; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
async function closeTaskAndKick(context: Context<EnvVars>, body: Required<Body>) { | ||
const client = context.getTwilioClient(); | ||
|
||
// retrieve attributes of the task to close | ||
const taskToClose = await client.taskrouter | ||
.workspaces(context.TWILIO_WORKSPACE_SID) | ||
.tasks(body.taskSid) | ||
.fetch(); | ||
const taskToCloseAttributes = JSON.parse(taskToClose.attributes); | ||
const { channelSid } = taskToCloseAttributes; | ||
|
||
const [closedTask] = await Promise.all([ | ||
closeTask(context, body.taskSid, taskToCloseAttributes), | ||
kickMember(context, body.memberToKick, channelSid), | ||
]); | ||
|
||
return closedTask; | ||
} | ||
|
||
export const handler: ServerlessFunctionSignature = TokenValidator( | ||
async (context: Context<EnvVars>, event: Body, callback: ServerlessCallback) => { | ||
const client = context.getTwilioClient(); | ||
|
||
const response = responseWithCors(); | ||
const resolve = bindResolve(callback)(response); | ||
|
||
const { taskSid, targetSid, workerName, mode, memberToKick } = event; | ||
|
||
try { | ||
if (taskSid === undefined) { | ||
resolve(error400('taskSid')); | ||
return; | ||
} | ||
if (targetSid === undefined) { | ||
resolve(error400('targetSid')); | ||
return; | ||
} | ||
if (workerName === undefined) { | ||
resolve(error400('workerName')); | ||
return; | ||
} | ||
if (mode === undefined) { | ||
resolve(error400('mode')); | ||
return; | ||
} | ||
if (memberToKick === undefined) { | ||
resolve(error400('memberToKick')); | ||
return; | ||
} | ||
|
||
// retrieve attributes of the original task | ||
const originalTask = await client.taskrouter | ||
.workspaces(context.TWILIO_WORKSPACE_SID) | ||
.tasks(taskSid) | ||
.fetch(); | ||
|
||
const originalAttributes = JSON.parse(originalTask.attributes); | ||
|
||
const newAttributes = { | ||
...originalAttributes, | ||
conversations: originalAttributes.conversation // set up attributes of the new task to link them to the original task in Flex Insights | ||
? originalAttributes.conversation | ||
: { conversation_id: taskSid }, | ||
ignoreAgent: workerName, // update task attributes to ignore the agent who transferred the task | ||
targetSid, // update task attributes to include the required targetSid on the task (workerSid or a queueSid) | ||
transferTargetType: targetSid.startsWith('WK') ? 'worker' : 'queue', | ||
}; | ||
|
||
// create New task | ||
const newTask = await client.taskrouter | ||
.workspaces(context.TWILIO_WORKSPACE_SID) | ||
.tasks.create({ | ||
workflowSid: context.TWILIO_CHAT_TRANSFER_WORKFLOW_SID, | ||
taskChannel: originalTask.taskChannelUniqueName, | ||
attributes: JSON.stringify(newAttributes), | ||
}); | ||
|
||
if (mode === 'COLD') { | ||
const validBody = { taskSid, targetSid, workerName, mode, memberToKick }; | ||
await closeTaskAndKick(context, validBody); | ||
} | ||
|
||
resolve(success({ taskSid: newTask.sid })); | ||
} catch (err) { | ||
resolve(error500(err)); | ||
} | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -113,4 +113,5 @@ export default { | |
MockRuntime, | ||
backupEnv, | ||
restoreEnv | ||
}; | ||
}; | ||
|
Oops, something went wrong.