Skip to content

Commit

Permalink
add welcome message to was message handler
Browse files Browse the repository at this point in the history
  • Loading branch information
hunterjackson committed Jun 24, 2024
1 parent 0eeea76 commit e704481
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 12 deletions.
34 changes: 29 additions & 5 deletions src/main/java/com/meta/cp4m/message/WAMessageHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,20 @@ public class WAMessageHandler implements MessageHandler<WAMessage> {

private static final TextChunker CHUNKER = TextChunker.standard(MAX_CHARS_PER_MESSAGE);

private final ExecutorService readExecutor = Executors.newVirtualThreadPerTaskExecutor();
private final ExecutorService asyncExecutor = Executors.newVirtualThreadPerTaskExecutor();
private final Deduplicator<Identifier> messageDeduplicator = new Deduplicator<>(10_000);
private final String appSecret;
private final String verifyToken;
private final String accessToken;
private final String appSecretProof;

private final @Nullable String welcomeMessage;
private URI baseURL = DEFAULT_BASE_URI;

public WAMessageHandler(WAMessengerConfig config) {
this.verifyToken = config.verifyToken();
this.accessToken = config.accessToken();
this.appSecret = config.appSecret();
this.welcomeMessage = config.welcomeMessage().orElse(null);
this.appSecretProof = MetaHandlerUtils.hmac(accessToken, appSecret);
}

Expand All @@ -77,6 +78,27 @@ private List<WAMessage> post(Context ctx, WebhookPayload payload) {
Payload<?> payloadValue;
switch (message) {
case TextWebhookMessage m -> payloadValue = new Payload.Text(m.text().body());
case WelcomeWebhookMessage ignored -> {
if (welcomeMessage != null) {
WAMessage welcome =
new WAMessage(
message.timestamp(),
message.id(),
message.from(),
phoneNumberId,
welcomeMessage,
Message.Role.USER);
asyncExecutor.submit(
() -> {
try {
respond(welcome);
} catch (IOException e) {
LOGGER.error("unable to send welcome message");
}
});
}
continue;
}
default -> {
LOGGER.warn(
"received message of type '"
Expand All @@ -93,7 +115,7 @@ private List<WAMessage> post(Context ctx, WebhookPayload payload) {
phoneNumberId,
payloadValue,
Message.Role.USER));
readExecutor.execute(() -> markRead(phoneNumberId, message.id().toString()));
asyncExecutor.execute(() -> markRead(phoneNumberId, message.id().toString()));
}
});
return waMessages;
Expand Down Expand Up @@ -144,7 +166,8 @@ private void send(Identifier recipient, Identifier sender, String text) throws I
.setHeader("Authorization", "Bearer " + accessToken)
.setHeader("appsecret_proof", appSecretProof)
.bodyString(bodyString, ContentType.APPLICATION_JSON)
.execute();
.execute()
.discardContent();
}

