Skip to content

Commit

Permalink
Merge pull request #54 from facebookincubator/hunter/better_send_resp…
Browse files Browse the repository at this point in the history
…onse_handling

more flexible handling of responses from the cloud api
  • Loading branch information
hunterjackson authored Jul 8, 2024
2 parents 7251071 + 2d5f6a7 commit 1cb5e71
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 49 deletions.
90 changes: 41 additions & 49 deletions src/main/java/com/meta/cp4m/message/WAMessageHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -87,13 +81,13 @@ private List<ThreadState<WAMessage>> 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 -> {
Expand Down Expand Up @@ -170,16 +164,20 @@ public ThreadState<WAMessage> respond(WAMessage message) throws IOException {
throw new UnsupportedOperationException(
"Non-text payloads cannot be sent to Whatsapp client currently");
}
@Nullable SendResponse response = null;
@Nullable List<SendResponse> 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<WAMessage> 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) {
Expand All @@ -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()
Expand All @@ -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<SendResponseContact> contacts) {}

@Override
public List<RouteDetails<?, WAMessage>> routeDetails() {
RouteDetails<WebhookPayload, WAMessage> postDetails =
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SendResponseContact> contacts,
List<SendResponseMessages> messages) {
@JsonCreator
public SendResponse(
@JsonProperty("messaging_product") String messagingProduct,
@JsonProperty("contacts") List<SendResponseContact> contacts,
@JsonProperty("messages") List<SendResponseMessages> 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) {}
}
25 changes: 25 additions & 0 deletions src/test/java/com/meta/cp4m/message/WAMessageHandlerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit 1cb5e71

Please sign in to comment.