From 2d9bde51bb44e8156965254422476c714e206575 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Thu, 18 Jul 2024 13:46:36 +0100 Subject: [PATCH 01/20] ch: first webhook draft --- .../serviceConversationListener.protected.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 functions/webhooks/serviceConversationListener.protected.ts diff --git a/functions/webhooks/serviceConversationListener.protected.ts b/functions/webhooks/serviceConversationListener.protected.ts new file mode 100644 index 00000000..0f4b86f9 --- /dev/null +++ b/functions/webhooks/serviceConversationListener.protected.ts @@ -0,0 +1,77 @@ +/** + * 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 { Context, ServerlessCallback } from '@twilio-labs/serverless-runtime-types/types'; +import { + responseWithCors, + bindResolve, + // error400, + error500, + // success, +} from '@tech-matters/serverless-helpers'; + +export type ConversationSid = `CH${string}`; +export type ChatChannelSid = `CH${string}`; + +export const sendConversationMessage = async ( + context: Context, + { + conversationSid, + author, + messageText, + messageAttributes, + }: { + conversationSid: ConversationSid; + author: string; + messageText: string; + messageAttributes?: string; + }, +) => + context + .getTwilioClient() + .conversations.conversations.get(conversationSid) + .messages.create({ + body: messageText, + author, + xTwilioWebhookEnabled: 'true', + ...(messageAttributes && { attributes: messageAttributes }), + }); + +type ServiceConversationListenerEvent = { + Body: string; + Author: string; + ParticipantSid?: string; + ConversationSid: string; + EventType: string; + Source: string; +}; + +export type Body = ServiceConversationListenerEvent; + +export const handler = async (context: Context, event: Body, callback: ServerlessCallback) => { + const response = responseWithCors(); + const resolve = bindResolve(callback)(response); + try { + const { Body, EventType, Source } = event; + + if (EventType === 'onMessageSent') { + console.log('EventType is here', Body, Source, event); + } + } catch (err) { + if (err instanceof Error) resolve(error500(err)); + else resolve(error500(new Error(String(err)))); + } +}; From 22449e52da67afd2b6b7b32dbed7058436b7c089 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Fri, 19 Jul 2024 09:18:52 +0100 Subject: [PATCH 02/20] ch: first webhook draft-2 --- functions/webhooks/serviceConversationListener.protected.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/webhooks/serviceConversationListener.protected.ts b/functions/webhooks/serviceConversationListener.protected.ts index 0f4b86f9..67906b0c 100644 --- a/functions/webhooks/serviceConversationListener.protected.ts +++ b/functions/webhooks/serviceConversationListener.protected.ts @@ -67,7 +67,7 @@ export const handler = async (context: Context, event: Body, callback: Serverles try { const { Body, EventType, Source } = event; - if (EventType === 'onMessageSent') { + if (EventType === 'onMessageAdded') { console.log('EventType is here', Body, Source, event); } } catch (err) { From 4585afd8dac49a4070dc9ec3922b4be61d957571 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Fri, 19 Jul 2024 09:47:39 +0100 Subject: [PATCH 03/20] ch: log messages --- .../serviceConversationListener.protected.ts | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/functions/webhooks/serviceConversationListener.protected.ts b/functions/webhooks/serviceConversationListener.protected.ts index 67906b0c..60c6819c 100644 --- a/functions/webhooks/serviceConversationListener.protected.ts +++ b/functions/webhooks/serviceConversationListener.protected.ts @@ -56,7 +56,9 @@ type ServiceConversationListenerEvent = { ParticipantSid?: string; ConversationSid: string; EventType: string; - Source: string; + MessageSid: string; + Attributes: any; + From: string; }; export type Body = ServiceConversationListenerEvent; @@ -65,10 +67,26 @@ export const handler = async (context: Context, event: Body, callback: Serverles const response = responseWithCors(); const resolve = bindResolve(callback)(response); try { - const { Body, EventType, Source } = event; + const { Body, EventType, ConversationSid, MessageSid } = event; if (EventType === 'onMessageAdded') { - console.log('EventType is here', Body, Source, event); + console.log('EventType is here', Body, MessageSid, ConversationSid, event); + + context + .getTwilioClient() + .conversations.v1.conversations(ConversationSid) + .messages(MessageSid) + .fetch() + .then((message) => { + console.log('Message Body:', message.body); + console.log('message:', message); + console.log('Media:', message.media); + console.log('Date Created:', message.dateCreated); + console.log('Date Updated:', message.dateUpdated); + }) + .catch((error) => { + console.error('Error fetching message:', error); + }); } } catch (err) { if (err instanceof Error) resolve(error500(err)); From 709e143018a9e0b7bffe96bf43772eb84f5a1701 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Fri, 19 Jul 2024 10:02:07 +0100 Subject: [PATCH 04/20] ch: log participantSid and author --- .../webhooks/serviceConversationListener.protected.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/functions/webhooks/serviceConversationListener.protected.ts b/functions/webhooks/serviceConversationListener.protected.ts index 60c6819c..a0ac9cf2 100644 --- a/functions/webhooks/serviceConversationListener.protected.ts +++ b/functions/webhooks/serviceConversationListener.protected.ts @@ -67,10 +67,10 @@ export const handler = async (context: Context, event: Body, callback: Serverles const response = responseWithCors(); const resolve = bindResolve(callback)(response); try { - const { Body, EventType, ConversationSid, MessageSid } = event; + const { Author, EventType, ConversationSid, MessageSid } = event; if (EventType === 'onMessageAdded') { - console.log('EventType is here', Body, MessageSid, ConversationSid, event); + console.log('EventType is here', Author, event); context .getTwilioClient() @@ -81,8 +81,8 @@ export const handler = async (context: Context, event: Body, callback: Serverles console.log('Message Body:', message.body); console.log('message:', message); console.log('Media:', message.media); - console.log('Date Created:', message.dateCreated); - console.log('Date Updated:', message.dateUpdated); + console.log('participantSid:', message.participantSid); + console.log('Author:', Author); }) .catch((error) => { console.error('Error fetching message:', error); From 0665f8a93d15f555e9e2d33d9fb69fdc16649912 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Fri, 19 Jul 2024 10:13:24 +0100 Subject: [PATCH 05/20] ch: log AuthorSid --- .../webhooks/serviceConversationListener.protected.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/functions/webhooks/serviceConversationListener.protected.ts b/functions/webhooks/serviceConversationListener.protected.ts index a0ac9cf2..5768ecc5 100644 --- a/functions/webhooks/serviceConversationListener.protected.ts +++ b/functions/webhooks/serviceConversationListener.protected.ts @@ -52,7 +52,7 @@ export const sendConversationMessage = async ( type ServiceConversationListenerEvent = { Body: string; - Author: string; + AuthorSid: string; ParticipantSid?: string; ConversationSid: string; EventType: string; @@ -67,10 +67,10 @@ export const handler = async (context: Context, event: Body, callback: Serverles const response = responseWithCors(); const resolve = bindResolve(callback)(response); try { - const { Author, EventType, ConversationSid, MessageSid } = event; + const { AuthorSid, EventType, ConversationSid, MessageSid, ParticipantSid } = event; if (EventType === 'onMessageAdded') { - console.log('EventType is here', Author, event); + console.log('EventType is here', AuthorSid, event); context .getTwilioClient() @@ -82,7 +82,8 @@ export const handler = async (context: Context, event: Body, callback: Serverles console.log('message:', message); console.log('Media:', message.media); console.log('participantSid:', message.participantSid); - console.log('Author:', Author); + console.log('AuthorSid:', AuthorSid); + console.log('ParticipantSid Cap:', ParticipantSid); }) .catch((error) => { console.error('Error fetching message:', error); From 3200599bb02acdefe3d3c26b082296f949e4f963 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Fri, 19 Jul 2024 10:43:41 +0100 Subject: [PATCH 06/20] ch: cleanup code for final test --- .../serviceConversationListener.protected.ts | 75 ++++++++++--------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/functions/webhooks/serviceConversationListener.protected.ts b/functions/webhooks/serviceConversationListener.protected.ts index 5768ecc5..86082868 100644 --- a/functions/webhooks/serviceConversationListener.protected.ts +++ b/functions/webhooks/serviceConversationListener.protected.ts @@ -15,16 +15,21 @@ */ import { Context, ServerlessCallback } from '@twilio-labs/serverless-runtime-types/types'; -import { - responseWithCors, - bindResolve, - // error400, - error500, - // success, -} from '@tech-matters/serverless-helpers'; +import { responseWithCors, bindResolve, error500 } from '@tech-matters/serverless-helpers'; export type ConversationSid = `CH${string}`; -export type ChatChannelSid = `CH${string}`; +export type ParticipantSid = `MB${string}`; + +type ServiceConversationListenerEvent = { + Body: string; + Author: string; + ParticipantSid: ParticipantSid; + ConversationSid: ConversationSid; + EventType: string; + MessageSid: string; +}; + +export type Body = ServiceConversationListenerEvent; export const sendConversationMessage = async ( context: Context, @@ -50,44 +55,46 @@ export const sendConversationMessage = async ( ...(messageAttributes && { attributes: messageAttributes }), }); -type ServiceConversationListenerEvent = { - Body: string; - AuthorSid: string; - ParticipantSid?: string; - ConversationSid: string; - EventType: string; - MessageSid: string; - Attributes: any; - From: string; -}; +const getTimeFromDate = async (isoString: Date): Promise => { + // Create a new Date object from the ISO string + const date = new Date(isoString); -export type Body = ServiceConversationListenerEvent; + // Extract the hours, minutes, and seconds + const hours = date.getUTCHours().toString().padStart(2, '0'); + const minutes = date.getUTCMinutes().toString().padStart(2, '0'); + const seconds = date.getUTCSeconds().toString().padStart(2, '0'); + + // Return the time string in HH:MM:SS format + return `${hours}:${minutes}:${seconds}`; +}; export const handler = async (context: Context, event: Body, callback: ServerlessCallback) => { const response = responseWithCors(); const resolve = bindResolve(callback)(response); try { - const { AuthorSid, EventType, ConversationSid, MessageSid, ParticipantSid } = event; + const { Author, EventType, ConversationSid, MessageSid, ParticipantSid, Body } = event; if (EventType === 'onMessageAdded') { - console.log('EventType is here', AuthorSid, event); - - context + const conversationMessage = await context .getTwilioClient() .conversations.v1.conversations(ConversationSid) .messages(MessageSid) - .fetch() - .then((message) => { - console.log('Message Body:', message.body); - console.log('message:', message); - console.log('Media:', message.media); - console.log('participantSid:', message.participantSid); - console.log('AuthorSid:', AuthorSid); - console.log('ParticipantSid Cap:', ParticipantSid); - }) - .catch((error) => { - console.error('Error fetching message:', error); + .fetch(); + + if ( + ParticipantSid === conversationMessage.participantSid && + !conversationMessage.media && + !Body + ) { + const messageTime = await getTimeFromDate(conversationMessage.dateCreated); + const messageText = `Sorry, your reaction sent at ${messageTime} could not be delivered.`; + + await sendConversationMessage(context, { + conversationSid: ConversationSid, + author: Author, + messageText, }); + } } } catch (err) { if (err instanceof Error) resolve(error500(err)); From a1c8aead8728a3d2dd45ca67c9138fd4bafc73f0 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Fri, 19 Jul 2024 11:18:08 +0100 Subject: [PATCH 07/20] ch: log paricipants --- .../serviceConversationListener.protected.ts | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/functions/webhooks/serviceConversationListener.protected.ts b/functions/webhooks/serviceConversationListener.protected.ts index 86082868..e8039041 100644 --- a/functions/webhooks/serviceConversationListener.protected.ts +++ b/functions/webhooks/serviceConversationListener.protected.ts @@ -60,9 +60,9 @@ const getTimeFromDate = async (isoString: Date): Promise => { const date = new Date(isoString); // Extract the hours, minutes, and seconds - const hours = date.getUTCHours().toString().padStart(2, '0'); - const minutes = date.getUTCMinutes().toString().padStart(2, '0'); - const seconds = date.getUTCSeconds().toString().padStart(2, '0'); + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + const seconds = date.getSeconds().toString().padStart(2, '0'); // Return the time string in HH:MM:SS format return `${hours}:${minutes}:${seconds}`; @@ -81,6 +81,24 @@ export const handler = async (context: Context, event: Body, callback: Serverles .messages(MessageSid) .fetch(); + context + .getTwilioClient() + .conversations.v1.conversations(ConversationSid) + .participants.list() + .then((participants) => { + console.log('participants are here', participants); + participants.forEach((participant) => { + console.log('Participant:', participant); + console.log('Participant Identity:', participant.identity); + console.log('Participant Attributes:', participant.attributes); + console.log('Date Created:', participant.dateCreated); + console.log('Date Updated:', participant.dateUpdated); + }); + }) + .catch((error) => { + console.error('Error fetching participants:', error); + }); + if ( ParticipantSid === conversationMessage.participantSid && !conversationMessage.media && From c8fb4a91c13dcc48b4a0f715266398fbfe867c2a Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Fri, 19 Jul 2024 11:47:49 +0100 Subject: [PATCH 08/20] ch: log paricipants-2 --- .../serviceConversationListener.protected.ts | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/functions/webhooks/serviceConversationListener.protected.ts b/functions/webhooks/serviceConversationListener.protected.ts index e8039041..05255b41 100644 --- a/functions/webhooks/serviceConversationListener.protected.ts +++ b/functions/webhooks/serviceConversationListener.protected.ts @@ -74,6 +74,8 @@ export const handler = async (context: Context, event: Body, callback: Serverles try { const { Author, EventType, ConversationSid, MessageSid, ParticipantSid, Body } = event; + let messageAuthor: string | undefined; + if (EventType === 'onMessageAdded') { const conversationMessage = await context .getTwilioClient() @@ -81,23 +83,16 @@ export const handler = async (context: Context, event: Body, callback: Serverles .messages(MessageSid) .fetch(); - context + const participantsList = await context .getTwilioClient() .conversations.v1.conversations(ConversationSid) - .participants.list() - .then((participants) => { - console.log('participants are here', participants); - participants.forEach((participant) => { - console.log('Participant:', participant); - console.log('Participant Identity:', participant.identity); - console.log('Participant Attributes:', participant.attributes); - console.log('Date Created:', participant.dateCreated); - console.log('Date Updated:', participant.dateUpdated); - }); - }) - .catch((error) => { - console.error('Error fetching participants:', error); - }); + .participants.list(); + + participantsList.forEach((participant) => { + if (participant.sid !== conversationMessage.participantSid) { + messageAuthor = participant.identity; + } + }); if ( ParticipantSid === conversationMessage.participantSid && @@ -107,9 +102,11 @@ export const handler = async (context: Context, event: Body, callback: Serverles const messageTime = await getTimeFromDate(conversationMessage.dateCreated); const messageText = `Sorry, your reaction sent at ${messageTime} could not be delivered.`; + console.log('Author', Author, messageAuthor); + await sendConversationMessage(context, { conversationSid: ConversationSid, - author: Author, + author: messageAuthor ?? '', messageText, }); } From 35f0b33f7bd88d715bca5d53166458760316ea17 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Fri, 19 Jul 2024 12:01:46 +0100 Subject: [PATCH 09/20] ch: final cleanup --- .../serviceConversationListener.protected.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/functions/webhooks/serviceConversationListener.protected.ts b/functions/webhooks/serviceConversationListener.protected.ts index 05255b41..833fd48c 100644 --- a/functions/webhooks/serviceConversationListener.protected.ts +++ b/functions/webhooks/serviceConversationListener.protected.ts @@ -22,7 +22,6 @@ export type ParticipantSid = `MB${string}`; type ServiceConversationListenerEvent = { Body: string; - Author: string; ParticipantSid: ParticipantSid; ConversationSid: ConversationSid; EventType: string; @@ -59,23 +58,27 @@ const getTimeFromDate = async (isoString: Date): Promise => { // Create a new Date object from the ISO string const date = new Date(isoString); - // Extract the hours, minutes, and seconds - const hours = date.getHours().toString().padStart(2, '0'); - const minutes = date.getMinutes().toString().padStart(2, '0'); - const seconds = date.getSeconds().toString().padStart(2, '0'); + // Extract the local hours, minutes, and seconds + const hours = date.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }); // Return the time string in HH:MM:SS format - return `${hours}:${minutes}:${seconds}`; + return hours; }; export const handler = async (context: Context, event: Body, callback: ServerlessCallback) => { const response = responseWithCors(); const resolve = bindResolve(callback)(response); try { - const { Author, EventType, ConversationSid, MessageSid, ParticipantSid, Body } = event; + const { EventType, ConversationSid, MessageSid, ParticipantSid, Body } = event; let messageAuthor: string | undefined; + // check if it's the onMessageAdded event if (EventType === 'onMessageAdded') { const conversationMessage = await context .getTwilioClient() @@ -90,6 +93,7 @@ export const handler = async (context: Context, event: Body, callback: Serverles participantsList.forEach((participant) => { if (participant.sid !== conversationMessage.participantSid) { + // The message author has to be the participant receiving the message (counsellor) not the sender messageAuthor = participant.identity; } }); @@ -102,8 +106,6 @@ export const handler = async (context: Context, event: Body, callback: Serverles const messageTime = await getTimeFromDate(conversationMessage.dateCreated); const messageText = `Sorry, your reaction sent at ${messageTime} could not be delivered.`; - console.log('Author', Author, messageAuthor); - await sendConversationMessage(context, { conversationSid: ConversationSid, author: messageAuthor ?? '', From 0e771d363e5f397f85093bb92aee83abe80eee05 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Fri, 19 Jul 2024 12:24:24 +0100 Subject: [PATCH 10/20] ch: refactor getTimeDifference --- .../serviceConversationListener.protected.ts | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/functions/webhooks/serviceConversationListener.protected.ts b/functions/webhooks/serviceConversationListener.protected.ts index 833fd48c..a894d0be 100644 --- a/functions/webhooks/serviceConversationListener.protected.ts +++ b/functions/webhooks/serviceConversationListener.protected.ts @@ -54,20 +54,25 @@ export const sendConversationMessage = async ( ...(messageAttributes && { attributes: messageAttributes }), }); -const getTimeFromDate = async (isoString: Date): Promise => { +const getTimeDifference = async (isoString: Date): Promise => { // Create a new Date object from the ISO string - const date = new Date(isoString); - - // Extract the local hours, minutes, and seconds - const hours = date.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false, - }); - - // Return the time string in HH:MM:SS format - return hours; + const givenDate = new Date(isoString); + + // Get the current date and time + const currentDate = new Date(); + + // Calculate the difference in milliseconds + const differenceInMillis = currentDate.getTime() - givenDate.getTime(); + + // Convert the difference to seconds + const differenceInSeconds = Math.floor(differenceInMillis / 1000); + const differenceInMinutes = Math.floor(differenceInSeconds / 60); + + // Determine whether to return the difference in seconds or minutes + + return differenceInSeconds < 60 + ? `${differenceInSeconds} seconds` + : `${differenceInMinutes} minutes`; }; export const handler = async (context: Context, event: Body, callback: ServerlessCallback) => { @@ -103,8 +108,8 @@ export const handler = async (context: Context, event: Body, callback: Serverles !conversationMessage.media && !Body ) { - const messageTime = await getTimeFromDate(conversationMessage.dateCreated); - const messageText = `Sorry, your reaction sent at ${messageTime} could not be delivered.`; + const messageTime = await getTimeDifference(conversationMessage.dateCreated); + const messageText = `Sorry, your reaction sent ${messageTime} could not be delivered.`; await sendConversationMessage(context, { conversationSid: ConversationSid, From f14948f0650c1f4f8f40ba0729409d221beda188 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Fri, 19 Jul 2024 12:51:20 +0100 Subject: [PATCH 11/20] ch: refactor getTimeDifference-2 --- functions/webhooks/serviceConversationListener.protected.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/functions/webhooks/serviceConversationListener.protected.ts b/functions/webhooks/serviceConversationListener.protected.ts index a894d0be..96b15bc6 100644 --- a/functions/webhooks/serviceConversationListener.protected.ts +++ b/functions/webhooks/serviceConversationListener.protected.ts @@ -55,6 +55,7 @@ export const sendConversationMessage = async ( }); const getTimeDifference = async (isoString: Date): Promise => { + const unitCheck = (count: number, unit: string) => (count > 1 ? `${unit}s` : unit); // Create a new Date object from the ISO string const givenDate = new Date(isoString); @@ -69,10 +70,9 @@ const getTimeDifference = async (isoString: Date): Promise => { const differenceInMinutes = Math.floor(differenceInSeconds / 60); // Determine whether to return the difference in seconds or minutes - return differenceInSeconds < 60 - ? `${differenceInSeconds} seconds` - : `${differenceInMinutes} minutes`; + ? `${differenceInSeconds} ${unitCheck(differenceInSeconds, 'second')}` + : `${differenceInMinutes} ${unitCheck(differenceInMinutes, 'minute')}`; }; export const handler = async (context: Context, event: Body, callback: ServerlessCallback) => { From 6865b628ab365c234eb7ad1fa3c5139fed91ab7c Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Fri, 19 Jul 2024 13:33:42 +0100 Subject: [PATCH 12/20] ch: final correction for PR --- functions/webhooks/serviceConversationListener.protected.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/webhooks/serviceConversationListener.protected.ts b/functions/webhooks/serviceConversationListener.protected.ts index 96b15bc6..9d6f607a 100644 --- a/functions/webhooks/serviceConversationListener.protected.ts +++ b/functions/webhooks/serviceConversationListener.protected.ts @@ -109,7 +109,7 @@ export const handler = async (context: Context, event: Body, callback: Serverles !Body ) { const messageTime = await getTimeDifference(conversationMessage.dateCreated); - const messageText = `Sorry, your reaction sent ${messageTime} could not be delivered.`; + const messageText = `Sorry, your reaction sent ${messageTime} ago could not be delivered.`; await sendConversationMessage(context, { conversationSid: ConversationSid, From 8e9d3d5a4a9478788555b791e1b6e87d74fae359 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Fri, 19 Jul 2024 13:39:03 +0100 Subject: [PATCH 13/20] ch: add more message text --- functions/webhooks/serviceConversationListener.protected.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/webhooks/serviceConversationListener.protected.ts b/functions/webhooks/serviceConversationListener.protected.ts index 9d6f607a..789bc1e5 100644 --- a/functions/webhooks/serviceConversationListener.protected.ts +++ b/functions/webhooks/serviceConversationListener.protected.ts @@ -109,7 +109,7 @@ export const handler = async (context: Context, event: Body, callback: Serverles !Body ) { const messageTime = await getTimeDifference(conversationMessage.dateCreated); - const messageText = `Sorry, your reaction sent ${messageTime} ago could not be delivered.`; + const messageText = `Sorry, your reaction sent ${messageTime} ago could not be delivered. Please send another message.`; await sendConversationMessage(context, { conversationSid: ConversationSid, From 10fc0a1e4397c3e334b27aaa92f3bda287cb51a4 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Mon, 22 Jul 2024 10:42:01 +0100 Subject: [PATCH 14/20] ch: log more event attributes --- .../serviceConversationListener.protected.ts | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/functions/webhooks/serviceConversationListener.protected.ts b/functions/webhooks/serviceConversationListener.protected.ts index 789bc1e5..2525dd44 100644 --- a/functions/webhooks/serviceConversationListener.protected.ts +++ b/functions/webhooks/serviceConversationListener.protected.ts @@ -26,6 +26,10 @@ type ServiceConversationListenerEvent = { ConversationSid: ConversationSid; EventType: string; MessageSid: string; + Media: any; + DateCreated: any; + Participants: any; + ParticipantsList: any; }; export type Body = ServiceConversationListenerEvent; @@ -79,7 +83,30 @@ export const handler = async (context: Context, event: Body, callback: Serverles const response = responseWithCors(); const resolve = bindResolve(callback)(response); try { - const { EventType, ConversationSid, MessageSid, ParticipantSid, Body } = event; + const { + EventType, + ConversationSid, + MessageSid, + ParticipantSid, + Body, + Media, + DateCreated, + Participants, + ParticipantsList, + } = event; + + console.log( + 'Event', + EventType, + ConversationSid, + MessageSid, + ParticipantSid, + Body, + Media, + DateCreated, + Participants, + ParticipantsList, + ); let messageAuthor: string | undefined; From 48123d4a8b7c70a75f7f2f9fe85421c2a0a0d02b Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Mon, 22 Jul 2024 11:17:46 +0100 Subject: [PATCH 15/20] ch: use Bot as author --- .../serviceConversationListener.protected.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/functions/webhooks/serviceConversationListener.protected.ts b/functions/webhooks/serviceConversationListener.protected.ts index 2525dd44..569c1183 100644 --- a/functions/webhooks/serviceConversationListener.protected.ts +++ b/functions/webhooks/serviceConversationListener.protected.ts @@ -108,7 +108,7 @@ export const handler = async (context: Context, event: Body, callback: Serverles ParticipantsList, ); - let messageAuthor: string | undefined; + // let messageAuthor: string | undefined; // check if it's the onMessageAdded event if (EventType === 'onMessageAdded') { @@ -118,17 +118,17 @@ export const handler = async (context: Context, event: Body, callback: Serverles .messages(MessageSid) .fetch(); - const participantsList = await context - .getTwilioClient() - .conversations.v1.conversations(ConversationSid) - .participants.list(); + // const participantsList = await context + // .getTwilioClient() + // .conversations.v1.conversations(ConversationSid) + // .participants.list(); - participantsList.forEach((participant) => { - if (participant.sid !== conversationMessage.participantSid) { - // The message author has to be the participant receiving the message (counsellor) not the sender - messageAuthor = participant.identity; - } - }); + // participantsList.forEach((participant) => { + // if (participant.sid !== conversationMessage.participantSid) { + // // The message author has to be the participant receiving the message (counsellor) not the sender + // messageAuthor = participant.identity; + // } + // }); if ( ParticipantSid === conversationMessage.participantSid && @@ -140,7 +140,7 @@ export const handler = async (context: Context, event: Body, callback: Serverles await sendConversationMessage(context, { conversationSid: ConversationSid, - author: messageAuthor ?? '', + author: 'Bot', messageText, }); } From ac14473439accc7443c3dac00107616860da88b9 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Mon, 22 Jul 2024 12:26:49 +0100 Subject: [PATCH 16/20] ch: add participantSid to sendConversationMessage --- .../serviceConversationListener.protected.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/functions/webhooks/serviceConversationListener.protected.ts b/functions/webhooks/serviceConversationListener.protected.ts index 569c1183..d3d7f54d 100644 --- a/functions/webhooks/serviceConversationListener.protected.ts +++ b/functions/webhooks/serviceConversationListener.protected.ts @@ -41,11 +41,13 @@ export const sendConversationMessage = async ( author, messageText, messageAttributes, + participantSid, }: { conversationSid: ConversationSid; author: string; messageText: string; - messageAttributes?: string; + messageAttributes?: Record; + participantSid: ParticipantSid; }, ) => context @@ -55,7 +57,12 @@ export const sendConversationMessage = async ( body: messageText, author, xTwilioWebhookEnabled: 'true', - ...(messageAttributes && { attributes: messageAttributes }), + ...(messageAttributes && { + attributes: JSON.stringify({ + ...(messageAttributes || {}), + participantSid, + }), + }), }); const getTimeDifference = async (isoString: Date): Promise => { @@ -142,6 +149,7 @@ export const handler = async (context: Context, event: Body, callback: Serverles conversationSid: ConversationSid, author: 'Bot', messageText, + participantSid: ParticipantSid, }); } } From 4f580eeb359b70a833aca24459a08535ef830732 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Mon, 22 Jul 2024 16:07:32 +0100 Subject: [PATCH 17/20] ch: create sendErrorMessageForUnsupportedMedia and separate codes --- ...ErrorMessageForUnsupportedMedia.private.ts | 101 ++++++++++++ .../serviceConversationListener.protected.ts | 144 ++---------------- 2 files changed, 112 insertions(+), 133 deletions(-) create mode 100644 functions/helpers/sendErrorMessageForUnsupportedMedia.private.ts diff --git a/functions/helpers/sendErrorMessageForUnsupportedMedia.private.ts b/functions/helpers/sendErrorMessageForUnsupportedMedia.private.ts new file mode 100644 index 00000000..a879e6e7 --- /dev/null +++ b/functions/helpers/sendErrorMessageForUnsupportedMedia.private.ts @@ -0,0 +1,101 @@ +/** + * 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 { Context } from '@twilio-labs/serverless-runtime-types/types'; + +export type ConversationSid = `CH${string}`; +export type ParticipantSid = `MB${string}`; + +type SendErrorMessageForUnsupportedMediaEvent = { + Body: string; + ParticipantSid: ParticipantSid; + ConversationSid: ConversationSid; + EventType: string; + Media: Record; + DateCreated: Date; +}; + +export type Event = SendErrorMessageForUnsupportedMediaEvent; + +export const sendConversationMessage = async ( + context: Context, + { + conversationSid, + author, + messageText, + messageAttributes, + participantSid, + }: { + conversationSid: ConversationSid; + author: string; + messageText: string; + messageAttributes?: Record; + participantSid: ParticipantSid; + }, +) => + context + .getTwilioClient() + .conversations.conversations(conversationSid) + .messages.create({ + body: messageText, + author, + xTwilioWebhookEnabled: 'true', + ...(messageAttributes && { + attributes: JSON.stringify({ + ...(messageAttributes || {}), + participantSid, + }), + }), + }); + +const getTimeDifference = async (isoString: Date): Promise => { + const unitCheck = (count: number, unit: string) => (count > 1 ? `${unit}s` : unit); + // Create a new Date object from the ISO string + const givenDate = new Date(isoString); + + // Get the current date and time + const currentDate = new Date(); + + // Calculate the difference in milliseconds + const differenceInMillis = currentDate.getTime() - givenDate.getTime(); + + // Convert the difference to seconds + const differenceInSeconds = Math.floor(differenceInMillis / 1000); + const differenceInMinutes = Math.floor(differenceInSeconds / 60); + + // Determine whether to return the difference in seconds or minutes + return differenceInSeconds < 60 + ? `${differenceInSeconds} ${unitCheck(differenceInSeconds, 'second')}` + : `${differenceInMinutes} ${unitCheck(differenceInMinutes, 'minute')}`; +}; + +export const sendErrorMessageForUnsupportedMedia = async (context: Context, event: Event) => { + const { EventType, Body, Media, ConversationSid, ParticipantSid, DateCreated } = event; + + if (EventType === 'onMessageAdded' && !Body && !Media) { + const messageTime = await getTimeDifference(DateCreated); + const messageText = `Sorry, your reaction sent ${messageTime} ago could not be delivered. Please send another message.`; + + await sendConversationMessage(context, { + conversationSid: ConversationSid, + author: 'Bot', + messageText, + participantSid: ParticipantSid, + }); + } +}; + +export type SendErrorMessageForUnsupportedMedia = typeof sendErrorMessageForUnsupportedMedia; diff --git a/functions/webhooks/serviceConversationListener.protected.ts b/functions/webhooks/serviceConversationListener.protected.ts index d3d7f54d..5d37b44e 100644 --- a/functions/webhooks/serviceConversationListener.protected.ts +++ b/functions/webhooks/serviceConversationListener.protected.ts @@ -16,143 +16,21 @@ import { Context, ServerlessCallback } from '@twilio-labs/serverless-runtime-types/types'; import { responseWithCors, bindResolve, error500 } from '@tech-matters/serverless-helpers'; +import { + Event, + SendErrorMessageForUnsupportedMedia, +} from '../helpers/sendErrorMessageForUnsupportedMedia.private'; -export type ConversationSid = `CH${string}`; -export type ParticipantSid = `MB${string}`; - -type ServiceConversationListenerEvent = { - Body: string; - ParticipantSid: ParticipantSid; - ConversationSid: ConversationSid; - EventType: string; - MessageSid: string; - Media: any; - DateCreated: any; - Participants: any; - ParticipantsList: any; -}; - -export type Body = ServiceConversationListenerEvent; - -export const sendConversationMessage = async ( - context: Context, - { - conversationSid, - author, - messageText, - messageAttributes, - participantSid, - }: { - conversationSid: ConversationSid; - author: string; - messageText: string; - messageAttributes?: Record; - participantSid: ParticipantSid; - }, -) => - context - .getTwilioClient() - .conversations.conversations.get(conversationSid) - .messages.create({ - body: messageText, - author, - xTwilioWebhookEnabled: 'true', - ...(messageAttributes && { - attributes: JSON.stringify({ - ...(messageAttributes || {}), - participantSid, - }), - }), - }); - -const getTimeDifference = async (isoString: Date): Promise => { - const unitCheck = (count: number, unit: string) => (count > 1 ? `${unit}s` : unit); - // Create a new Date object from the ISO string - const givenDate = new Date(isoString); - - // Get the current date and time - const currentDate = new Date(); - - // Calculate the difference in milliseconds - const differenceInMillis = currentDate.getTime() - givenDate.getTime(); - - // Convert the difference to seconds - const differenceInSeconds = Math.floor(differenceInMillis / 1000); - const differenceInMinutes = Math.floor(differenceInSeconds / 60); - - // Determine whether to return the difference in seconds or minutes - return differenceInSeconds < 60 - ? `${differenceInSeconds} ${unitCheck(differenceInSeconds, 'second')}` - : `${differenceInMinutes} ${unitCheck(differenceInMinutes, 'minute')}`; -}; - -export const handler = async (context: Context, event: Body, callback: ServerlessCallback) => { +export const handler = async (context: Context, event: Event, callback: ServerlessCallback) => { const response = responseWithCors(); const resolve = bindResolve(callback)(response); - try { - const { - EventType, - ConversationSid, - MessageSid, - ParticipantSid, - Body, - Media, - DateCreated, - Participants, - ParticipantsList, - } = event; - - console.log( - 'Event', - EventType, - ConversationSid, - MessageSid, - ParticipantSid, - Body, - Media, - DateCreated, - Participants, - ParticipantsList, - ); - - // let messageAuthor: string | undefined; - - // check if it's the onMessageAdded event - if (EventType === 'onMessageAdded') { - const conversationMessage = await context - .getTwilioClient() - .conversations.v1.conversations(ConversationSid) - .messages(MessageSid) - .fetch(); + // eslint-disable-next-line global-require,import/no-dynamic-require + const sendErrorMessageForUnsupportedMedia = require(Runtime.getFunctions()[ + 'helpers/sendErrorMessageForUnsupportedMedia' + ].path).sendErrorMessageForUnsupportedMedia as SendErrorMessageForUnsupportedMedia; - // const participantsList = await context - // .getTwilioClient() - // .conversations.v1.conversations(ConversationSid) - // .participants.list(); - - // participantsList.forEach((participant) => { - // if (participant.sid !== conversationMessage.participantSid) { - // // The message author has to be the participant receiving the message (counsellor) not the sender - // messageAuthor = participant.identity; - // } - // }); - - if ( - ParticipantSid === conversationMessage.participantSid && - !conversationMessage.media && - !Body - ) { - const messageTime = await getTimeDifference(conversationMessage.dateCreated); - const messageText = `Sorry, your reaction sent ${messageTime} ago could not be delivered. Please send another message.`; - - await sendConversationMessage(context, { - conversationSid: ConversationSid, - author: 'Bot', - messageText, - participantSid: ParticipantSid, - }); - } - } + try { + await sendErrorMessageForUnsupportedMedia(context, event); } catch (err) { if (err instanceof Error) resolve(error500(err)); else resolve(error500(new Error(String(err)))); From 287fb719618d12903060113ccb24f6c459e8a18d Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Mon, 22 Jul 2024 17:17:28 +0100 Subject: [PATCH 18/20] ch: further code clean up --- ...dErrorMessageForUnsupportedMedia.private.ts | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/functions/helpers/sendErrorMessageForUnsupportedMedia.private.ts b/functions/helpers/sendErrorMessageForUnsupportedMedia.private.ts index a879e6e7..fe541f39 100644 --- a/functions/helpers/sendErrorMessageForUnsupportedMedia.private.ts +++ b/functions/helpers/sendErrorMessageForUnsupportedMedia.private.ts @@ -17,11 +17,9 @@ import { Context } from '@twilio-labs/serverless-runtime-types/types'; export type ConversationSid = `CH${string}`; -export type ParticipantSid = `MB${string}`; type SendErrorMessageForUnsupportedMediaEvent = { Body: string; - ParticipantSid: ParticipantSid; ConversationSid: ConversationSid; EventType: string; Media: Record; @@ -37,28 +35,21 @@ export const sendConversationMessage = async ( author, messageText, messageAttributes, - participantSid, }: { conversationSid: ConversationSid; author: string; messageText: string; - messageAttributes?: Record; - participantSid: ParticipantSid; + messageAttributes?: string; }, ) => context .getTwilioClient() - .conversations.conversations(conversationSid) + .conversations.conversations.get(conversationSid) .messages.create({ body: messageText, author, xTwilioWebhookEnabled: 'true', - ...(messageAttributes && { - attributes: JSON.stringify({ - ...(messageAttributes || {}), - participantSid, - }), - }), + ...(messageAttributes && { attributes: messageAttributes }), }); const getTimeDifference = async (isoString: Date): Promise => { @@ -83,7 +74,7 @@ const getTimeDifference = async (isoString: Date): Promise => { }; export const sendErrorMessageForUnsupportedMedia = async (context: Context, event: Event) => { - const { EventType, Body, Media, ConversationSid, ParticipantSid, DateCreated } = event; + const { EventType, Body, Media, ConversationSid, DateCreated } = event; if (EventType === 'onMessageAdded' && !Body && !Media) { const messageTime = await getTimeDifference(DateCreated); @@ -93,7 +84,6 @@ export const sendErrorMessageForUnsupportedMedia = async (context: Context, even conversationSid: ConversationSid, author: 'Bot', messageText, - participantSid: ParticipantSid, }); } }; From 98354b1c6e9613f86c8af689fdc49552b0a52928 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Wed, 24 Jul 2024 15:22:48 +0100 Subject: [PATCH 19/20] ch: testing for line --- .../helpers/sendErrorMessageForUnsupportedMedia.private.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/functions/helpers/sendErrorMessageForUnsupportedMedia.private.ts b/functions/helpers/sendErrorMessageForUnsupportedMedia.private.ts index fe541f39..61af869d 100644 --- a/functions/helpers/sendErrorMessageForUnsupportedMedia.private.ts +++ b/functions/helpers/sendErrorMessageForUnsupportedMedia.private.ts @@ -19,10 +19,10 @@ import { Context } from '@twilio-labs/serverless-runtime-types/types'; export type ConversationSid = `CH${string}`; type SendErrorMessageForUnsupportedMediaEvent = { - Body: string; + Body?: string; ConversationSid: ConversationSid; - EventType: string; - Media: Record; + EventType?: string; + Media?: Record; DateCreated: Date; }; @@ -77,6 +77,7 @@ export const sendErrorMessageForUnsupportedMedia = async (context: Context, even const { EventType, Body, Media, ConversationSid, DateCreated } = event; if (EventType === 'onMessageAdded' && !Body && !Media) { + console.log('onMessageAdded is here', Media, Body, event); const messageTime = await getTimeDifference(DateCreated); const messageText = `Sorry, your reaction sent ${messageTime} ago could not be delivered. Please send another message.`; From a6c6297fe340b95c74bb7533f40cc7cf1627d143 Mon Sep 17 00:00:00 2001 From: Stephen Okpalaononuju Date: Wed, 24 Jul 2024 17:04:51 +0100 Subject: [PATCH 20/20] ch: modify message text and add comments --- .../helpers/sendErrorMessageForUnsupportedMedia.private.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/functions/helpers/sendErrorMessageForUnsupportedMedia.private.ts b/functions/helpers/sendErrorMessageForUnsupportedMedia.private.ts index 61af869d..5663cd78 100644 --- a/functions/helpers/sendErrorMessageForUnsupportedMedia.private.ts +++ b/functions/helpers/sendErrorMessageForUnsupportedMedia.private.ts @@ -76,10 +76,12 @@ const getTimeDifference = async (isoString: Date): Promise => { export const sendErrorMessageForUnsupportedMedia = async (context: Context, event: Event) => { const { EventType, Body, Media, ConversationSid, DateCreated } = event; + /* Valid message will have either a body/media. A message with no + body or media implies that there was an error sending such message + */ if (EventType === 'onMessageAdded' && !Body && !Media) { - console.log('onMessageAdded is here', Media, Body, event); const messageTime = await getTimeDifference(DateCreated); - const messageText = `Sorry, your reaction sent ${messageTime} ago could not be delivered. Please send another message.`; + const messageText = `Sorry, the message sent ${messageTime} ago is unsupported and could not be delivered.`; await sendConversationMessage(context, { conversationSid: ConversationSid,