@Override
Expand Down Expand Up @@ -192,7 +215,8 @@ private void markRead(Identifier phoneNumberId, String messageId) {
.setHeader("Authorization", "Bearer " + accessToken)
.setHeader("appsecret_proof", appSecretProof)
.bodyString(bodyString, ContentType.APPLICATION_JSON)
.execute();
.execute()
.discardContent();
} catch (IOException e) {
// nothing we can do here, marking later messages as read will mark all previous messages read
// so this is not a fatal issue
Expand Down
21 changes: 19 additions & 2 deletions src/main/java/com/meta/cp4m/message/WAMessengerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,25 @@

import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;
import java.util.Optional;
import java.util.UUID;
import org.checkerframework.checker.nullness.qual.Nullable;

public class WAMessengerConfig implements HandlerConfig {

private final String name;
private final String verifyToken;
private final String appSecret;
private final String accessToken;
private final @Nullable String welcomeMessage;

private WAMessengerConfig(
@JsonProperty("name") String name,
@JsonProperty("verify_token") String verifyToken,
@JsonProperty("app_secret") String appSecret,
@JsonProperty("access_token") String accessToken) {
@JsonProperty("access_token") String accessToken,
@Nullable @JsonProperty("welcome_message") String welcomeMessage) {
this.welcomeMessage = welcomeMessage;

Preconditions.checkArgument(name != null && !name.isBlank(), "name cannot be blank");
Preconditions.checkArgument(
Expand All @@ -41,7 +46,15 @@ private WAMessengerConfig(

public static WAMessengerConfig of(String verifyToken, String appSecret, String accessToken) {
// human readability of the name only matters when it's coming from a config
return new WAMessengerConfig(UUID.randomUUID().toString(), verifyToken, appSecret, accessToken);
return new WAMessengerConfig(
UUID.randomUUID().toString(), verifyToken, appSecret, accessToken, null);
}

public static WAMessengerConfig of(
String verifyToken, String appSecret, String accessToken, @Nullable String welcomeMessage) {
// human readability of the name only matters when it's coming from a config
return new WAMessengerConfig(
UUID.randomUUID().toString(), verifyToken, appSecret, accessToken, welcomeMessage);
}

@Override
Expand All @@ -65,4 +78,8 @@ public WAMessageHandler toMessageHandler() {
public String accessToken() {
return accessToken;
}

public Optional<String> welcomeMessage() {
return Optional.ofNullable(welcomeMessage);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
@JsonSubTypes.Type(value = StickerWebhookMessage.class, name = "sticker"),
@JsonSubTypes.Type(value = UnknownWebhookMessage.class, name = "unknown"),
@JsonSubTypes.Type(value = VideoWebhookMessage.class, name = "video"),
@JsonSubTypes.Type(value = WelcomeWebhookMessage.class, name = "request_welcome"),
})
public interface WebhookMessage {

Expand Down Expand Up @@ -100,6 +101,7 @@ enum WebhookMessageType {
STICKER,
SYSTEM,
VIDEO,
REQUEST_WELCOME,
UNKNOWN;

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
*
* 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 com.meta.cp4m.Identifier;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import org.checkerframework.checker.nullness.qual.Nullable;

public class WelcomeWebhookMessage implements ReferableWebhookMessage {
private final @Nullable Referral referral;
private final @Nullable WebhookMessageContext context;
private final Collection<Error> errors;
private final Identifier from;
private final Identifier id;
private final Instant timestamp;

@JsonCreator
public WelcomeWebhookMessage(
@JsonProperty("context") @Nullable WebhookMessageContext context,
@JsonProperty("error") Collection<Error> errors,
@JsonProperty("from") String from,
@JsonProperty("id") String id,
@JsonProperty("timestamp") long timestamp,
@JsonProperty("referral") @Nullable Referral referral) {
this.referral = referral;
this.context = context;
this.errors = errors == null ? Collections.emptyList() : errors;
this.from = Identifier.from(Objects.requireNonNull(from));
this.id = Identifier.from(Objects.requireNonNull(id));
this.timestamp = Instant.ofEpochSecond(timestamp);
}

@Override
public Optional<Referral> referral() {
return Optional.ofNullable(referral);
}

@Override
public Optional<WebhookMessageContext> context() {
return Optional.ofNullable(context);
}

@Override
public Collection<Error> errors() {
return errors;
}

@Override
public Identifier from() {
return from;
}

@Override
public Identifier id() {
return id;
}

@Override
public Instant timestamp() {
return timestamp;
}

@Override
public WebhookMessageType type() {
return WebhookMessageType.REQUEST_WELCOME;
}
}
17 changes: 15 additions & 2 deletions src/test/java/com/meta/cp4m/message/ServiceTestHarness.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.net.URIBuilder;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.common.reflection.qual.NewInstance;
import org.checkerframework.common.returnsreceiver.qual.This;

public class ServiceTestHarness<T extends Message> {
Expand Down Expand Up @@ -48,13 +49,25 @@ private ServiceTestHarness(
public static ServiceTestHarness<WAMessage> newWAServiceTestHarness() {
ChatStore<WAMessage> chatStore = MemoryStoreConfig.of(1, 1).toStore();
DummyLLMPlugin<WAMessage> llmPlugin = new DummyLLMPlugin<>("dummy plugin response text");
WAMessageHandler handler =
WAMessengerConfig.of(VERIFY_TOKEN, APP_SECRET, ACCESS_TOKEN).toMessageHandler();
WAMessengerConfig config = WAMessengerConfig.of(VERIFY_TOKEN, APP_SECRET, ACCESS_TOKEN);
WAMessageHandler handler = config.toMessageHandler();
ServiceTestHarness<WAMessage> harness = new ServiceTestHarness<>(chatStore, handler, llmPlugin);
handler.baseUrl(harness.webserverURI());
return harness;
}

public @NewInstance ServiceTestHarness<T> withHandler(MessageHandler<T> handler) {
this.stop();
ServiceTestHarness<T> harness =
new ServiceTestHarness<>(this.chatStore, handler, this.llmPlugin);
switch (handler) {
case WAMessageHandler w -> w.baseUrl(harness.webserverURI());
case FBMessageHandler fb -> fb.baseURLFactory(ignored -> harness.webserverURI());
default -> {}
}
return harness;
}

public Request post() {
return Request.post(serviceURI());
}
Expand Down
49 changes: 46 additions & 3 deletions src/test/java/com/meta/cp4m/message/WAMessageHandlerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import static org.assertj.core.api.Assertions.assertThat;

import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.meta.cp4m.DummyWebServer.ReceivedRequest;
import com.meta.cp4m.Identifier;
import com.meta.cp4m.message.webhook.whatsapp.Utils;
Expand All @@ -22,8 +23,10 @@
import java.util.stream.Stream;
import org.apache.hc.client5.http.fluent.Response;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;

class WAMessageHandlerTest {

Expand Down Expand Up @@ -104,13 +107,51 @@ class WAMessageHandlerTest {
private final ServiceTestHarness<WAMessage> harness =
ServiceTestHarness.newWAServiceTestHarness();

@BeforeEach
void setUp() {
@ParameterizedTest
@NullSource
@ValueSource(strings = {"Hello Worldy!!", ""})
void welcomeMessage(final @Nullable String welcomeMessage)
throws IOException, InterruptedException {
ServiceTestHarness<WAMessage> harness =
this.harness.withHandler(
WAMessengerConfig.of(
this.harness.verifyToken(),
this.harness.appSecret(),
this.harness.accessToken(),
welcomeMessage)
.toMessageHandler());
harness.start();
ObjectNode webhookPayload = (ObjectNode) MAPPER.readTree(VALID);
ObjectNode msg =
(ObjectNode)
webhookPayload
.get("entry")
.get(0)
.get("changes")
.get(0)
.get("value")
.get("messages")
.get(0);
msg.remove("text");
msg.put("type", "request_welcome");
harness.post(MAPPER.writeValueAsString(webhookPayload)).execute();
@Nullable ReceivedRequest res = harness.pollWebserver(200);
if (welcomeMessage == null) {
assertThat(res).isNull();
} else {
assertThat(res).isNotNull();
assertThat(MAPPER.readTree(res.body()))
.isEqualTo(
MAPPER.readTree(
"{\"recipient_type\":\"individual\",\"messaging_product\":\"whatsapp\",\"type\":\"text\",\"to\":\"123456123\",\"text\":{\"body\":\""
+ welcomeMessage
+ "\"}}"));
}
}

@Test
void valid() throws IOException, InterruptedException {
harness.start();
Response request = harness.post(VALID).execute();
assertThat(request.returnResponse().getCode()).isEqualTo(200);

Expand Down Expand Up @@ -156,6 +197,7 @@ void valid() throws IOException, InterruptedException {

@Test
void doesNotSendNonTextMessages() throws IOException, InterruptedException {
harness.start();
harness.llmPlugin().addResponseToSend(new Payload.Image(new byte[0], "image/jpeg"));
Response request = harness.post(VALID).execute();
assertThat(request.returnResponse().getCode()).isEqualTo(200);
Expand All @@ -173,6 +215,7 @@ void doesNotSendNonTextMessages() throws IOException, InterruptedException {

@Test
void noMessages() throws IOException, InterruptedException {
harness.start();
Response request = harness.post(NO_MESSAGES).execute();
assertThat(request.returnResponse().getCode()).isEqualTo(200);
assertThat(harness.pollWebserver(250)).isNull();
Expand Down

0 comments on commit e704481

Please sign in to comment.