Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* 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 {
registerTaskRouterEventHandler,
TaskRouterEventHandler,
} from '../taskrouter/taskrouterEventHandler';
import { AccountSID, WorkerSID } from '@tech-matters/twilio-types';
import { Twilio } from 'twilio';
import { RESERVATION_ACCEPTED, RESERVATION_REJECTED } from '../taskrouter/eventTypes';
import { EventFields } from '../taskrouter';
import { retrieveFeatureFlags } from '../configuration/aseloConfiguration';
import { adjustChatCapacity } from './adjustChatCapacity';

const adjustCapacityHandler: TaskRouterEventHandler = async (
event: EventFields,
accountSid: AccountSID,
client: Twilio,
) => {
const featureFlags = await retrieveFeatureFlags(client);

if (!featureFlags.use_twilio_lambda_adjust_capacity) {
console.debug(
`AdjustCapacityTaskRouterListener skipped for account ${accountSid} - use_twilio_lambda_adjust_capacity flag not enabled`,
);
return;
}

const {
WorkerSid: workerSid,
TaskSid: taskSid,
TaskChannelUniqueName: taskChannelUniqueName,
EventType: eventType,
} = event;

if (taskChannelUniqueName !== 'chat') return;

if (!featureFlags.enable_manual_pulling) {
console.debug(
`AdjustCapacityListener skipped for account ${accountSid}, task ${taskSid} - enable_manual_pulling flag not enabled`,
);
return;
}

const { status, message } = await adjustChatCapacity(accountSid, {
workerSid: workerSid as WorkerSID,
adjustment: 'setTo1',
});
console.info(
`AdjustCapacityListener completed for account ${accountSid}, task ${taskSid}, worker ${workerSid}, event ${eventType}: status ${status}, '${message}'`,
);
};

export { adjustCapacityHandler as handleEvent };

registerTaskRouterEventHandler(
[RESERVATION_ACCEPTED, RESERVATION_REJECTED],
adjustCapacityHandler,
);
19 changes: 0 additions & 19 deletions lambdas/account-scoped/src/conversation/adjustChatCapacity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,8 @@

import '@twilio-labs/serverless-runtime-types';
import { WorkerChannelInstance } from 'twilio/lib/rest/taskrouter/v1/workspace/worker/workerChannel';
import { AccountScopedHandler, HttpRequest } from '../httpTypes';
import { newMissingParameterResult } from '../httpErrors';
import type { AccountSID, WorkerSID } from '@tech-matters/twilio-types';
import { getTwilioClient, getWorkspaceSid } from '@tech-matters/twilio-configuration';
import { newOk } from '../Result';

