diff --git a/src/main/java/com/meta/cp4m/message/WAMessageHandler.java b/src/main/java/com/meta/cp4m/message/WAMessageHandler.java index 5f3ae26..6a06219 100644 --- a/src/main/java/com/meta/cp4m/message/WAMessageHandler.java +++ b/src/main/java/com/meta/cp4m/message/WAMessageHandler.java @@ -12,12 +12,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import org.apache.hc.client5.http.fluent.Content; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.json.JsonMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.meta.cp4m.Identifier; import com.meta.cp4m.message.webhook.whatsapp.*; import io.javalin.http.Context; @@ -87,13 +81,13 @@ private List> post(Context ctx, WebhookPayload payload) { switch (message) { case TextWebhookMessage m -> payloadValue = new Payload.Text(m.text().body()); case ImageWebhookMessage m -> { - try { - URI url = this.getUrlFromID(m.image().id()); - byte[] media = this.getMediaFromUrl(url); - payloadValue = new Payload.Image(media, m.image().mimeType()); - } catch (IOException | URISyntaxException e) { - throw new RuntimeException(e); - } + try { + URI url = this.getUrlFromID(m.image().id()); + byte[] media = this.getMediaFromUrl(url); + payloadValue = new Payload.Image(media, m.image().mimeType()); + } catch (IOException | URISyntaxException e) { + throw new RuntimeException(e); + } } case DocumentWebhookMessage m -> { @@ -170,16 +164,20 @@ public ThreadState respond(WAMessage message) throws IOException { throw new UnsupportedOperationException( "Non-text payloads cannot be sent to Whatsapp client currently"); } - @Nullable SendResponse response = null; + @Nullable List responses = new ArrayList<>(); for (String text : CHUNKER.chunks(message.message()).toList()) { - response = send(message.recipientId(), message.senderId(), text); + SendResponse r = send(message.recipientId(), message.senderId(), text); + responses.add(r); } ThreadState ts = ThreadState.of(message); - if (response == null) { - return ts; - } - return ts.withUserData( - ts.userData().withPhoneNumber(response.contacts().getFirst().phoneNumber())); + return responses.stream() + .map(SendResponse::contacts) + .flatMap(List::stream) + .findAny() + .map(SendResponse.SendResponseContact::phoneNumber) + .map(ph -> ts.userData().withPhoneNumber(ph)) + .map(ts::withUserData) + .orElse(ts); } private URI messagesURI(Identifier phoneNumberId) { @@ -194,8 +192,7 @@ private URI messagesURI(Identifier phoneNumberId) { } } - private SendResponse send(Identifier recipient, Identifier sender, String text) - throws IOException { + SendResponse send(Identifier recipient, Identifier sender, String text) throws IOException { ObjectNode body = MAPPER .createObjectNode() @@ -221,16 +218,6 @@ private SendResponse send(Identifier recipient, Identifier sender, String text) }); } - record SendResponseContact( - @JsonProperty("input") String phoneNumber, @JsonProperty("wa_id") String phoneNumberId) {} - - record SendResponseMessage( - @JsonProperty("id") String messageId, @JsonProperty("pacing_status") String pacingStatus) {} - - record SendResponse( - @JsonProperty("messaging_product") String messagingProduct, - @JsonProperty List contacts) {} - @Override public List> routeDetails() { RouteDetails postDetails = @@ -285,26 +272,31 @@ private void markRead(Identifier phoneNumberId, String messageId) { } } - private URI getUrlFromID(String mediaID) throws IOException, URISyntaxException { - return Request.get(new URIBuilder(this.baseURL).appendPath(mediaID).build()) - .setHeader("Authorization", "Bearer " + accessToken) - .setHeader("appsecret_proof", appSecretProof) - .execute().handleResponse(response -> { - try { - String jsonResponse = EntityUtils.toString(response.getEntity()); - JsonNode jsonNode = MAPPER.readTree(jsonResponse); - return new URIBuilder(jsonNode.get("url").asText()); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - }).build(); - } + private URI getUrlFromID(String mediaID) throws IOException, URISyntaxException { + return Request.get(new URIBuilder(this.baseURL).appendPath(mediaID).build()) + .setHeader("Authorization", "Bearer " + accessToken) + .setHeader("appsecret_proof", appSecretProof) + .execute() + .handleResponse( + response -> { + try { + String jsonResponse = EntityUtils.toString(response.getEntity()); + JsonNode jsonNode = MAPPER.readTree(jsonResponse); + return new URIBuilder(jsonNode.get("url").asText()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + }) + .build(); + } private byte[] getMediaFromUrl(URI url) throws IOException { return Request.get(url) - .setHeader("Authorization", "Bearer " + accessToken) - .setHeader("appsecret_proof", appSecretProof) - .execute().handleResponse(response -> { + .setHeader("Authorization", "Bearer " + accessToken) + .setHeader("appsecret_proof", appSecretProof) + .execute() + .handleResponse( + response -> { try { return EntityUtils.toByteArray(response.getEntity()); } catch (IOException e) { diff --git a/src/main/java/com/meta/cp4m/message/webhook/whatsapp/SendResponse.java b/src/main/java/com/meta/cp4m/message/webhook/whatsapp/SendResponse.java new file mode 100644 index 0000000..e3e20e3 --- /dev/null +++ b/src/main/java/com/meta/cp4m/message/webhook/whatsapp/SendResponse.java @@ -0,0 +1,51 @@ +/* + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.meta.cp4m.message.webhook.whatsapp; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; + +public record SendResponse( + String messagingProduct, + List contacts, + List messages) { + @JsonCreator + public SendResponse( + @JsonProperty("messaging_product") String messagingProduct, + @JsonProperty("contacts") List contacts, + @JsonProperty("messages") List messages) { + this.messagingProduct = messagingProduct; + this.contacts = contacts == null ? Collections.emptyList() : contacts; + this.messages = messages == null ? Collections.emptyList() : messages; + } + + @Override + public String toString() { + return "SendResponse[" + + "messagingProduct=" + + messagingProduct + + ", " + + "contacts=" + + contacts + + ", " + + "messages=" + + messages + + ']'; + } + + public record SendResponseContact( + @JsonProperty("input") String phoneNumber, @JsonProperty("wa_id") String phoneNumberId) {} + + public record SendResponseMessages( + @JsonProperty("id") String messageId, + @Nullable @JsonProperty("message_status") String messageStatus) {} +} diff --git a/src/test/java/com/meta/cp4m/message/WAMessageHandlerTest.java b/src/test/java/com/meta/cp4m/message/WAMessageHandlerTest.java index 0cd701a..0a72f5b 100644 --- a/src/test/java/com/meta/cp4m/message/WAMessageHandlerTest.java +++ b/src/test/java/com/meta/cp4m/message/WAMessageHandlerTest.java @@ -15,6 +15,7 @@ import com.google.common.base.Stopwatch; import com.meta.cp4m.DummyWebServer.ReceivedRequest; import com.meta.cp4m.Identifier; +import com.meta.cp4m.message.webhook.whatsapp.SendResponse; import com.meta.cp4m.message.webhook.whatsapp.Utils; import java.io.IOException; import java.util.ArrayList; @@ -246,6 +247,30 @@ void valid() throws IOException, InterruptedException { assertThat(harness.pollWebserver(500)).isNull(); } + @Test + void validResponseWithoutContacts() throws IOException { + final String sendResponse = + """ +{ + "messaging_product": "whatsapp", + "contacts": [ + ], + "messages": [ + { + "id": "wamid.HBgLMTY1MDUwNzY1MjAVAgARGBI5QTNDQTVCM9Q0Q0Q2RTY3RTcA", + "message_status": "accepted" + } + ] + }"""; + harness.dummyWebServer().response(ctx -> ctx.body().contains("\"type\""), sendResponse); + harness.start(); + WAMessageHandler handler = (WAMessageHandler) harness.handler(); + SendResponse response = handler.send(Identifier.random(), Identifier.random(), "test"); + assertThat(response.contacts()).isEmpty(); + assertThat(response.messages().getFirst().messageId()) + .isEqualTo("wamid.HBgLMTY1MDUwNzY1MjAVAgARGBI5QTNDQTVCM9Q0Q0Q2RTY3RTcA"); + } + @Test void doesNotSendNonTextMessages() throws IOException, InterruptedException { harness.start();