diff --git a/bot/connector-whatsapp-cloud/src/main/kotlin/WhatsAppCloudApiClient.kt b/bot/connector-whatsapp-cloud/src/main/kotlin/WhatsAppCloudApiClient.kt index 95ca5507b2..4c64fc0b96 100644 --- a/bot/connector-whatsapp-cloud/src/main/kotlin/WhatsAppCloudApiClient.kt +++ b/bot/connector-whatsapp-cloud/src/main/kotlin/WhatsAppCloudApiClient.kt @@ -33,7 +33,7 @@ import retrofit2.http.Headers private const val VERSION = "19.0" -class WhatsAppCloudApiClient(val secretKey: String, val token: String) { +class WhatsAppCloudApiClient(val secretKey: String, val token: String, val phoneNumber: String) { interface GraphApi { diff --git a/bot/connector-whatsapp-cloud/src/main/kotlin/WhatsAppCloudApiClientRepository.kt b/bot/connector-whatsapp-cloud/src/main/kotlin/WhatsAppCloudApiClientRepository.kt index 3fe82d3209..fb8a3bfea8 100644 --- a/bot/connector-whatsapp-cloud/src/main/kotlin/WhatsAppCloudApiClientRepository.kt +++ b/bot/connector-whatsapp-cloud/src/main/kotlin/WhatsAppCloudApiClientRepository.kt @@ -19,6 +19,7 @@ package ai.tock.bot.connector.whatsapp.cloud import ai.tock.bot.connector.ConnectorConfiguration import ai.tock.bot.connector.whatsapp.cloud.WhatsAppConnectorCloudProvider.SECRET import ai.tock.bot.connector.whatsapp.cloud.WhatsAppConnectorCloudProvider.TOKEN +import ai.tock.bot.connector.whatsapp.cloud.WhatsAppConnectorCloudProvider.WHATSAPP_PHONE_NUMBER_ID import java.util.concurrent.ConcurrentHashMap private val cloudApiClientCache = ConcurrentHashMap() @@ -26,7 +27,8 @@ private val cloudApiClientCache = ConcurrentHashMap> T.sendToWhatsApp( +fun > T.sendToWhatsAppCloud( messageProvider: T.() -> WhatsAppCloudBotMessage, delay: Long = defaultDelay(currentAnswerIndex) ): T { @@ -81,7 +81,7 @@ fun > T.withWhatsAppCloud(messageProvider: () -> WhatsAppCloudConnect * @param text the text sent * @param previewUrl is preview mode is used? */ -fun BotBus.whatsAppText( +fun BotBus.whatsAppCloudText( text: CharSequence, previewUrl: Boolean = false ): WhatsAppCloudBotTextMessage = @@ -92,16 +92,17 @@ fun BotBus.whatsAppText( userId = userId.id, ) -fun I18nTranslator.replyButtonMessage( +fun I18nTranslator.whatsAppCloudReplyButtonMessage( text: CharSequence, vararg replies: QuickReply, - ) : WhatsAppCloudBotInteractiveMessage = replyButtonMessage(text, replies.toList()) + ): WhatsAppCloudBotInteractiveMessage = + whatsAppCloudReplyButtonMessage(text, replies.toList()) -fun I18nTranslator.replyButtonMessage( +fun I18nTranslator.whatsAppCloudReplyButtonMessage( text: CharSequence, replies: List, -) : WhatsAppCloudBotInteractiveMessage = WhatsAppCloudBotInteractiveMessage( +): WhatsAppCloudBotInteractiveMessage = WhatsAppCloudBotInteractiveMessage( messagingProduct = "whatsapp", recipientType = WhatsAppCloudBotRecipientType.individual, interactive = WhatsAppCloudBotInteractive( @@ -120,11 +121,11 @@ fun I18nTranslator.replyButtonMessage( ) ) -fun I18nTranslator.urlButtonMessage( - text: CharSequence?=null, +fun I18nTranslator.whatsAppCloudUrlButtonMessage( + text: CharSequence? = null, textButton: String, url: String -) : WhatsAppCloudBotInteractiveMessage = WhatsAppCloudBotInteractiveMessage( +): WhatsAppCloudBotInteractiveMessage = WhatsAppCloudBotInteractiveMessage( messagingProduct = "whatsapp", recipientType = WhatsAppCloudBotRecipientType.individual, interactive = WhatsAppCloudBotInteractive( @@ -140,41 +141,41 @@ fun I18nTranslator.urlButtonMessage( ) ) -fun I18nTranslator.listMessage( +fun I18nTranslator.whatsAppCloudListMessage( text: CharSequence, button: CharSequence, vararg replies: QuickReply -) : WhatsAppCloudBotInteractiveMessage = - listMessage(text, button, replies.toList()) +): WhatsAppCloudBotInteractiveMessage = + whatsAppCloudListMessage(text, button, replies.toList()) -fun I18nTranslator.listMessage( +fun I18nTranslator.whatsAppCloudListMessage( text: CharSequence, button: CharSequence, replies: List -) : WhatsAppCloudBotInteractiveMessage = - completeListMessage( +): WhatsAppCloudBotInteractiveMessage = + whatsAppCloudCompleteListMessage( text, button, WhatsAppCloudBotActionSection(rows = replies.map { - WhatsAppBotRow( - id = it.payload, - title = it.title, - ) - }) + WhatsAppBotRow( + id = it.payload, + title = it.title, + ) + }) ) -fun I18nTranslator.completeListMessage( +fun I18nTranslator.whatsAppCloudCompleteListMessage( text: CharSequence, button: CharSequence, vararg sections: WhatsAppCloudBotActionSection -) : WhatsAppCloudBotInteractiveMessage = completeListMessage(text, button, sections.toList()) +): WhatsAppCloudBotInteractiveMessage = whatsAppCloudCompleteListMessage(text, button, sections.toList()) -fun I18nTranslator.completeListMessage( +fun I18nTranslator.whatsAppCloudCompleteListMessage( text: CharSequence, button: CharSequence, sections: List, -) : WhatsAppCloudBotInteractiveMessage = WhatsAppCloudBotInteractiveMessage( +): WhatsAppCloudBotInteractiveMessage = WhatsAppCloudBotInteractiveMessage( messagingProduct = "whatsapp", recipientType = WhatsAppCloudBotRecipientType.individual, - interactive = WhatsAppCloudBotInteractive( + interactive = WhatsAppCloudBotInteractive( type = WhatsAppCloudBotInteractiveType.list, body = WhatsAppCloudBotBody(translate(text).toString()), action = WhatsAppCloudBotAction( @@ -186,7 +187,8 @@ fun I18nTranslator.completeListMessage( WhatsAppBotRow( id = row.id.checkLength(WHATS_APP_ROW_ID_MAX_LENGTH), title = translate(row.title).toString().checkLength(WHATS_APP_ROW_TITLE_MAX_LENGTH), - description = translate(row.description).toString().checkLength(WHATS_APP_ROW_DESCRIPTION_MAX_LENGTH) + description = translate(row.description).toString() + .checkLength(WHATS_APP_ROW_DESCRIPTION_MAX_LENGTH) ) } ) @@ -206,9 +208,9 @@ fun I18nTranslator.completeListMessage( } -fun I18nTranslator.replyLocationMessage( +fun I18nTranslator.whatsAppCloudReplyLocationMessage( text: CharSequence -) : WhatsAppCloudBotInteractiveMessage = WhatsAppCloudBotInteractiveMessage( +): WhatsAppCloudBotInteractiveMessage = WhatsAppCloudBotInteractiveMessage( messagingProduct = "whatsapp", recipientType = WhatsAppCloudBotRecipientType.individual, interactive = WhatsAppCloudBotInteractive( @@ -221,54 +223,54 @@ fun I18nTranslator.replyLocationMessage( ) -fun > T.quickReply( +fun > T.whatsAppCloudQuickReply( title: CharSequence, targetIntent: IntentAware, parameters: Parameters ): QuickReply = - quickReply(title, targetIntent, stepName, parameters.toMap()) + whatsAppCloudQuickReply(title, targetIntent, stepName, parameters.toMap()) -fun > T.quickReply( +fun > T.whatsAppCloudQuickReply( title: CharSequence, targetIntent: IntentAware, step: StoryStep? = null, vararg parameters: Pair -) : QuickReply = quickReply(title, targetIntent.wrappedIntent(), step?.name, parameters.toMap()) +): QuickReply = whatsAppCloudQuickReply(title, targetIntent.wrappedIntent(), step?.name, parameters.toMap()) -fun > T.quickReply( +fun > T.whatsAppCloudQuickReply( title: CharSequence, targetIntent: IntentAware, step: String? = null, parameters: Map = mapOf() -) : QuickReply = - quickReply(title, targetIntent, step, parameters) { intent, s, params -> +): QuickReply = + whatsAppCloudQuickReply(title, targetIntent, step, parameters) { intent, s, params -> SendChoice.encodeChoiceId(intent, s, params, null, null, sourceAppId = null) } -private fun I18nTranslator.quickReply( +private fun I18nTranslator.whatsAppCloudQuickReply( title: CharSequence, targetIntent: IntentAware, step: String? = null, parameters: Map, payloadEncoder: (IntentAware, String?, Map) -> String -) : QuickReply = QuickReply( +): QuickReply = QuickReply( translate(title).toString(), payloadEncoder.invoke(targetIntent, step, parameters) ) -fun I18nTranslator.nlpQuickReply( +fun I18nTranslator.whatsAppCloudNlpQuickReply( title: CharSequence, textToSend: CharSequence = title, -) : QuickReply = QuickReply( +): QuickReply = QuickReply( translate(title).toString(), SendChoice.encodeNlpChoiceId(translate(textToSend).toString()), ) -fun I18nTranslator.buildTemplateMessage( +fun I18nTranslator.whatsAppCloudBuildTemplateMessage( templateName: String, languageCode: String, components: List -): WhatsAppCloudBotTemplateMessage { +): WhatsAppCloudBotTemplateMessage { return WhatsAppCloudBotTemplateMessage( messagingProduct = "whatsapp", recipientType = WhatsAppCloudBotRecipientType.individual, @@ -283,9 +285,9 @@ fun I18nTranslator.buildTemplateMessage( } -fun I18nTranslator.buildTemplateMessageCarousel( +fun I18nTranslator.whatsAppCloudBuildTemplateMessageCarousel( templateName: String, - components : List, + components: List, languageCode: String ): WhatsAppCloudBotTemplateMessage { return WhatsAppCloudBotTemplateMessage( @@ -307,102 +309,108 @@ fun I18nTranslator.buildTemplateMessageCarousel( } -fun > T.cardCarousel( +fun > T.whatsAppCloudCardCarousel( cardIndex: Int, components: List ): Component.Card = Component.Card( cardIndex = cardIndex, components = components ) -fun > T.bodyTemplate( + +fun > T.whatsAppCloudBodyTemplate( parameters: List -):Component.Body = Component.Body( +): Component.Body = Component.Body( type = ComponentType.BODY, parameters = parameters ) -fun > T.TextParameterTemplate( - typeParameter:CharSequence?, +fun > T.whatsAppCloudTextParameterTemplate( + typeParameter: CharSequence?, textButton: CharSequence? -):TextParameter = TextParameter( +): TextParameter = TextParameter( type = ParameterType.valueOf(translate(typeParameter).toString()), text = translate(textButton).toString(), ) -fun buttonTemplate( +fun whatsAppCloudButtonTemplate( index: String, subType: String, parameters: List -):Component.Button = Component.Button( +): Component.Button = Component.Button( type = ComponentType.BUTTON, subType = ButtonSubType.valueOf(subType), index = index, parameters = parameters ) -fun > T.postbackButton( +fun > T.whatsAppCloudPostbackButton( index: String, textButton: String, payload: String? -):Component.Button = buttonTemplate(index, ButtonSubType.QUICK_REPLY.name, listOf( - payloadParameterTemplate(textButton, payload, ParameterType.PAYLOAD.name) -)) +): Component.Button = whatsAppCloudButtonTemplate( + index, ButtonSubType.QUICK_REPLY.name, listOf( + whatsAppCloudPayloadParameterTemplate(textButton, payload, ParameterType.PAYLOAD.name) + ) +) -fun > T.whatsAppPostbackButton( +fun > T.whatsAppCloudPostbackButton( index: String, title: CharSequence, targetIntent: IntentAware, step: StoryStep? = null, parameters: Parameters = Parameters() -):Component.Button = postbackButton( +): Component.Button = whatsAppCloudPostbackButton( index = index, textButton = translate(title).toString(), - targetIntent.let { i -> SendChoice.encodeChoiceId(this, i, step, parameters.toMap()+(index to index)) } + targetIntent.let { i -> SendChoice.encodeChoiceId(this, i, step, parameters.toMap() + (index to index)) } ) -fun > T.whatsAppNLPPostbackButton( +fun > T.whatsAppCloudNLPPostbackButton( index: String, title: CharSequence, textToSend: CharSequence = title, -):Component.Button = postbackButton( +): Component.Button = whatsAppCloudPostbackButton( index = index, textButton = translate(title).toString(), payload = SendChoice.encodeNlpChoiceId(translate(textToSend).toString()), ) -fun > T.whatsAppUrlButton( +fun > T.whatsAppCloudUrlButton( index: String, textButton: String, -):Component.Button = buttonTemplate(index, ButtonSubType.URL.name, listOf( - payloadParameterTemplate(textButton, null,ParameterType.TEXT.name) -)) +): Component.Button = whatsAppCloudButtonTemplate( + index, ButtonSubType.URL.name, listOf( + whatsAppCloudPayloadParameterTemplate(textButton, null, ParameterType.TEXT.name) + ) +) -fun payloadParameterTemplate( +fun whatsAppCloudPayloadParameterTemplate( textButton: String, payload: String?, - typeParameter:String, -):PayloadParameter = PayloadParameter( + typeParameter: String, +): PayloadParameter = PayloadParameter( type = ParameterType.valueOf(typeParameter), payload = payload, text = textButton, ) -fun headerTemplate( - typeParameter:String, +fun whatsAppCloudHeaderTemplate( + typeParameter: String, imageId: String -):Component.Header = Component.Header( +): Component.Header = Component.Header( type = ComponentType.HEADER, - parameters = listOf(HeaderParameter.Image( - type = ParameterType.valueOf(typeParameter), - image = ImageId( - id = imageId + parameters = listOf( + HeaderParameter.Image( + type = ParameterType.valueOf(typeParameter), + image = ImageId( + id = imageId + ) ) ) - ) ) -private fun String.checkLength(maxLength: Int) : String { +private fun String.checkLength(maxLength: Int): String { if (maxLength > 0 && this.length > maxLength) { logger.info { "text $this should not exceed $maxLength chars." } } diff --git a/bot/connector-whatsapp-cloud/src/main/kotlin/WhatsAppConnectorCloudProvider.kt b/bot/connector-whatsapp-cloud/src/main/kotlin/WhatsAppConnectorCloudProvider.kt index a7b4cb3e05..33453ed276 100644 --- a/bot/connector-whatsapp-cloud/src/main/kotlin/WhatsAppConnectorCloudProvider.kt +++ b/bot/connector-whatsapp-cloud/src/main/kotlin/WhatsAppConnectorCloudProvider.kt @@ -22,7 +22,7 @@ import ai.tock.shared.resourceAsString internal object WhatsAppConnectorCloudProvider : ConnectorProvider { private const val APP_ID = "appId" - private const val WHATSAPP_PHONE_NUMBER_ID = "whatsAppPhoneNumberId" + internal const val WHATSAPP_PHONE_NUMBER_ID = "whatsAppPhoneNumberId" private const val WHATSAPP_BUSINESS_ACCOUNT_ID = "whatsAppBusinessAccountId" internal const val TOKEN = "token" private const val VERIFY_TOKEN = "verifyToken" diff --git a/bot/connector-whatsapp-cloud/src/main/kotlin/database/repository/PayloadWhatsAppCloudDAO.kt b/bot/connector-whatsapp-cloud/src/main/kotlin/database/repository/PayloadWhatsAppCloudDAO.kt index fdaea9d9e5..6f695c047f 100644 --- a/bot/connector-whatsapp-cloud/src/main/kotlin/database/repository/PayloadWhatsAppCloudDAO.kt +++ b/bot/connector-whatsapp-cloud/src/main/kotlin/database/repository/PayloadWhatsAppCloudDAO.kt @@ -18,8 +18,11 @@ package ai.tock.bot.connector.whatsapp.cloud.database.repository import ai.tock.bot.connector.whatsapp.cloud.database.model.PayloadWhatsAppCloud +/** + * In order to workaround the characters limit of the WhatsApp API, the payloads are stored in the database. + */ interface PayloadWhatsAppCloudDAO { fun getPayloadById(id: String): String? fun save(payloadWhatsAppCloud: PayloadWhatsAppCloud) -} \ No newline at end of file +} diff --git a/bot/connector-whatsapp-cloud/src/main/kotlin/database/repository/PayloadWhatsAppCloudMongoDAO.kt b/bot/connector-whatsapp-cloud/src/main/kotlin/database/repository/PayloadWhatsAppCloudMongoDAO.kt index 246f0759c9..55ccbe0ae6 100644 --- a/bot/connector-whatsapp-cloud/src/main/kotlin/database/repository/PayloadWhatsAppCloudMongoDAO.kt +++ b/bot/connector-whatsapp-cloud/src/main/kotlin/database/repository/PayloadWhatsAppCloudMongoDAO.kt @@ -20,6 +20,8 @@ import ai.tock.bot.connector.whatsapp.cloud.database.model.PayloadWhatsAppCloud import ai.tock.shared.TOCK_BOT_DATABASE import ai.tock.shared.error import ai.tock.shared.injector +import ai.tock.shared.intProperty +import ai.tock.shared.longProperty import ai.tock.shared.property import com.github.salomonbrys.kodein.instance import com.mongodb.client.MongoCollection @@ -35,6 +37,7 @@ object PayloadWhatsAppCloudMongoDAO : PayloadWhatsAppCloudDAO { * Name of the MongoDB database collection used to store WhatsApp payloads. */ private val collectionName = property("tock_whatsapp_payload", "whatsapp_payload") + private val payloadTTL = longProperty("tock_whatsapp_payload_ttl_days", 10) private val uuidRegex = Regex("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}") private val database: MongoDatabase by injector.instance( @@ -79,7 +82,7 @@ object PayloadWhatsAppCloudMongoDAO : PayloadWhatsAppCloudDAO { try { this.ensureIndex( PayloadWhatsAppCloud::payloadExpireDate, - indexOptions = IndexOptions().expireAfter(1L, TimeUnit.DAYS) + indexOptions = IndexOptions().expireAfter(payloadTTL, TimeUnit.DAYS) ) } catch (e: Exception) { logger.error(e) @@ -89,4 +92,4 @@ object PayloadWhatsAppCloudMongoDAO : PayloadWhatsAppCloudDAO { private fun isUUID(uuid: String): Boolean { return uuidRegex.matches(uuid) } -} \ No newline at end of file +} diff --git a/bot/connector-whatsapp-cloud/src/main/kotlin/model/webhook/message/content/ButtonContent.kt b/bot/connector-whatsapp-cloud/src/main/kotlin/model/webhook/message/content/ButtonContent.kt index 7390e0423f..836c0e5e06 100644 --- a/bot/connector-whatsapp-cloud/src/main/kotlin/model/webhook/message/content/ButtonContent.kt +++ b/bot/connector-whatsapp-cloud/src/main/kotlin/model/webhook/message/content/ButtonContent.kt @@ -17,6 +17,6 @@ package ai.tock.bot.connector.whatsapp.cloud.model.webhook.message.content data class ButtonContent( - val payload: String, - val text: String -) \ No newline at end of file + val payload: String, + val text: String +) diff --git a/bot/connector-whatsapp-cloud/src/main/kotlin/services/WhatsAppCloudApiService.kt b/bot/connector-whatsapp-cloud/src/main/kotlin/services/WhatsAppCloudApiService.kt index 7b387c754c..5baf529350 100644 --- a/bot/connector-whatsapp-cloud/src/main/kotlin/services/WhatsAppCloudApiService.kt +++ b/bot/connector-whatsapp-cloud/src/main/kotlin/services/WhatsAppCloudApiService.kt @@ -36,9 +36,13 @@ import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.HeaderPar import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.PayloadParameter import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsAppCloudBotActionButton import ai.tock.bot.engine.BotRepository +import ai.tock.shared.Executor import ai.tock.shared.TockProxyAuthenticator import ai.tock.shared.cache.getOrCache import ai.tock.shared.error +import ai.tock.shared.injector +import ai.tock.shared.provide +import kotlinx.coroutines.runBlocking import mu.KotlinLogging import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody @@ -56,6 +60,7 @@ class WhatsAppCloudApiService(private val apiClient: WhatsAppCloudApiClient) { private val logger = KotlinLogging.logger {} private val payloadWhatsApp: PayloadWhatsAppCloudDAO = PayloadWhatsAppCloudMongoDAO + private val executor:Executor = injector.provide() fun sendMessage(phoneNumberId: String, token: String, messageRequest: WhatsAppCloudSendBotMessage) { try { @@ -98,13 +103,15 @@ class WhatsAppCloudApiService(private val apiClient: WhatsAppCloudApiClient) { private fun updateButton(button: WhatsAppCloudBotActionButton): WhatsAppCloudBotActionButton = if (button.reply.id.length >= 256) { val uuidPayload = UUID.randomUUID().toString() - payloadWhatsApp.save( - PayloadWhatsAppCloud( - uuidPayload, - button.reply.id, - Date.from(Instant.now()), + executor.executeBlocking { + payloadWhatsApp.save( + PayloadWhatsAppCloud( + uuidPayload, + button.reply.id, + Date.from(Instant.now()), + ) ) - ) + } val copyReply = button.reply.copy(title = button.reply.title, id = uuidPayload) button.copy(reply = copyReply) } else { @@ -163,13 +170,15 @@ class WhatsAppCloudApiService(private val apiClient: WhatsAppCloudApiClient) { val updatedParameters = component.parameters.map { param -> if ((param.payload?.length ?: 0) > 128) { val newPayload = UUID.randomUUID().toString() - payloadWhatsApp.save( - PayloadWhatsAppCloud( - newPayload, - param.payload!!, - Date.from(Instant.now()) + executor.executeBlocking { + payloadWhatsApp.save( + PayloadWhatsAppCloud( + newPayload, + param.payload!!, + Date.from(Instant.now()) + ) ) - ) + } updatePayloadParameter(param, newPayload) } else { param @@ -189,7 +198,9 @@ class WhatsAppCloudApiService(private val apiClient: WhatsAppCloudApiClient) { button.copy(parameters = button.parameters.map { parameters -> parameters.payload?.takeIf { it.length >= 128 }?.let { val uuidPayload = UUID.randomUUID().toString() - payloadWhatsApp.save(PayloadWhatsAppCloud(uuidPayload, it, Date.from(Instant.now()))) + executor.executeBlocking { + payloadWhatsApp.save(PayloadWhatsAppCloud(uuidPayload, it, Date.from(Instant.now()))) + } parameters.copy(payload = uuidPayload) } ?: parameters })