Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,13 @@ import { newErr, newOk } from '../Result';
import { getTwilioClient, getDocsBucketName } from '@tech-matters/twilio-configuration';
import { newMissingParameterResult } from '../httpErrors';

export const getExternalRecordingS3LocationHandler: FlexValidatedHandler = async (
{ body: event },
accountSid: AccountSID,
) => {
const { callSid } = event as { callSid?: string };

if (!callSid) {
return newMissingParameterResult('callSid');
}

export const getExternalRecordingS3Location = async ({
callSid,
accountSid,
}: {
callSid: string;
accountSid: AccountSID;
}) => {
try {
const client = await getTwilioClient(accountSid);
const bucket = await getDocsBucketName(accountSid);
Expand All @@ -56,3 +53,16 @@ export const getExternalRecordingS3LocationHandler: FlexValidatedHandler = async
return newErr({ message: err.message, error: { statusCode: 500, cause: err } });
}
};

export const getExternalRecordingS3LocationHandler: FlexValidatedHandler = async (
{ body: event },
accountSid: AccountSID,
) => {
const { callSid } = event as { callSid?: string };

if (!callSid) {
return newMissingParameterResult('callSid');
}

return getExternalRecordingS3Location({ accountSid, callSid });
};
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { HrmContact } from '@tech-matters/hrm-types';
import { populateHrmContactFormFromTaskByMappings } from './populateHrmContactFormFromTaskByMappings';
import { parseISO } from 'date-fns/parseISO';
import { HttpClientError } from '../httpErrors';
import { getExternalRecordingS3Location } from '../conversation/getExternalRecordingS3Location';

// Temporarily copied to this repo, will share the flex types when we move them into the same repo

Expand Down Expand Up @@ -215,7 +216,9 @@ export const handleEvent = async (
timeOfContact: timeOfContactDate.toISOString(),
number: identifier,
};

console.debug('Creating HRM contact with timeOfContact:', newContact.timeOfContact);

const prepopulate = usePrepopulateMappings
? populateHrmContactFormFromTaskByMappings
: populateHrmContactFormFromTaskByKeys;
Expand Down Expand Up @@ -245,6 +248,44 @@ export const handleEvent = async (
const savedTimeOfContactDate = parseISO(savedTimeOfContactString);
console.info(`Created HRM contact with id ${id} for task ${taskSid}`);

if (channel === ('voicemail' as any)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we attempt to consolidate setting recordings on the backend instead of having this as a special case for the productionising step?

console.info(
`Channel type is ${channel}, adding conversation media with call sid ${taskAttributes.callSid}`,
);
const recordingResult = await getExternalRecordingS3Location({
accountSid,
callSid: taskAttributes.callSid,
});

if (isOk(recordingResult)) {
const conversationMedia = [
{
storeType: 'S3',
storeTypeSpecificData: {
type: 'recording',
location: {
bucket: recordingResult.data.bucket,
key: recordingResult.data.key,
},
},
},
];

const conversationMediaResult = await postToInternalHrmEndpoint<
HrmContact['conversationMedia'],
HrmContact
>(
hrmAccountId,
hrmApiVersion,
`contacts/${id}/conversationMedia`,
conversationMedia,
);
console.debug(
`[SENSITIVE] Conversation media result ${conversationMediaResult.status} ${isOk(conversationMediaResult) ? JSON.stringify(conversationMediaResult.data) : JSON.stringify(conversationMediaResult.error)}`,
);
}
}

const taskContext = client.taskrouter.v1.workspaces
.get(twilioWorkspaceSid)
.tasks.get(taskSid);
Expand Down
1 change: 1 addition & 0 deletions lambdas/account-scoped/src/hrm/sanitizeIdentifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const aseloConnectorNormalization = (s: string) => s.match(/sip:([^@]+)/)?.[1] |
type TransformIdentifierFunction = (c: string) => string;
const channelTransformations: { [k: string]: TransformIdentifierFunction[] } = {
voice: [aseloConnectorNormalization, phoneNumberStandardization],
voicemail: [aseloConnectorNormalization, phoneNumberStandardization],
sms: [phoneNumberStandardization],
whatsapp: [s => s.replace('whatsapp:', ''), phoneNumberStandardization],
modica: [s => s.replace('modica:', ''), phoneNumberStandardization],
Expand Down
5 changes: 5 additions & 0 deletions lambdas/account-scoped/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import { triggerPostStudioFlowHandler } from './studioFlow/postStudioFlowTaskRou
import { randomOptionSelectorHandler } from './randomOptionSelector';
import { isSkilledWorkerAvailableHandler } from './worker/isSkilledWorkerAvailable';
import { filterCountryOrVoIPHandler } from './voice/filterCountryOrVoIP';
import { recordingCompleteCallback } from './voicemail/recordingCompleteCallback';

/**
* Super simple router sufficient for directly ported Twilio Serverless functions
Expand Down Expand Up @@ -390,6 +391,10 @@ const ACCOUNTSID_ROUTES: Record<
requestPipeline: [validateRequestMethod('POST'), validateWebhookRequest],
handler: sendMessageAndRunJanitorHandler,
}),
'voicemail/recordingCompleteCallback': newRoute({
requestPipeline: [validateWebhookRequest],
handler: recordingCompleteCallback,
}),
issueSyncToken: newRoute({
requestPipeline: [
validateRequestMethod('POST'),
Expand Down
63 changes: 63 additions & 0 deletions lambdas/account-scoped/src/voicemail/recordingCompleteCallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* 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 { getTwilioClient, getWorkspaceSid } from '@tech-matters/twilio-configuration';
import { AccountScopedHandler, HttpError } from '../httpTypes';
import { newOk, Result } from '../Result';

export type RecordingCompleteCallbackRequestBody = {
callFrom: string;
};

export const recordingCompleteCallback: AccountScopedHandler = async (
{ body },
accountSid,
): Promise<Result<HttpError, any>> => {
console.debug('recordingCompleteCallback body', JSON.stringify(body, null, 2));
// const { callFrom } = body as RecordingCompleteCallbackRequestBody;

// if (!callFrom) {
// return newErr({
// message: 'callFrom parameter is missing',
// error: { statusCode: 400 },
// });
// }

const twilioClient = await getTwilioClient(accountSid);

const workspaceSid = await getWorkspaceSid(accountSid);
const voicemailTask = await twilioClient.taskrouter.v1
.workspaces(workspaceSid)
.tasks.create({
timeout: 604800, // 7 days
attributes: JSON.stringify({
...(body.routingAttributes ? JSON.parse(body.routingAttributes) : {}),
isVoicemail: true,
callSid: body.callSid,
from: body.from,
name: body.from,
channelType: 'voicemail',
customChannelType: 'voicemail',
ignoreAgent: '',
transferTargetType: '',
}),
workflowSid: body.voicemailWorkflowSid,
// TODO: factor out channel types into an enum
taskChannel: 'voicemail',
});

return newOk({ voicemailTask });
};
1 change: 1 addition & 0 deletions lambdas/packages/hrm-types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type HangUpBy =

export type ChannelTypes =
| 'voice'
| 'voicemail'
| 'sms'
| 'facebook'
| 'messenger'
Expand Down
3 changes: 3 additions & 0 deletions plugin-hrm-form/src/HrmFormPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { FeatureFlags } from './types/FeatureFlags';
import { setUpFullStory } from './fullStory/setUp';
import { getPathFromUrl } from './states/routing/reducer';
import { setUpCustomSideLinks } from './components/customSideLinks/setUpCustomSideLinks';
import { setUpVoicemailComponents } from './voicemail/setUpVoicemailComponents';

const PLUGIN_NAME = 'HrmFormPlugin';

Expand Down Expand Up @@ -145,6 +146,8 @@ const setUpComponents = (featureFlags: FeatureFlags, setupObject: ReturnType<typ
if (featureFlags.enable_language_selector) Components.setupWorkerLanguageSelect();

setUpCustomSideLinks();

setUpVoicemailComponents();
};

const setUpActions = (
Expand Down
2 changes: 2 additions & 0 deletions plugin-hrm-form/src/channels/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const whatsappColor = mainChannelColor(DefaultTaskChannels.ChatWhatsApp);
const telegramColor = '#1DA1F2';
const instagramColor = '#833AB4';
const lineColor = '#00C300';
const voicemailColor = voiceColor;

export const colors: ChannelColors = {
voice: voiceColor,
Expand All @@ -53,4 +54,5 @@ export const colors: ChannelColors = {
telegram: telegramColor,
instagram: instagramColor,
line: lineColor,
voicemail: voicemailColor,
};
3 changes: 3 additions & 0 deletions plugin-hrm-form/src/components/case/timeline/TimelineIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import WhatsappIcon from '../../common/icons/WhatsappIcon';
import FacebookIcon from '../../common/icons/FacebookIcon';
import SmsIcon from '../../common/icons/SmsIcon';
import CallIcon from '../../common/icons/CallIcon';
import VoicemailIcon from '../../common/icons/VoicemailIcon';
import { colors } from '../../../channels/colors';

export type IconType = ChannelTypes | 'note' | 'referral';
Expand All @@ -55,6 +56,8 @@ export const getIcon = (type: IconType, size: string = '24px') => {
return <InstagramIcon width={size} height={size} color={colors.instagram} />;
case channelTypes.line:
return <LineIcon width={size} height={size} color={colors.line} />;
case channelTypes.voicemail:
return <VoicemailIcon width={size} height={size} color={colors.voicemail} />;
case 'note':
return <NoteIcon style={{ opacity: 0.62, fontSize: size }} />;
case 'referral':
Expand Down
40 changes: 40 additions & 0 deletions plugin-hrm-form/src/components/common/icons/VoicemailIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* 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/.
*/

/* eslint-disable react/prop-types */
import React from 'react';

type Props = {
width: string;
height: string;
color?: string;
};
/* eslint-disable react/prop-types */
const VoicemailIcon: React.FC<Props> = ({ width, height, color }) => {
return (
// <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
<svg width={width} height={height} viewBox="0 0 48 48" fill={color} version="1.1" aria-label="Voicemail">
<path d="M0 0h24v24H0z" fill="none" />
<path d="M18.5 6C15.46 6 13 8.46 13 11.5c0 1.33.47 2.55 1.26 3.5H9.74c.79-.95 1.26-2.17 1.26-3.5C11 8.46 8.54 6 5.5 6S0 8.46 0 11.5 2.46 17 5.5 17h13c3.04 0 5.5-2.46 5.5-5.5S21.54 6 18.5 6zm-13 9C3.57 15 2 13.43 2 11.5S3.57 8 5.5 8 9 9.57 9 11.5 7.43 15 5.5 15zm13 0c-1.93 0-3.5-1.57-3.5-3.5S16.57 8 18.5 8 22 9.57 22 11.5 20.43 15 18.5 15z" />
</svg>
);
};

VoicemailIcon.displayName = 'VoicemailIcon';
VoicemailIcon.defaultProps = {
color: '#00C300',
};
export default VoicemailIcon;
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* 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 React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { Template } from '@twilio/flex-ui';
import CircularProgress from '@material-ui/core/CircularProgress';

Expand All @@ -24,13 +24,19 @@ import { fetchHrmApi, generateSignedURLPath } from '../../../services/fetchHrmAp
type OwnProps = {
contactId: string;
externalStoredRecording?: S3StoredRecording;
loadConversationIntoOverlay: () => Promise<void>;
loadConversationIntoOverlay?: () => Promise<void>;
autoLoad?: boolean;
};

const RecordingSection: React.FC<OwnProps> = ({ contactId, externalStoredRecording, loadConversationIntoOverlay }) => {
const RecordingSection: React.FC<OwnProps> = ({
contactId,
externalStoredRecording,
loadConversationIntoOverlay,
autoLoad = false,
}) => {
const [voiceRecording, setVoiceRecording] = useState(null);
const [loading, setLoading] = useState(false);
const [showButton, setShowButton] = useState(true);
const [loading, setLoading] = useState(autoLoad || false);
const [showButton, setShowButton] = useState(!autoLoad);
const [errorMessage, setErrorMessage] = useState(null);

const fetchAndLoadRecording = async () => {
Expand Down Expand Up @@ -72,6 +78,13 @@ const RecordingSection: React.FC<OwnProps> = ({ contactId, externalStoredRecordi
setLoading(false);
};

useEffect(() => {
if (autoLoad) {
fetchAndLoadRecording();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

if (errorMessage) {
return (
<ErrorFont>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import { getIcon } from '../../case/timeline/TimelineIcon';
import { CoreChannelTypes, coreChannelTypes } from '../../../states/DomainConstants';
import { channelTypes, CoreChannelTypes, coreChannelTypes } from '../../../states/DomainConstants';
import { customSmsChannelTypes, customFacebookChannelTypes } from '../../../utils/groupedChannels';

type ExtendedChannelTypes =
Expand All @@ -38,4 +38,5 @@ export const iconsFromTask: { [channelType in ExtendedChannelTypes]: JSX.Element
},
[customSmsChannelTypes.modica]: getIcon(customSmsChannelTypes.modica, iconSize),
[customFacebookChannelTypes.messenger]: getIcon(customFacebookChannelTypes.messenger, iconSize),
[channelTypes.voicemail]: getIcon(channelTypes.voicemail, iconSize),
};
2 changes: 2 additions & 0 deletions plugin-hrm-form/src/components/queuesStatus/QueueCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ const renderChannel = ({
return getChannelUI('IG', channelColor, contactsWaiting, true);
case coreChannelTypes.line:
return getChannelUI('LN', channelColor, contactsWaiting, true);
case coreChannelTypes.voicemail:
return getChannelUI('VM', channelColor, contactsWaiting, true);
default:
return null;
}
Expand Down
1 change: 1 addition & 0 deletions plugin-hrm-form/src/components/queuesStatus/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const newQueueEntry: QueueEntry = {
telegram: 0,
instagram: 0,
line: 0,
voicemail: 0,
longestWaitingDate: null,
isChatPending: false,
};
Expand Down
1 change: 1 addition & 0 deletions plugin-hrm-form/src/states/DomainConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const customChannelTypes = {
telegram: 'telegram',
instagram: 'instagram',
line: 'line',
voicemail: 'voicemail',
} as const;

/**
Expand Down
3 changes: 1 addition & 2 deletions plugin-hrm-form/src/transfer/transferTaskState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,7 @@ export const closeCallSelf = async (task: ITask): Promise<void> => {
};

export const canTransferConference = (task: ITask) => {
const isChatTask = TaskHelper.isChatBasedTask(task);
if (isChatTask) {
if (!TaskHelper.isVoiceTask(task)) {
return true;
}

Expand Down
6 changes: 5 additions & 1 deletion plugin-hrm-form/src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -669,5 +669,9 @@
"BrowserNotification-ChatMessage-MaskedTitle": "New message",

"AsV1-ChildInformationTab-FirstName-Title": "First Name description",
"AsV1-ChildInformationTab-FirstName-Content": "Lorem \n ipsum dolor \n sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
"AsV1-ChildInformationTab-FirstName-Content": "Lorem \n ipsum dolor \n sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",

"VoicemailTaskPanel-CallBack": "Call Back",
"VoicemailTaskPanel-RetryLater": "Retry Later"

}
Loading
Loading