Skip to content

Commit ef7db56

Browse files
authored
resolves #1864 whatsapp_connector: deprecate legacy connector and clean up successor (#1871)
* resolves #1864 whatsapp_connector: deprecate legacy connector and clean up successor * fix test name
1 parent 10948eb commit ef7db56

32 files changed

+617
-454
lines changed

bot/connector-whatsapp-cloud/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
> **This connector is still in alpha phase of development**
1+
> **This connector is in active development**
22
>
3-
> Breaking changes may occur.
3+
> Breaking changes may occur in the Bot API. Refer to [this issue](https://github.com/theopenconversationkit/tock/issues/1863)
4+
> for an outline of the changes to expect.
45
56
## Prerequisites
67

@@ -13,7 +14,6 @@
1314
* (Optional) **Meta Application id**: The id of the Meta application, only required for [template management](#template-management).
1415

1516
* Then go to the *Configuration* -> *Bot Configurations* menu in Tock Studio, and create a new configuration with these parameters.
16-
Set the `Mode` field to "subscribe".
1717

1818
## Bot API
1919

bot/connector-whatsapp-cloud/src/main/kotlin/WebhookActionConverter.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ package ai.tock.bot.connector.whatsapp.cloud
1919
import ai.tock.bot.connector.whatsapp.cloud.UserHashedIdCache.createHashedId
2020
import ai.tock.bot.connector.whatsapp.cloud.database.repository.PayloadWhatsAppCloudDAO
2121
import ai.tock.bot.connector.whatsapp.cloud.database.repository.PayloadWhatsAppCloudMongoDAO
22-
import ai.tock.bot.connector.whatsapp.cloud.model.webhook.message.*
22+
import ai.tock.bot.connector.whatsapp.cloud.model.webhook.message.WhatsAppCloudButtonMessage
23+
import ai.tock.bot.connector.whatsapp.cloud.model.webhook.message.WhatsAppCloudImageMessage
24+
import ai.tock.bot.connector.whatsapp.cloud.model.webhook.message.WhatsAppCloudInteractiveMessage
25+
import ai.tock.bot.connector.whatsapp.cloud.model.webhook.message.WhatsAppCloudLocationMessage
26+
import ai.tock.bot.connector.whatsapp.cloud.model.webhook.message.WhatsAppCloudMessage
27+
import ai.tock.bot.connector.whatsapp.cloud.model.webhook.message.WhatsAppCloudTextMessage
2328
import ai.tock.bot.connector.whatsapp.cloud.services.WhatsAppCloudApiService
2429
import ai.tock.bot.engine.action.SendAttachment
2530
import ai.tock.bot.engine.action.SendChoice
@@ -38,8 +43,7 @@ internal object WebhookActionConverter {
3843
fun toEvent(
3944
message: WhatsAppCloudMessage,
4045
applicationId: String,
41-
whatsAppCloudApiService: WhatsAppCloudApiService,
42-
token: String
46+
whatsAppCloudApiService: WhatsAppCloudApiService
4347
): Event? {
4448
val senderId = createHashedId(message.from)
4549
return when (message) {
@@ -51,7 +55,7 @@ internal object WebhookActionConverter {
5155
)
5256

5357
is WhatsAppCloudImageMessage -> {
54-
val binaryImg = whatsAppCloudApiService.downloadImgByBinary(token, message.image.id, message.image.mimeType)
58+
val binaryImg = whatsAppCloudApiService.downloadImgByBinary(message.image.id, message.image.mimeType)
5559
SendAttachment(
5660
PlayerId(senderId),
5761
applicationId,

bot/connector-whatsapp-cloud/src/main/kotlin/WhatsAppCloudApiClient.kt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import com.fasterxml.jackson.module.kotlin.readValue
3737
import java.io.IOException
3838
import mu.KotlinLogging
3939
import okhttp3.MediaType.Companion.toMediaTypeOrNull
40+
import okhttp3.MultipartBody
4041
import okhttp3.OkHttpClient
4142
import okhttp3.Request
4243
import okhttp3.RequestBody
@@ -58,7 +59,7 @@ private const val WHATSAPP_API_BASE_URL = "https://graph.facebook.com"
5859
private const val VERSION = "22.0"
5960
private const val WHATSAPP_API_URL = "$WHATSAPP_API_BASE_URL/v$VERSION"
6061

61-
class WhatsAppCloudApiClient(val token: String, val businessAccountId: String, val phoneNumber: String) {
62+
class WhatsAppCloudApiClient(private val token: String, val businessAccountId: String, val phoneNumberId: String) {
6263

6364
interface GraphApi {
6465

@@ -136,7 +137,7 @@ class WhatsAppCloudApiClient(val token: String, val businessAccountId: String, v
136137
}
137138

138139
private val logger = KotlinLogging.logger {}
139-
val graphApi: GraphApi = retrofitBuilderWithTimeoutAndLogger(
140+
private val graphApi: GraphApi = retrofitBuilderWithTimeoutAndLogger(
140141
longProperty("tock_whatsappcloud_request_timeout_ms", 30000),
141142
logger,
142143
requestGZipEncoding = booleanProperty("tock_whatsappcloud_request_gzip", false)
@@ -146,6 +147,17 @@ class WhatsAppCloudApiClient(val token: String, val businessAccountId: String, v
146147
.build()
147148
.create()
148149

150+
fun uploadMediaInWhatsAppAccount(file: RequestBody) = graphApi.uploadMediaInWhatsAppAccount(
151+
phoneNumberId,
152+
"Bearer $token",
153+
MultipartBody.Builder().setType(MultipartBody.FORM)
154+
.addFormDataPart("file", "fileimage", file)
155+
.addFormDataPart("messaging_product", "whatsapp")
156+
.build()
157+
)
158+
fun retrieveMediaUrl(imgId: String) = graphApi.retrieveMediaUrl(imgId, token)
159+
fun downloadMediaBinary(url: String) = graphApi.downloadMediaBinary(url, "Bearer $token")
160+
fun sendMessage(phoneNumberId: String, messageRequest: WhatsAppCloudSendBotMessage) = graphApi.sendMessage(phoneNumberId, token, messageRequest)
149161
fun createMessageTemplate(template: WhatsappTemplate) = graphApi.createMessageTemplate(businessAccountId, token, template)
150162
fun deleteMessageTemplate(templateName: String) = graphApi.deleteMessageTemplate(businessAccountId, token, templateName)
151163
fun editMessageTemplate(templateId: String, updatedTemplate: WhatsappTemplate) = graphApi.editMessageTemplate(token, templateId, updatedTemplate)

bot/connector-whatsapp-cloud/src/main/kotlin/WhatsAppCloudBuilder.kt

Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,34 @@ import ai.tock.bot.connector.whatsapp.cloud.model.common.TextContent
2222
import ai.tock.bot.connector.whatsapp.cloud.model.send.QuickReply
2323
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.WhatsAppCloudBotMessage
2424
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.WhatsAppCloudBotRecipientType
25-
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.*
25+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.ButtonSubType
26+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.ComponentType
27+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.HeaderParameter
28+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.ImageId
29+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.Language
30+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.ParameterType
31+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.ParametersUrl
32+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.PayloadParameter
33+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.TextParameter
34+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsAppBotRow
35+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsAppCloudBotAction
36+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsAppCloudBotActionButton
37+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsAppCloudBotActionButtonReply
38+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsAppCloudBotActionSection
2639
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsAppCloudBotBody
40+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsAppCloudBotFooter
41+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsAppCloudBotHeaderType
42+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsAppCloudBotImage
43+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsAppCloudBotImageMessage
44+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsAppCloudBotInteractive
45+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsAppCloudBotInteractiveHeader
46+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsAppCloudBotInteractiveMessage
47+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsAppCloudBotInteractiveType
48+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsAppCloudBotMediaImage
49+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsAppCloudBotTemplate
50+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsAppCloudBotTemplateMessage
51+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsAppCloudBotTextMessage
52+
import ai.tock.bot.connector.whatsapp.cloud.model.send.message.content.WhatsappTemplateComponent
2753
import ai.tock.bot.definition.IntentAware
2854
import ai.tock.bot.definition.Parameters
2955
import ai.tock.bot.definition.StoryHandlerDefinition
@@ -37,6 +63,7 @@ import mu.KotlinLogging
3763

3864
private val logger = KotlinLogging.logger {}
3965
private val errorOnInvalidMessages = booleanProperty("tock_whatsapp_error_on_invalid_messages", false)
66+
private val uploadImagesToWhatsapp = booleanProperty("tock_whatsapp_reupload_images", true)
4067

4168
internal const val WHATS_APP_CONNECTOR_TYPE_ID = "whatsapp_cloud"
4269
const val WHATSAPP_BUTTONS_TITLE_MAX_LENGTH = 50
@@ -71,6 +98,19 @@ fun <T : Bus<T>> T.sendToWhatsAppCloud(
7198
return this
7299
}
73100

101+
/**
102+
* Sends an WhatsApp message as last bot answer, only if the [ConnectorType] of the current [BotBus] is [whatsAppCloudConnectorType].
103+
*/
104+
fun <T : Bus<T>> T.endForWhatsAppCloud(
105+
messageProvider: T.() -> WhatsAppCloudBotMessage,
106+
delay: Long = defaultDelay(currentAnswerIndex)
107+
): T {
108+
if (isCompatibleWith(whatsAppCloudConnectorType)) {
109+
withMessage(messageProvider(this))
110+
end(delay)
111+
}
112+
return this
113+
}
74114

75115
/**
76116
* Adds a WhatsApp [ConnectorMessage] if the current connector is WhatsApp.
@@ -91,7 +131,6 @@ fun BotBus.whatsAppCloudText(
91131
previewUrl: Boolean = false
92132
): WhatsAppCloudBotTextMessage =
93133
WhatsAppCloudBotTextMessage(
94-
messagingProduct = "whatsapp",
95134
text = TextContent(translate(text).toString()),
96135
recipientType = WhatsAppCloudBotRecipientType.individual,
97136
userId = userId.id,
@@ -101,20 +140,44 @@ fun BotBus.whatsAppCloudText(
101140
/**
102141
* Creates an [Image Message](https://developers.facebook.com/docs/whatsapp/cloud-api/messages/image-messages)
103142
*
104-
* @param id the URL of the image
143+
* @param url the URL of the image
144+
* @param caption a caption to display below the image
145+
* @param uploadToWhatsapp if `true`, the image will be uploaded to Meta's servers (recommended)
146+
*/
147+
fun BotBus.whatsAppCloudImage(
148+
url: String,
149+
caption: CharSequence? = null,
150+
uploadToWhatsapp: Boolean = uploadImagesToWhatsapp,
151+
): WhatsAppCloudBotImageMessage =
152+
WhatsAppCloudBotImageMessage(
153+
image = WhatsAppCloudBotImage.LinkedImage(
154+
url = url,
155+
caption = translate(caption).toString().checkLength(WHATSAPP_IMAGE_CAPTION_MAX_LENGTH),
156+
uploadToWhatsapp,
157+
),
158+
recipientType = WhatsAppCloudBotRecipientType.individual,
159+
userId = userId.id,
160+
)
161+
162+
/**
163+
* Creates an [Image Message](https://developers.facebook.com/docs/whatsapp/cloud-api/messages/image-messages)
164+
*
165+
* @param id a unique ID for the image
166+
* @param imageBytes a byte array containing the image data
105167
* @param caption a caption to display below the image
106168
*/
107169
fun BotBus.whatsAppCloudImage(
108170
id: String,
109-
link: String? = null,
171+
imageBytes: ByteArray,
110172
caption: CharSequence? = null,
173+
mimeType: String = "image/png",
111174
): WhatsAppCloudBotImageMessage =
112175
WhatsAppCloudBotImageMessage(
113-
messagingProduct = "whatsapp",
114-
image = WhatsAppCloudBotImage(
176+
image = WhatsAppCloudBotImage.UploadedImage(
115177
id = id,
116-
link = link,
117-
caption = translate(caption).toString().checkLength(WHATSAPP_IMAGE_CAPTION_MAX_LENGTH)
178+
bytes = imageBytes,
179+
mimeType = mimeType,
180+
caption = translate(caption).toString().checkLength(WHATSAPP_IMAGE_CAPTION_MAX_LENGTH),
118181
),
119182
recipientType = WhatsAppCloudBotRecipientType.individual,
120183
userId = userId.id,
@@ -175,7 +238,6 @@ internal fun I18nTranslator.whatsAppCloudReplyButtonMessage(
175238
replies: List<QuickReply>,
176239
header: WhatsAppCloudBotInteractiveHeader?
177240
) = WhatsAppCloudBotInteractiveMessage(
178-
messagingProduct = "whatsapp",
179241
recipientType = WhatsAppCloudBotRecipientType.individual,
180242
interactive = WhatsAppCloudBotInteractive(
181243
type = WhatsAppCloudBotInteractiveType.button,
@@ -275,7 +337,6 @@ fun I18nTranslator.whatsAppCloudUrlButtonMessage(
275337
header: CharSequence? = null,
276338
footer: CharSequence? = null,
277339
): WhatsAppCloudBotInteractiveMessage = WhatsAppCloudBotInteractiveMessage(
278-
messagingProduct = "whatsapp",
279340
recipientType = WhatsAppCloudBotRecipientType.individual,
280341
interactive = WhatsAppCloudBotInteractive(
281342
type = WhatsAppCloudBotInteractiveType.cta_url,
@@ -407,7 +468,6 @@ fun I18nTranslator.whatsAppCloudListMessage(
407468
footer: CharSequence? = null,
408469
): WhatsAppCloudBotInteractiveMessage {
409470
return WhatsAppCloudBotInteractiveMessage(
410-
messagingProduct = "whatsapp",
411471
recipientType = WhatsAppCloudBotRecipientType.individual,
412472
interactive = WhatsAppCloudBotInteractive(
413473
type = WhatsAppCloudBotInteractiveType.list,
@@ -486,7 +546,6 @@ fun I18nTranslator.whatsAppCloudListSection(title: CharSequence, rows: List<Quic
486546
fun I18nTranslator.whatsAppCloudReplyLocationMessage(
487547
text: CharSequence
488548
): WhatsAppCloudBotInteractiveMessage = WhatsAppCloudBotInteractiveMessage(
489-
messagingProduct = "whatsapp",
490549
recipientType = WhatsAppCloudBotRecipientType.individual,
491550
interactive = WhatsAppCloudBotInteractive(
492551
type = WhatsAppCloudBotInteractiveType.location_request_message,
@@ -616,7 +675,7 @@ fun I18nTranslator.whatsAppCloudNlpQuickReply(
616675
fun I18nTranslator.whatsAppBuildCloudTemplateMessage(
617676
templateName: String,
618677
languageCode: String,
619-
components: List<Component>
678+
components: List<WhatsappTemplateComponent>
620679
) = whatsAppCloudTemplateMessage(templateName, languageCode, components)
621680

622681
/**
@@ -627,10 +686,9 @@ fun I18nTranslator.whatsAppBuildCloudTemplateMessage(
627686
fun I18nTranslator.whatsAppCloudTemplateMessage(
628687
templateName: String,
629688
languageCode: String,
630-
components: List<Component>
689+
components: List<WhatsappTemplateComponent>
631690
): WhatsAppCloudBotTemplateMessage {
632691
return WhatsAppCloudBotTemplateMessage(
633-
messagingProduct = "whatsapp",
634692
recipientType = WhatsAppCloudBotRecipientType.individual,
635693
template = WhatsAppCloudBotTemplate(
636694
name = templateName,
@@ -645,7 +703,7 @@ fun I18nTranslator.whatsAppCloudTemplateMessage(
645703
@Deprecated("renamed", ReplaceWith("whatsAppCloudTemplateMessageCarousel(templateName, components, languageCode)"))
646704
fun I18nTranslator.whatsAppCloudBuildTemplateMessageCarousel(
647705
templateName: String,
648-
components: List<Component.Card>,
706+
components: List<WhatsappTemplateComponent.Card>,
649707
languageCode: String
650708
) = whatsAppCloudTemplateMessageCarousel(templateName, components, languageCode)
651709

@@ -656,19 +714,18 @@ fun I18nTranslator.whatsAppCloudBuildTemplateMessageCarousel(
656714
*/
657715
fun I18nTranslator.whatsAppCloudTemplateMessageCarousel(
658716
templateName: String,
659-
components: List<Component.Card>,
717+
components: List<WhatsappTemplateComponent.Card>,
660718
languageCode: String
661719
): WhatsAppCloudBotTemplateMessage {
662720
return WhatsAppCloudBotTemplateMessage(
663-
messagingProduct = "whatsapp",
664721
recipientType = WhatsAppCloudBotRecipientType.individual,
665722
template = WhatsAppCloudBotTemplate(
666723
name = templateName,
667724
language = Language(
668725
code = languageCode,
669726
),
670727
components = listOf(
671-
Component.Carousel(
728+
WhatsappTemplateComponent.Carousel(
672729
type = ComponentType.CAROUSEL,
673730
cards = components
674731
)
@@ -677,7 +734,7 @@ fun I18nTranslator.whatsAppCloudTemplateMessageCarousel(
677734
)
678735
}
679736

680-
fun <T : Bus<T>> T.whatsAppCloudCardCarousel(cardIndex: Int, components: List<Component>): Component.Card {
737+
fun <T : Bus<T>> T.whatsAppCloudCardCarousel(cardIndex: Int, components: List<WhatsappTemplateComponent>): WhatsappTemplateComponent.Card {
681738
return whatsAppCloudTemplateCard(
682739
cardIndex, components
683740
)
@@ -693,8 +750,8 @@ fun <T : Bus<T>> T.whatsAppCloudCardCarousel(cardIndex: Int, components: List<Co
693750
*/
694751
fun <T : Bus<T>> T.whatsAppCloudTemplateCard(
695752
cardIndex: Int,
696-
components: List<Component>
697-
): Component.Card = Component.Card(
753+
components: List<WhatsappTemplateComponent>
754+
): WhatsappTemplateComponent.Card = WhatsappTemplateComponent.Card(
698755
cardIndex = cardIndex,
699756
components = components
700757
)
@@ -711,7 +768,7 @@ fun <T : Bus<T>> T.whatsAppCloudBodyTemplate(
711768
*/
712769
fun <T : Bus<T>> T.whatsAppCloudTemplateBody(
713770
parameters: List<TextParameter>
714-
): Component.Body = Component.Body(
771+
): WhatsappTemplateComponent.Body = WhatsappTemplateComponent.Body(
715772
type = ComponentType.BODY,
716773
parameters = parameters
717774
)
@@ -736,7 +793,7 @@ fun whatsAppCloudButtonTemplate(
736793
index: Int,
737794
subType: ButtonSubType,
738795
parameters: List<PayloadParameter>
739-
): Component.Button = Component.Button(
796+
): WhatsappTemplateComponent.Button = WhatsappTemplateComponent.Button(
740797
type = ComponentType.BUTTON,
741798
subType = subType,
742799
index = index.toString(),
@@ -747,7 +804,7 @@ fun <T : Bus<T>> T.whatsAppCloudPostbackButton(
747804
index: Int,
748805
textButton: String,
749806
payload: String?
750-
): Component.Button = whatsAppCloudButtonTemplate(
807+
): WhatsappTemplateComponent.Button = whatsAppCloudButtonTemplate(
751808
index, ButtonSubType.QUICK_REPLY, listOf(
752809
whatsAppCloudPayloadParameterTemplate(textButton, payload, ParameterType.PAYLOAD)
753810
)
@@ -768,7 +825,7 @@ fun <T : Bus<T>> T.whatsAppCloudPostbackButton(
768825
targetIntent: IntentAware,
769826
step: StoryStep<out StoryHandlerDefinition>? = null,
770827
parameters: Parameters = Parameters()
771-
): Component.Button = whatsAppCloudPostbackButton(
828+
): WhatsappTemplateComponent.Button = whatsAppCloudPostbackButton(
772829
index = index,
773830
textButton = translate(title).toString(),
774831
// Add an index parameter to ensure that all buttons in the list have unique ids
@@ -779,7 +836,7 @@ fun <T : Bus<T>> T.whatsAppCloudNLPPostbackButton(
779836
index: Int,
780837
title: CharSequence,
781838
textToSend: CharSequence = title,
782-
): Component.Button = whatsAppCloudPostbackButton(
839+
): WhatsappTemplateComponent.Button = whatsAppCloudPostbackButton(
783840
index = index,
784841
textButton = translate(title).toString(),
785842
payload = SendChoice.encodeNlpChoiceId(translate(textToSend).toString()),
@@ -788,7 +845,7 @@ fun <T : Bus<T>> T.whatsAppCloudNLPPostbackButton(
788845
fun <T : Bus<T>> T.whatsAppCloudUrlButton(
789846
index: Int,
790847
textButton: String,
791-
): Component.Button = whatsAppCloudButtonTemplate(
848+
): WhatsappTemplateComponent.Button = whatsAppCloudButtonTemplate(
792849
index, ButtonSubType.URL, listOf(
793850
whatsAppCloudPayloadParameterTemplate(textButton, null, ParameterType.TEXT)
794851
)
@@ -813,7 +870,7 @@ fun whatsAppCloudHeaderTemplate(
813870

814871
fun whatsAppCloudTemplateImageHeader(
815872
imageId: String
816-
): Component.Header = Component.Header(
873+
): WhatsappTemplateComponent.Header = WhatsappTemplateComponent.Header(
817874
type = ComponentType.HEADER,
818875
parameters = listOf(
819876
HeaderParameter.Image(

0 commit comments

Comments
 (0)