export type AdjustChatCapacityParameters = {
workerSid: WorkerSID;
Expand Down Expand Up @@ -125,19 +122,3 @@ export const adjustChatCapacity = async (

return { status: 400, message: 'Invalid adjustment argument' };
};

export const adjustChatCapacityHandler: AccountScopedHandler = async (
{ body: event }: HttpRequest,
accountSid: AccountSID,
) => {
const { workerSid, adjustment } = event;

if (workerSid === undefined) return newMissingParameterResult('workerSid');
if (adjustment === undefined) return newMissingParameterResult('adjustment');

const validBody = { workerSid, adjustment };

const { status, message } = await adjustChatCapacity(accountSid, validBody);

return newOk({ message, status });
};
209 changes: 209 additions & 0 deletions lambdas/account-scoped/src/conversation/janitorTaskRouterListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/**
* 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 {
registerTaskRouterEventHandler,
TaskRouterEventHandler,
} from '../taskrouter/taskrouterEventHandler';
import { AccountSID } from '@tech-matters/twilio-types';
import { Twilio } from 'twilio';
import {
EventType,
TASK_CANCELED,
TASK_COMPLETED,
TASK_DELETED,
TASK_SYSTEM_DELETED,
TASK_WRAPUP,
} from '../taskrouter/eventTypes';
import { EventFields } from '../taskrouter';
import { retrieveFeatureFlags } from '../configuration/aseloConfiguration';
import { chatChannelJanitor } from './chatChannelJanitor';
import { hasTaskControl } from '../transfer/hasTaskControl';
import { isChatCaptureControlTask } from '../channelCapture/channelCaptureHandlers';
import { isAseloCustomChannel } from '../customChannels/aseloCustomChannels';
import { getWorkspaceSid } from '@tech-matters/twilio-configuration';
import { ChatChannelSID, ConversationSID } from '@tech-matters/twilio-types';

const isCleanupBotCapture = (
eventType: EventType,
taskAttributes: { isChatCaptureControl?: boolean },
) => eventType === TASK_CANCELED && isChatCaptureControlTask(taskAttributes);

const isHandledByOtherListener = async (
client: Twilio,
workspaceSid: string,
taskSid: string,
taskAttributes: { isChatCaptureControl?: boolean },
) => {
if (isChatCaptureControlTask(taskAttributes)) {
console.debug('isHandledByOtherListener? - Yes, isChatCaptureControl');
return true;
}
const res = !(await hasTaskControl({ client, workspaceSid, taskSid }));
if (res) {
console.debug(
'isHandledByOtherListener? - Yes, does not have task control',
taskAttributes,
);
} else {
console.debug('isHandledByOtherListener? - No, not handled by other listener');
}
return res;
};

const isCleanupCustomChannel = async (
eventType: EventType,
client: Twilio,
workspaceSid: string,
taskSid: string,
taskAttributes: {
channelType?: string;
customChannelType?: string;
isChatCaptureControl?: boolean;
},
) => {
if (![TASK_DELETED, TASK_SYSTEM_DELETED, TASK_CANCELED].includes(eventType as any)) {
return false;
}

if (await isHandledByOtherListener(client, workspaceSid, taskSid, taskAttributes)) {
return false;
}

return isAseloCustomChannel(
taskAttributes.customChannelType || taskAttributes.channelType,
);
};

const wait = (ms: number): Promise<void> =>
new Promise(resolve => {
setTimeout(resolve, ms);
});

const janitorHandler: TaskRouterEventHandler = async (
event: EventFields,
accountSid: AccountSID,
client: Twilio,
) => {
const featureFlags = await retrieveFeatureFlags(client);

if (!featureFlags.use_twilio_lambda_janitor) {
console.debug(
`JanitorTaskRouterListener skipped for account ${accountSid} - use_twilio_lambda_janitor flag not enabled`,
);
return;
}

try {
const {
EventType: eventType,
TaskAttributes: taskAttributesString,
TaskSid: taskSid,
TaskChannelUniqueName: taskChannelUniqueName,
} = event;

if (!['chat', 'survey'].includes(taskChannelUniqueName)) return;

console.info(
`JanitorListener executing for account ${accountSid}, task ${taskSid}, event: ${eventType}`,
);

if (taskChannelUniqueName === 'survey' && eventType !== TASK_CANCELED) {
console.debug(
`Survey task ${taskSid} (account ${accountSid}) skipped on event ${eventType} - only handled on ${TASK_CANCELED}`,
);
return;
}

const taskAttributes = JSON.parse(taskAttributesString || '{}');
const { channelSid, conversationSid } = taskAttributes;
const workspaceSid = await getWorkspaceSid(accountSid);

if (isCleanupBotCapture(eventType, taskAttributes)) {
await wait(3000);

await chatChannelJanitor(accountSid, {
channelSid: channelSid as ChatChannelSID,
conversationSid: conversationSid as ConversationSID,
});

console.info(
`JanitorListener: bot capture clean up finished for account ${accountSid}, task ${taskSid}`,
);
return;
}

if (
await isCleanupCustomChannel(
eventType,
client,
workspaceSid,
taskSid,
taskAttributes,
)
) {
console.info(
`JanitorListener: handling custom channel clean up for account ${accountSid}, task ${taskSid}`,
);

await chatChannelJanitor(accountSid, {
channelSid: taskAttributes.channelSid as ChatChannelSID,
});

console.info(
`JanitorListener: custom channel clean up finished for account ${accountSid}, task ${taskSid}`,
);
return;
}

if (
!(await isHandledByOtherListener(client, workspaceSid, taskSid, taskAttributes))
) {
if (!featureFlags.enable_post_survey) {
console.info(
`JanitorListener: deactivating conversation orchestration for account ${accountSid}, task ${taskSid}`,
);

await chatChannelJanitor(accountSid, {
channelSid: channelSid as ChatChannelSID,
conversationSid: conversationSid as ConversationSID,
});

console.info(
`JanitorListener: conversation orchestration deactivated for account ${accountSid}, task ${taskSid}`,
);
return;
}
}

console.info(
`JanitorListener finished successfully for account ${accountSid}, task ${taskSid}, event: ${eventType}`,
);
} catch (err) {
console.error(
`JanitorListener failed for account ${accountSid}, task ${event.TaskSid}`,
err,
);
throw err;
}
};

export { janitorHandler as handleEvent };

registerTaskRouterEventHandler(
[TASK_CANCELED, TASK_WRAPUP, TASK_COMPLETED, TASK_DELETED, TASK_SYSTEM_DELETED],
janitorHandler,
);
25 changes: 25 additions & 0 deletions lambdas/account-scoped/src/customChannels/aseloCustomChannels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* 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/.
*/

export enum AseloCustomChannel {
Instagram = 'instagram',
Line = 'line',
Modica = 'modica',
Telegram = 'telegram',
}

export const isAseloCustomChannel = (channelType?: string): boolean =>
Object.values(AseloCustomChannel).includes(channelType as AseloCustomChannel);
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { AccountSID, ConversationSID } from '@tech-matters/twilio-types';
import { Twilio } from 'twilio';
import { getTwilioClient } from '@tech-matters/twilio-configuration';
import { AseloCustomChannel } from './aseloCustomChannels';

const CONVERSATION_CLOSE_TIMEOUT = 'P3D'; // ISO 8601 duration format https://en.wikipedia.org/wiki/ISO_8601

Expand Down Expand Up @@ -75,12 +76,7 @@ export const removeConversation = async (
},
) => client.conversations.v1.conversations(conversationSid).remove();

