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
4 changes: 0 additions & 4 deletions lambdas/account-scoped/src/conversation/chatChannelJanitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,8 @@ const deactivateConversation = async (
const conversation = await client.conversations.v1.conversations
.get(conversationSid)
.fetch();
const attributes = JSON.parse(conversation.attributes);

if (conversation.state !== 'closed') {
if (attributes.proxySession) {
await deleteProxySession(accountSid, attributes.proxySession);
}
console.info('Attempting to deactivate active conversation', conversationSid);
const updated = await conversation.update({
state: 'closed',
Expand Down
107 changes: 107 additions & 0 deletions lambdas/account-scoped/src/conversation/createConversation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* Copyright (C) 2021-2026 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 { Twilio } from 'twilio';
import { ConversationSID } from '@tech-matters/twilio-types';
import { AseloCustomChannel } from '../customChannels/aseloCustomChannels';

const CONVERSATION_CLOSE_TIMEOUT = 'P3D'; // ISO 8601 duration format https://en.wikipedia.org/wiki/ISO_8601
export type CreateFlexConversationParams = {
studioFlowSid: string;
channelType: AseloCustomChannel | 'web'; // The chat channel being used
uniqueUserName: string; // Unique identifier for this user
senderScreenName: string; // Friendly info to show in the Flex UI (like Telegram handle)
onMessageAddedWebhookUrl?: string; // The url that must be used as the onMessageSent event webhook.
conversationFriendlyName: string; // A name for the Flex conversation (typically same as uniqueUserName)
twilioNumber: string; // The target Twilio number (usually have the shape <channel>:<id>, e.g. telegram:1234567)
additionalConversationAttributes?: Record<string, any>; // Any additional conversation attributes
testSessionId?: string; // A session identifier to identify the test run if this is part of an integration test.
};
/**
* Creates a new Flex conversation in the provided Flex Flow and subscribes webhooks to it's events.
* Adds to the channel attributes the provided twilioNumber used for routing.
*/
export const createConversation = async (
client: Twilio,
{
conversationFriendlyName,
channelType,
twilioNumber,
uniqueUserName,
senderScreenName,
onMessageAddedWebhookUrl,
studioFlowSid,
additionalConversationAttributes,
testSessionId,
}: CreateFlexConversationParams,
): Promise<{ conversationSid: ConversationSID; error?: Error }> => {
if (testSessionId) {
console.info(
'testSessionId specified. All outgoing messages will be sent to the test API.',
);
}

const conversationInstance = await client.conversations.v1.conversations.create({
xTwilioWebhookEnabled: 'true',
friendlyName: conversationFriendlyName,
uniqueName: `${channelType}/${uniqueUserName}/${Date.now()}`,
});
const conversationSid = conversationInstance.sid as ConversationSID;

try {
const conversationContext =
client.conversations.v1.conversations.get(conversationSid);
await conversationContext.participants.create({
identity: uniqueUserName,
});
const channelAttributes = JSON.parse((await conversationContext.fetch()).attributes);

console.debug('channelAttributes prior to update', channelAttributes);

await conversationContext.update({
'timers.closed': CONVERSATION_CLOSE_TIMEOUT,
state: 'active',
attributes: JSON.stringify({
...channelAttributes,
channel_type: channelType,
channelType,
senderScreenName,
twilioNumber,
testSessionId,
...additionalConversationAttributes,
}),
});

await conversationContext.webhooks.create({
target: 'studio',
'configuration.flowSid': studioFlowSid,
'configuration.filters': ['onMessageAdded'],
});
if (onMessageAddedWebhookUrl) {
/* const onMessageAdded = */
await conversationContext.webhooks.create({
target: 'webhook',
'configuration.method': 'POST',
'configuration.url': onMessageAddedWebhookUrl,
'configuration.filters': ['onMessageAdded'],
});
}
} catch (err) {
return { conversationSid, error: err as Error };
}

return { conversationSid };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* 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 { ConversationInstance } from 'twilio/lib/rest/conversations/v1/conversation';
import { Twilio } from 'twilio';
import { ConversationSID } from '@tech-matters/twilio-types';

// TODO: Add optimistic concurrency checks
/**
*
* @param client - Twilio client
* @param conversation - either the instance of the conversation which must have been fetched prior to calling, or the sid to fetch
* @param attributePatch - attributes to update. Will add these if they don't exist or overwrite them if they do. Setting attributes undefined will NOT remove them, there will be an attribute e
*/
export const patchConversationAttributes = async (
client: Twilio,
conversation: ConversationInstance | ConversationSID,
attributePatch: Record<string, any>,
) => {
let conversationInstance: ConversationInstance;
if (typeof conversation !== 'object') {
conversationInstance = await client.conversations.v1.conversations
.get(conversation)
.fetch();
} else {
conversationInstance = conversation;
}
const conversationAttributes = JSON.parse(conversationInstance.attributes);
const patchedAttributes = { ...conversationAttributes, ...attributePatch };
return client.conversations.v1.conversations
.get(conversationInstance.sid)
.update({ attributes: patchedAttributes });
};
2 changes: 1 addition & 1 deletion lambdas/account-scoped/src/customChannels/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export const getFacebookAppSecret = (): Promise<string> =>

export const getChannelStudioFlowSid = (
accountSid: AccountSID,
channelName: AseloCustomChannel,
channelName: AseloCustomChannel | 'web',
): Promise<string> =>
getSsmParameter(
`/${process.env.NODE_ENV}/twilio/${accountSid}/${channelName}_studio_flow_sid`,
Expand Down
95 changes: 6 additions & 89 deletions lambdas/account-scoped/src/customChannels/customChannelToFlex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
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
import {
createConversation,
CreateFlexConversationParams,
} from '../conversation/createConversation';

export const findExistingConversation = async (
client: Twilio,
Expand Down Expand Up @@ -78,90 +79,6 @@ export const removeConversation = async (

export { AseloCustomChannel, isAseloCustomChannel } from './aseloCustomChannels';

type CreateFlexConversationParams = {
studioFlowSid: string;
channelType: AseloCustomChannel; // The chat channel being used
uniqueUserName: string; // Unique identifier for this user
senderScreenName: string; // Friendly info to show in the Flex UI (like Telegram handle)
onMessageSentWebhookUrl: string; // The url that must be used as the onMessageSent event webhook.
conversationFriendlyName: string; // A name for the Flex conversation (typically same as uniqueUserName)
testSessionId?: string; // A session identifier to identify the test run if this is part of an integration test.
twilioNumber: string; // The target Twilio number (usually have the shape <channel>:<id>, e.g. telegram:1234567)
};

/**
* Creates a new Flex conversation in the provided Flex Flow and subscribes webhooks to it's events.
* Adds to the channel attributes the provided twilioNumber used for routing.
*/
const createConversation = async (
client: Twilio,
{
conversationFriendlyName,
channelType,
twilioNumber,
uniqueUserName,
senderScreenName,
onMessageSentWebhookUrl,
studioFlowSid,
testSessionId,
}: CreateFlexConversationParams,
): Promise<{ conversationSid: ConversationSID; error?: Error }> => {
if (testSessionId) {
console.info(
'testSessionId specified. All outgoing messages will be sent to the test API.',
);
}

const conversationInstance = await client.conversations.v1.conversations.create({
xTwilioWebhookEnabled: 'true',
friendlyName: conversationFriendlyName,
uniqueName: `${channelType}/${uniqueUserName}/${Date.now()}`,
});
const conversationSid = conversationInstance.sid as ConversationSID;

try {
const conversationContext =
client.conversations.v1.conversations.get(conversationSid);
await conversationContext.participants.create({
identity: uniqueUserName,
});
const channelAttributes = JSON.parse((await conversationContext.fetch()).attributes);

console.debug('channelAttributes prior to update', channelAttributes);

await conversationContext.update({
'timers.closed': CONVERSATION_CLOSE_TIMEOUT,
state: 'active',
attributes: JSON.stringify({
...channelAttributes,
channel_type: channelType,
channelType,
senderScreenName,
twilioNumber,
testSessionId,
}),
});

await conversationContext.webhooks.create({
target: 'studio',
'configuration.flowSid': studioFlowSid,
'configuration.filters': ['onMessageAdded'],
});

/* const onMessageAdded = */
await conversationContext.webhooks.create({
target: 'webhook',
'configuration.method': 'POST',
'configuration.url': onMessageSentWebhookUrl,
'configuration.filters': ['onMessageAdded'],
});
} catch (err) {
return { conversationSid, error: err as Error };
}

return { conversationSid };
};

type SendConversationMessageToFlexParams = Omit<
CreateFlexConversationParams,
'twilioNumber'
Expand All @@ -188,7 +105,7 @@ export const sendConversationMessageToFlex = async (
customTwilioNumber,
uniqueUserName,
senderScreenName,
onMessageSentWebhookUrl,
onMessageAddedWebhookUrl,
messageText,
messageAttributes = undefined,
senderExternalId,
Expand Down Expand Up @@ -216,7 +133,7 @@ export const sendConversationMessageToFlex = async (
twilioNumber,
uniqueUserName,
senderScreenName,
onMessageSentWebhookUrl,
onMessageAddedWebhookUrl,
conversationFriendlyName,
testSessionId,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export const instagramToFlexHandler: AccountScopedHandler = async (
channelType,
uniqueUserName,
senderScreenName,
onMessageSentWebhookUrl,
onMessageAddedWebhookUrl: onMessageSentWebhookUrl,
messageText,
messageAttributes,
senderExternalId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export const lineToFlexHandler: AccountScopedHandler = async (
channelType,
uniqueUserName,
senderScreenName,
onMessageSentWebhookUrl,
onMessageAddedWebhookUrl: onMessageSentWebhookUrl,
messageText,
senderExternalId,
customSubscribedExternalId: subscribedExternalId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const modicaToFlexHandler: AccountScopedHandler = async (
channelType,
uniqueUserName,
senderScreenName,
onMessageSentWebhookUrl,
onMessageAddedWebhookUrl: onMessageSentWebhookUrl,
messageText,
senderExternalId,
customSubscribedExternalId: subscribedExternalId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const telegramToFlexHandler: AccountScopedHandler = async (
const chatFriendlyName = username || `${channelType}:${senderExternalId}`;
const uniqueUserName = `${channelType}:${senderExternalId}`;
const senderScreenName = firstName || username || 'child';
const onMessageSentWebhookUrl = `${process.env.WEBHOOK_BASE_URL}/lambda/twilio/account-scoped/${accountSid}/customChannels/telegram/flexToTelegram?recipientId=${senderExternalId}`;
const onMessageAddedWebhookUrl = `${process.env.WEBHOOK_BASE_URL}/lambda/twilio/account-scoped/${accountSid}/customChannels/telegram/flexToTelegram?recipientId=${senderExternalId}`;
const studioFlowSid = await getChannelStudioFlowSid(
accountSid,
AseloCustomChannel.Telegram,
Expand All @@ -82,7 +82,7 @@ export const telegramToFlexHandler: AccountScopedHandler = async (
channelType,
uniqueUserName,
senderScreenName,
onMessageSentWebhookUrl,
onMessageAddedWebhookUrl,
messageText,
senderExternalId,
conversationFriendlyName: chatFriendlyName,
Expand Down
8 changes: 0 additions & 8 deletions lambdas/account-scoped/src/hrm/sanitizeIdentifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,6 @@ export const sanitizeIdentifierFromTrigger = ({
});
}

if (channelType === 'web') {
console.error(`Channel type ${channelType} is not supported in conversations`);
return newErr({
message: `Channel type ${channelType} is not supported in conversations`,
error: { type: 'unsupported-channel', channelType },
});
}

const transformed = applyTransformations({
channelType,
identifier: trigger.conversation.Author,
Expand Down
Loading
Loading