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
@@ -0,0 +1,94 @@
/**
* 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 type { AccountSID } from '@tech-matters/twilio-types';
import { getChatServiceSid, getTwilioClient } from '@tech-matters/twilio-configuration';
import { newErr } from '../Result';
import type { AccountScopedHandler, HttpRequest } from '../httpTypes';
import { sendSystemMessage } from './sendSystemMessage';
import { chatChannelJanitor } from './chatChannelJanitor';

export const sendMessageAndRunJanitorHandler: AccountScopedHandler = async (
{ body }: HttpRequest,
accountSid: AccountSID,
) => {
try {
const { channelSid, conversationSid } = body;
console.debug('sendMessageAndRunJanitor execution', {
accountSid,
conversationSid,
channelSid,
});

if (channelSid === undefined && conversationSid === undefined) {
return newErr({
message: 'none of channelSid and conversationSid provided, exactly one expected.',
error: { statusCode: 400 },
});
}

const client = await getTwilioClient(accountSid);

if (conversationSid) {
const conversationWebhooks = await client.conversations.v1.conversations
.get(conversationSid)
.webhooks.list();

console.debug(`Removing studio webhooks from conversation ${conversationSid}`);
// Remove the studio trigger webhooks to prevent this channel from triggering subsequent Studio flows executions
await Promise.all(
conversationWebhooks.map(async w => {
if (w.target === 'studio') {
await w.remove();
}
}),
);
console.info(`Studio webhooks removed from conversation ${conversationSid}`);

const result = await sendSystemMessage(accountSid, body);

await chatChannelJanitor(accountSid, { conversationSid });

return result;
} else {
const chatServiceSid = await getChatServiceSid(accountSid);
const channelWebhooks = await client.chat.v2.services
.get(chatServiceSid)
.channels.get(channelSid)
.webhooks.list();

console.debug(`Removing studio webhooks from channel ${channelSid}`);
// Remove the studio trigger webhooks to prevent this channel from triggering subsequent Studio flows executions
await Promise.all(
channelWebhooks.map(async w => {
if (w.type === 'studio') {
await w.remove();
}
}),
);
console.info(`Studio webhooks removed from channel ${channelSid}`);

const result = await sendSystemMessage(accountSid, body);

await chatChannelJanitor(accountSid, { channelSid });

return result;
}
} catch (err: any) {
console.error('sendMessageAndRunJanitor failed', err);
return newErr({ message: err.message, error: { statusCode: 500, cause: err } });
}
};
58 changes: 58 additions & 0 deletions lambdas/account-scoped/src/conversation/sendStudioMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* 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 type { AccountSID, ChatChannelSID } from '@tech-matters/twilio-types';
import { getChatServiceSid, getTwilioClient } from '@tech-matters/twilio-configuration';
import { newErr } from '../Result';
import type { AccountScopedHandler, HttpRequest } from '../httpTypes';
import { sendSystemMessage } from './sendSystemMessage';

const removeStudioWebhook = async (
accountSid: AccountSID,
channelSid: ChatChannelSID,
): Promise<void> => {
const client = await getTwilioClient(accountSid);
const chatServiceSid = await getChatServiceSid(accountSid);
const webhooks = await client.chat.v2.services
.get(chatServiceSid)
.channels.get(channelSid)
.webhooks.list();

const studioWebhook = webhooks.find(w => w.type === 'studio');
await studioWebhook?.remove();
};

export const sendStudioMessageHandler: AccountScopedHandler = async (
{ body }: HttpRequest,
accountSid: AccountSID,
) => {
try {
const { channelSid } = body;
console.debug('sendStudioMessage execution', { accountSid, channelSid });

/**
* We need to remove the studio webhook before calling sendSystemMessage
* because it would trigger another execution of studio.
*/
console.debug(`Removing studio webhook from channel ${channelSid}`);
await removeStudioWebhook(accountSid, channelSid);
console.info(`Studio webhook removed from channel ${channelSid}`);
return await sendSystemMessage(accountSid, body);
} catch (err: any) {
console.error('sendStudioMessage failed', err);
return newErr({ message: err.message, error: { statusCode: 500, cause: err } });
}
};
135 changes: 135 additions & 0 deletions lambdas/account-scoped/src/conversation/sendSystemMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/**
* 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 type {
AccountSID,
ChatChannelSID,
ConversationSID,
} from '@tech-matters/twilio-types';
import {
getChatServiceSid,
getTwilioClient,
getWorkspaceSid,
} from '@tech-matters/twilio-configuration';
import { newErr, newOk } from '../Result';
import type { Result } from '../Result';
import type { HttpError } from '../httpTypes';
import type { FlexValidatedHandler } from '../validation/flexToken';

export type SendSystemMessageBody = (
| { channelSid: ChatChannelSID; conversationSid?: ConversationSID; taskSid?: string }
| { channelSid?: ChatChannelSID; conversationSid: ConversationSID; taskSid?: string }
| { channelSid?: ChatChannelSID; conversationSid?: ConversationSID; taskSid: string }
) & {
message?: string;
from?: string;
};

export const sendSystemMessage = async (
accountSid: AccountSID,
event: SendSystemMessageBody,
): Promise<Result<HttpError, any>> => {
const { taskSid, channelSid, conversationSid, message, from } = event;

console.debug('sendSystemMessage execution', {
accountSid,
conversationSid,
channelSid,
taskSid,
});

if (!channelSid && !taskSid && !conversationSid) {
return newErr({
message:
'none of taskSid, channelSid, or conversationSid provided, exactly one expected.',
error: { statusCode: 400 },
});
}

if (message === undefined) {
return newErr({ message: 'missing message.', error: { statusCode: 400 } });
}

const client = await getTwilioClient(accountSid);

let channelSidToMessage: ChatChannelSID | null = null;
let conversationSidToMessage: ConversationSID | null = null;

if (channelSid) {
channelSidToMessage = channelSid;
} else if (conversationSid) {
conversationSidToMessage = conversationSid;
} else if (taskSid) {
const workspaceSid = await getWorkspaceSid(accountSid);
const task = await client.taskrouter.v1.workspaces
.get(workspaceSid)
.tasks.get(taskSid)
.fetch();
const taskAttributes = JSON.parse(task.attributes);
const { channelSid: taskChannelSid, conversationSid: taskConversationSid } =
taskAttributes;
channelSidToMessage = taskChannelSid ?? null;
conversationSidToMessage = taskConversationSid ?? null;
}

if (conversationSidToMessage) {
console.info(`Sending system message to conversation ${conversationSidToMessage}`);
const messageResult = await client.conversations.v1.conversations
.get(conversationSidToMessage)
.messages.create({
body: message,
author: from,
xTwilioWebhookEnabled: 'true',
});
console.info(
`System message sent successfully to conversation ${conversationSidToMessage}`,
);
return newOk(messageResult);
}

if (channelSidToMessage) {
console.info(`Sending system message to channel ${channelSidToMessage}`);
const chatServiceSid = await getChatServiceSid(accountSid);
const messageResult = await client.chat.v2.services
.get(chatServiceSid)
.channels.get(channelSidToMessage)
.messages.create({
body: message,
from,
xTwilioWebhookEnabled: 'true',
});
console.info(`System message sent successfully to channel ${channelSidToMessage}`);
return newOk(messageResult);
}

return newErr({
message:
'Conversation or Chat Channel SID were not provided directly or specified on the provided task',
error: { statusCode: 400 },
});
};

export const sendSystemMessageHandler: FlexValidatedHandler = async (
{ body },
accountSid,
) => {
try {
return await sendSystemMessage(accountSid, body as SendSystemMessageBody);
} catch (err: any) {
console.error('sendSystemMessage failed', err);
return newErr({ message: err.message, error: { statusCode: 500, cause: err } });
}
};
15 changes: 15 additions & 0 deletions lambdas/account-scoped/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ import { transferStartHandler } from './transfer/transferStart';
import { adjustChatCapacityHandler } from './conversation/adjustChatCapacity';
import { reportToIWFHandler } from './integrations/iwf/reportToIWF';
import { selfReportToIWFHandler } from './integrations/iwf/selfReportToIWF';
import { sendSystemMessageHandler } from './conversation/sendSystemMessage';
import { sendStudioMessageHandler } from './conversation/sendStudioMessage';
import { sendMessageAndRunJanitorHandler } from './conversation/sendMessageAndRunJanitor';

/**
* Super simple router sufficient for directly ported Twilio Serverless functions
Expand Down Expand Up @@ -194,6 +197,18 @@ const ACCOUNTSID_ROUTES: Record<
requestPipeline: [validateFlexTokenRequest({ tokenMode: 'agent' })],
handler: selfReportToIWFHandler,
},
'conversation/sendSystemMessage': {
requestPipeline: [validateFlexTokenRequest({ tokenMode: 'agent' })],
handler: sendSystemMessageHandler,
},
'conversation/sendStudioMessage': {
requestPipeline: [validateWebhookRequest],
handler: sendStudioMessageHandler,
},
'conversation/sendMessageAndRunJanitor': {
requestPipeline: [validateWebhookRequest],
handler: sendMessageAndRunJanitorHandler,
},
};

const ENV_SHORTCODE_ROUTES: Record<string, FunctionRoute> = {
Expand Down
Loading
Loading