Skip to content

Commit

Permalink
Fix function exporting
Browse files Browse the repository at this point in the history
  • Loading branch information
stephenhand committed Jul 30, 2024
1 parent 15761d6 commit 3d8d516
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 84 deletions.
82 changes: 82 additions & 0 deletions functions/interaction/interactionChannelParticipants.private.ts
Original file line number Diff line number Diff line change
@@ -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<Context<any>['getTwilioClient']>,
twilioWorkspaceSid: string,
taskSid: string,
targetStatus: InteractionChannelParticipantStatus,
interactionChannelParticipantSid?: string,
): Promise<string[] | { errorType: 'Validation' | 'Exception'; errorMessage: string }> => {
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<InteractionChannelParticipantInstance>).value.sid);
}
return { errorType: 'Exception', errorMessage: failures[0].reason };
};

export type InteractionChannelParticipants = {
transitionAgentParticipants: typeof transitionAgentParticipants;
};
85 changes: 6 additions & 79 deletions functions/interaction/transitionAgentParticipants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -39,79 +37,6 @@ type Body = {
request: { cookies: {}; headers: {} };
};

const transitionAgentParticipants = async (
client: ReturnType<Context<EnvVars>['getTwilioClient']>,
twilioWorkspaceSid: string,
taskSid: string,
targetStatus: InteractionChannelParticipantStatus,
interactionChannelParticipantSid?: string,
): Promise<string[] | { errorType: 'Validation' | 'Exception'; errorMessage: string }> => {
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<InteractionChannelParticipantInstance>).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.
Expand All @@ -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(),
Expand All @@ -149,5 +78,3 @@ export const handler = TokenValidator(
}
},
);

export type TransitionAgentParticipants = typeof transitionAgentParticipants;
10 changes: 5 additions & 5 deletions functions/taskrouterListeners/transfersListener.private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -193,9 +193,9 @@ export const handleEvent = async (context: Context<EnvVars>, 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,
Expand Down Expand Up @@ -231,9 +231,9 @@ export const handleEvent = async (context: Context<EnvVars>, 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,
Expand Down

0 comments on commit 3d8d516

Please sign in to comment.