export enum AseloCustomChannel {
Instagram = 'instagram',
Line = 'line',
Modica = 'modica',
Telegram = 'telegram',
}
export { AseloCustomChannel, isAseloCustomChannel } from './aseloCustomChannels';

type CreateFlexConversationParams = {
studioFlowSid: string;
Expand Down
5 changes: 0 additions & 5 deletions lambdas/account-scoped/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ import { refreshTokenHandler } from './webchatAuthentication/refreshToken';
import { getAccountSid } from '@tech-matters/twilio-configuration';
import { validateRequestWithTwilioJwtToken } from './validation/twilioJwt';
import { transferStartHandler } from './transfer/transferStart';
import { adjustChatCapacityHandler } from './conversation/adjustChatCapacity';
import { reportToIWFHandler } from './integrations/iwf/reportToIWF';
import { selfReportToIWFHandler } from './integrations/iwf/selfReportToIWF';

Expand Down Expand Up @@ -122,10 +121,6 @@ const ACCOUNTSID_ROUTES: Record<
requestPipeline: [validateWebhookRequest],
handler: handleConversationEvent,
},
'conversation/adjustChatCapacity': {
requestPipeline: [validateFlexTokenRequest({ tokenMode: 'agent' })],
handler: adjustChatCapacityHandler,
},
'customChannels/instagram/instagramToFlex': {
requestPipeline: [],
handler: instagramToFlexHandler,
Expand Down
3 changes: 3 additions & 0 deletions lambdas/account-scoped/src/taskrouter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import '../hrm/conversationDurationTaskRouterListener';
import '../task/addCustomerExternalIdTaskRouterListener';
import '../task/addInitialHangUpByTaskRouterListener';
import '../conversation/addTaskSidToChannelAttributesTaskRouterListener';
import '../conversation/adjustCapacityTaskRouterListener';
import '../conversation/janitorTaskRouterListener';
import '../channelCapture/postSurveyListener';
import '../transfer/transfersTaskRouterListener';

export { handleTaskRouterEvent } from './taskrouterEventHandler';

Expand Down
Loading
Loading