From 997ba46831aef41beca0d00f7860a6140e78ff0e Mon Sep 17 00:00:00 2001 From: Hunter Jackson Date: Mon, 18 Dec 2023 11:12:56 -0500 Subject: [PATCH 1/6] get code back to passing all tests --- CODE_OF_CONDUCT.md | 2 +- src/main/java/com/meta/cp4m/Service.java | 43 ++++++++++ .../java/com/meta/cp4m/ServicesRunner.java | 74 ++++++++++++++-- .../meta/cp4m/message/FBMessageHandler.java | 85 ++++++++++++++++++- .../com/meta/cp4m/message/MessageHandler.java | 5 ++ .../meta/cp4m/message/MetaHandlerUtils.java | 42 +++++++++ .../com/meta/cp4m/message/RequestHandler.java | 15 ++++ .../meta/cp4m/message/RequestProcessor.java | 19 +++++ .../meta/cp4m/message/WAMessageHandler.java | 64 +++++++++++++- .../java/com/meta/cp4m/routing/Acceptor.java | 25 ++++++ .../java/com/meta/cp4m/routing/Handler.java | 19 +++++ .../java/com/meta/cp4m/routing/Route.java | 15 ++++ .../com/meta/cp4m/routing/RouteRegistrar.java | 22 +++++ .../cp4m/llm/HuggingFaceLlamaPluginTest.java | 2 +- .../com/meta/cp4m/llm/OpenAIPluginTest.java | 2 +- ...st.java => FBMessageRouteDetailsTest.java} | 25 +++--- ...st.java => WAMessageRouteDetailsTest.java} | 4 +- 17 files changed, 435 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/meta/cp4m/message/RequestHandler.java create mode 100644 src/main/java/com/meta/cp4m/message/RequestProcessor.java create mode 100644 src/main/java/com/meta/cp4m/routing/Acceptor.java create mode 100644 src/main/java/com/meta/cp4m/routing/Handler.java create mode 100644 src/main/java/com/meta/cp4m/routing/Route.java create mode 100644 src/main/java/com/meta/cp4m/routing/RouteRegistrar.java rename src/test/java/com/meta/cp4m/message/{FBMessageHandlerTest.java => FBMessageRouteDetailsTest.java} (95%) rename src/test/java/com/meta/cp4m/message/{WAMessageHandlerTest.java => WAMessageRouteDetailsTest.java} (97%) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 08b500a..1b4e4bd 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -33,7 +33,7 @@ Examples of unacceptable behavior by participants include: ## Our Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable +Project maintainers are responsible for clarifying the standards of acceptor behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. diff --git a/src/main/java/com/meta/cp4m/Service.java b/src/main/java/com/meta/cp4m/Service.java index 1cb1d68..eadd21b 100644 --- a/src/main/java/com/meta/cp4m/Service.java +++ b/src/main/java/com/meta/cp4m/Service.java @@ -11,13 +11,19 @@ import com.meta.cp4m.llm.LLMPlugin; import com.meta.cp4m.message.Message; import com.meta.cp4m.message.MessageHandler; +import com.meta.cp4m.message.RequestProcessor; import com.meta.cp4m.message.ThreadState; +import com.meta.cp4m.routing.Acceptor; +import com.meta.cp4m.routing.Handler; +import com.meta.cp4m.routing.Route; import com.meta.cp4m.store.ChatStore; import io.javalin.Javalin; import io.javalin.http.Context; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,6 +55,25 @@ void handle(Context ctx) { } } + void handler(Context ctx, IN in, RequestProcessor processor) { + List messages = null; + try { + messages = processor.process(ctx, in); + } catch (Exception e) { + LOGGER + .atError() + .addKeyValue("body", ctx.body()) + .addKeyValue("headers", ctx.headerMap()) + .setMessage("unable to process request") + .log(); + } + // TODO: once we have a non-volatile store, on startup send stored but not replied to messages + for (T m : messages) { + ThreadState thread = store.add(m); + executorService.submit(() -> execute(thread)); + } + } + public void register(Javalin app) { handler.handlers().forEach(m -> app.addHandler(m, path, this::handle)); } @@ -79,4 +104,22 @@ private void execute(ThreadState thread) { LOGGER.error("an error occurred while attempting to respond", e); } } + + private Route toRoute(MessageHandler.RouteDetails routeDetails) { + return new Route<>( + path, + routeDetails.handlerType(), + routeDetails.acceptor(), + (ctx, in) -> handler(ctx, in, routeDetails.requestProcessor())); + } + + List> routes() { + List> routeDetails = handler.routeDetails(); + List> routes = new ArrayList<>(routeDetails.size()); + for (MessageHandler.RouteDetails routeDetail : routeDetails) { + Route route = toRoute(routeDetail); + routes.add(route); + } + return routes; + } } diff --git a/src/main/java/com/meta/cp4m/ServicesRunner.java b/src/main/java/com/meta/cp4m/ServicesRunner.java index 4355cda..b01c4db 100644 --- a/src/main/java/com/meta/cp4m/ServicesRunner.java +++ b/src/main/java/com/meta/cp4m/ServicesRunner.java @@ -9,16 +9,23 @@ package com.meta.cp4m; import com.google.common.base.Preconditions; +import com.meta.cp4m.routing.Route; import io.javalin.Javalin; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; + +import java.util.*; + +import io.javalin.http.BadRequestResponse; +import io.javalin.http.Context; +import io.javalin.http.HandlerType; import org.checkerframework.common.returnsreceiver.qual.This; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ServicesRunner implements AutoCloseable { + + private static final Logger LOGGER = LoggerFactory.getLogger(ServicesRunner.class); private final Javalin app = Javalin.create(); - private final Set> services = new HashSet<>(); + private final Set> services = new LinkedHashSet<>(); private boolean started = false; private int port = 8080; @@ -28,7 +35,59 @@ public static ServicesRunner newInstance() { return new ServicesRunner(); } + private boolean didAcceptAndHandle(Context ctx, Route route) { + Optional acceptorOutput = route.acceptor().accept(ctx); + if (acceptorOutput.isPresent()) { + try { + route.handler().handle(ctx, acceptorOutput.get()); + } catch (Exception e) { + throw new BadRequestResponse("Unable to process request"); + } + return true; + } + return false; + } + + /** + * Find the first route that will accept this payload and then handle the payload + * + * @param ctx context from Javalin + * @param routes the routes to check for acceptability and process if accepted + */ + private void routeSelectorAndHandler(Context ctx, List> routes) { + for (Route route : routes) { + if (didAcceptAndHandle(ctx, route)) { + return; + } + } + LOGGER + .atError() + .setMessage("Unable to handle incoming webhook") + .addKeyValue("body", ctx.body()) + .addKeyValue("headers", ctx.headerMap()) + .log(); + throw new BadRequestResponse("unable to handle webhook"); + } + public @This ServicesRunner start() { + record RouteGroup(String path, HandlerType handlerType) {} + Map>> routeGroups = new HashMap<>(); + for (Service s : services) { // this is not a stream because order matters here + s.routes() + .forEach( + r -> + routeGroups + .computeIfAbsent( + new RouteGroup(r.path(), r.handlerType()), k -> new ArrayList<>()) + .add(r)); + } + routeGroups.forEach( + (routeGroup, routes) -> + app.addHandler( + routeGroup.handlerType(), + routeGroup.path(), + ctx -> this.routeSelectorAndHandler(ctx, routes))); + if (!started) { started = true; app.start(port); @@ -38,9 +97,8 @@ public static ServicesRunner newInstance() { public @This ServicesRunner service(Service service) { Preconditions.checkState(!started, "cannot add service, server already started"); - if (services.add(service)) { - service.register(app); - } + + services.add(service); return this; } diff --git a/src/main/java/com/meta/cp4m/message/FBMessageHandler.java b/src/main/java/com/meta/cp4m/message/FBMessageHandler.java index 2f869f0..2f2da79 100644 --- a/src/main/java/com/meta/cp4m/message/FBMessageHandler.java +++ b/src/main/java/com/meta/cp4m/message/FBMessageHandler.java @@ -9,6 +9,7 @@ package com.meta.cp4m.message; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -88,7 +89,7 @@ public List processRequest(Context ctx) { } catch (JsonProcessingException | NullPointerException e) { LOGGER .atWarn() - .setMessage("Unable to parse message form Meta webhook") + .setMessage("Unable to parse message from Meta webhook") .setCause(e) .addKeyValue("body", ctx.body()) .addKeyValue("headers", ctx.headerMap()) @@ -183,6 +184,59 @@ private List postHandler(Context ctx) throws JsonProcessingException return output; } + private List postHandler(Context ctx, JsonNode body) { + JsonNode entries = body.get("entry"); + ArrayList output = new ArrayList<>(); + for (JsonNode entry : entries) { + @Nullable JsonNode messaging = entry.get("messaging"); + if (messaging == null) { + continue; + } + for (JsonNode message : messaging) { + + Identifier senderId = Identifier.from(message.get("sender").get("id").asLong()); + Identifier recipientId = Identifier.from(message.get("recipient").get("id").asLong()); + Instant timestamp = Instant.ofEpochMilli(message.get("timestamp").asLong()); + @Nullable JsonNode messageObject = message.get("message"); + if (messageObject != null) { + // https://developers.facebook.com/docs/messenger-platform/reference/webhook-events/messages + Identifier messageId = Identifier.from(messageObject.get("mid").textValue()); + if (messageDeduplicator.addAndGetIsDuplicate(messageId)) { + continue; + } + + @Nullable JsonNode textObject = messageObject.get("text"); + if (textObject != null && textObject.isTextual()) { + FBMessage m = + new FBMessage( + timestamp, + messageId, + senderId, + recipientId, + textObject.textValue(), + Message.Role.USER); + output.add(m); + } else { + LOGGER + .atWarn() + .setMessage("received message without text, unable to handle this") + .addKeyValue("body", body) + .log(); + } + } else { + LOGGER + .atWarn() + .setMessage( + "received a message without a 'message' key, unable to handle this message type") + .addKeyValue("body", body) + .log(); + } + } + } + + return output; + } + @TestOnly public @This FBMessageHandler baseURLFactory(Function baseURLFactory) { this.baseURLFactory = Objects.requireNonNull(baseURLFactory); @@ -237,4 +291,33 @@ private void send(String message, Identifier recipient, Identifier sender) throw public Collection handlers() { return List.of(HandlerType.GET, HandlerType.POST); } + + @Override + public List> routeDetails() { + RouteDetails postDetails = + new RouteDetails<>( + HandlerType.POST, + ctx -> { + @Nullable String contentType = ctx.contentType(); + if (contentType != null + && ContentType.parse(contentType).isSameMimeType(ContentType.APPLICATION_JSON) + && MetaHandlerUtils.postHeaderValid(ctx, appSecret)) { + JsonNode body; + try { + body = MAPPER.readTree(ctx.body()); + } catch (JsonProcessingException e) { + throw new BadRequestResponse("unable to parse body"); + } + // TODO: need better validation + @Nullable JsonNode objectNode = body.get("object"); + if (objectNode != null && objectNode.textValue().equals("page")) { + return Optional.of(body); + } + } + return Optional.empty(); + }, + this::postHandler); + + return List.of(MetaHandlerUtils.subscriptionVerificationRouteDetails(verifyToken), postDetails); + } } diff --git a/src/main/java/com/meta/cp4m/message/MessageHandler.java b/src/main/java/com/meta/cp4m/message/MessageHandler.java index 7b852fc..1ab16ee 100644 --- a/src/main/java/com/meta/cp4m/message/MessageHandler.java +++ b/src/main/java/com/meta/cp4m/message/MessageHandler.java @@ -8,6 +8,7 @@ package com.meta.cp4m.message; +import com.meta.cp4m.routing.Acceptor; import io.javalin.http.Context; import io.javalin.http.HandlerType; import java.io.IOException; @@ -15,6 +16,8 @@ import java.util.List; public interface MessageHandler { + record RouteDetails( + HandlerType handlerType, Acceptor acceptor, RequestProcessor requestProcessor) {} /** * Process incoming requests from the messaging service, including messages from the user. @@ -35,4 +38,6 @@ public interface MessageHandler { * @return The different {@link HandlerType}s that this handler expects to receive */ Collection handlers(); + + List> routeDetails(); } diff --git a/src/main/java/com/meta/cp4m/message/MetaHandlerUtils.java b/src/main/java/com/meta/cp4m/message/MetaHandlerUtils.java index 85b2cdf..6fc44d1 100644 --- a/src/main/java/com/meta/cp4m/message/MetaHandlerUtils.java +++ b/src/main/java/com/meta/cp4m/message/MetaHandlerUtils.java @@ -13,9 +13,16 @@ import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; + +import io.javalin.http.HandlerType; import org.apache.hc.client5.http.utils.Hex; +import org.checkerframework.checker.nullness.qual.Nullable; class MetaHandlerUtils { static void subscriptionVerification(Context ctx, String verifyToken) { @@ -27,6 +34,26 @@ static void subscriptionVerification(Context ctx, String verifyToken) { ctx.result(String.valueOf(challenge)); } + static + MessageHandler.RouteDetails subscriptionVerificationRouteDetails( + String verifyToken) { + return new MessageHandler.RouteDetails<>( + HandlerType.GET, + ctx -> + // validateSubscription handles putting challenge into context response if it succeeds + { + if (Objects.equals(ctx.queryParam("hub.mode"), "subscribe") + && Objects.equals(ctx.queryParam("hub.verify_token"), verifyToken)) { + return Optional.of(ctx.queryParamAsClass("hub.challenge", Integer.class).get()); + } + return Optional.empty(); + }, + (ctx, challenge) -> { + ctx.result(String.valueOf(challenge)); + return List.of(); + }); + } + static String hmac(String body, String appSecret) { Mac sha256HMAC; SecretKeySpec secretKey; @@ -65,4 +92,19 @@ static void postHeaderValidator(Context ctx, String appSecret) { "X-Hub-Signature-256 could not be validated") .getOrThrow(ignored -> new ForbiddenResponse("X-Hub-Signature-256 could not be validated")); } + + static boolean postHeaderValid(Context ctx, String appSecret) { + @Nullable String sig = ctx.headerMap().get("X-Hub-Signature-256"); + if (sig == null) { + return false; + } + + String[] hashParts = sig.strip().split("="); + if (hashParts.length != 2) { + return false; + } + + String calculatedHmac = hmac(ctx.body(), appSecret); + return hashParts[1].equals(calculatedHmac); + } } diff --git a/src/main/java/com/meta/cp4m/message/RequestHandler.java b/src/main/java/com/meta/cp4m/message/RequestHandler.java new file mode 100644 index 0000000..097bbec --- /dev/null +++ b/src/main/java/com/meta/cp4m/message/RequestHandler.java @@ -0,0 +1,15 @@ +/* + * + * 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; + +import com.meta.cp4m.routing.Acceptor; +import io.javalin.http.HandlerType; + +public record RequestHandler( + HandlerType type, Acceptor acceptor, RequestProcessor processor) {} diff --git a/src/main/java/com/meta/cp4m/message/RequestProcessor.java b/src/main/java/com/meta/cp4m/message/RequestProcessor.java new file mode 100644 index 0000000..ba29c01 --- /dev/null +++ b/src/main/java/com/meta/cp4m/message/RequestProcessor.java @@ -0,0 +1,19 @@ +/* + * + * 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; + +import io.javalin.http.Context; + +import java.util.List; + +@FunctionalInterface +public interface RequestProcessor { + + List process(Context ctx, IN in) throws Exception; +} diff --git a/src/main/java/com/meta/cp4m/message/WAMessageHandler.java b/src/main/java/com/meta/cp4m/message/WAMessageHandler.java index a2e7ecb..26f617c 100644 --- a/src/main/java/com/meta/cp4m/message/WAMessageHandler.java +++ b/src/main/java/com/meta/cp4m/message/WAMessageHandler.java @@ -21,16 +21,14 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; +import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Function; import org.apache.hc.client5.http.fluent.Request; 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.returnsreceiver.qual.This; import org.jetbrains.annotations.TestOnly; import org.slf4j.Logger; @@ -108,6 +106,39 @@ public List processRequest(Context ctx) { throw new UnsupportedOperationException("Only accepting get and post methods"); } + private List post(Context ctx, WebhookPayload payload) { + List waMessages = new ArrayList<>(); + payload.entry().stream() + .flatMap(e -> e.changes().stream()) + .forEach( + change -> { + Identifier phoneNumberId = change.value().metadata().phoneNumberId(); + for (WebhookMessage message : change.value().messages()) { + if (messageDeduplicator.addAndGetIsDuplicate(message.id())) { + continue; // message is a duplicate + } + if (message.type() != WebhookMessage.WebhookMessageType.TEXT) { + LOGGER.warn( + "received message of type '" + + message.type() + + "', only able to handle text messages at this time"); + continue; + } + TextWebhookMessage textMessage = (TextWebhookMessage) message; + waMessages.add( + new WAMessage( + message.timestamp(), + message.id(), + message.from(), + phoneNumberId, + textMessage.text().body(), + Message.Role.USER)); + readExecutor.execute(() -> markRead(phoneNumberId, textMessage.id().toString())); + } + }); + return waMessages; + } + List postHandler(Context ctx) { MetaHandlerUtils.postHeaderValidator(ctx, appSecret); String bodyString = ctx.body(); @@ -193,6 +224,31 @@ public Collection handlers() { return List.of(HandlerType.GET, HandlerType.POST); } + @Override + public List> routeDetails() { + RouteDetails postDetails = + new RouteDetails<>( + HandlerType.POST, + ctx -> { + @Nullable String contentType = ctx.contentType(); + if (contentType != null && + ContentType.parse(contentType).isSameMimeType(ContentType.APPLICATION_JSON) + && MetaHandlerUtils.postHeaderValid(ctx, appSecret)) { + String bodyString = ctx.body(); + WebhookPayload payload; + try { + payload = MAPPER.readValue(bodyString, WebhookPayload.class); + return Optional.of(payload); + } catch (Exception e) { + return Optional.empty(); + } + } + return Optional.empty(); + }, + this::post); + return List.of(MetaHandlerUtils.subscriptionVerificationRouteDetails(verifyToken), postDetails); + } + private void markRead(Identifier phoneNumberId, String messageId) { ObjectNode body = MAPPER diff --git a/src/main/java/com/meta/cp4m/routing/Acceptor.java b/src/main/java/com/meta/cp4m/routing/Acceptor.java new file mode 100644 index 0000000..f62cdfa --- /dev/null +++ b/src/main/java/com/meta/cp4m/routing/Acceptor.java @@ -0,0 +1,25 @@ +/* + * + * 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.routing; + +import com.fasterxml.jackson.databind.JsonNode; +import com.meta.cp4m.message.Message; +import io.javalin.http.Context; + +import java.util.Optional; + +@FunctionalInterface +public interface Acceptor { + /** + * @param ctx contex of an incoming message on a webhook + * @return not empty if the {@link com.meta.cp4m.message.MessageHandler} can accept the message, + * empty otherwise + */ + Optional accept(Context ctx); +} diff --git a/src/main/java/com/meta/cp4m/routing/Handler.java b/src/main/java/com/meta/cp4m/routing/Handler.java new file mode 100644 index 0000000..1fabbb2 --- /dev/null +++ b/src/main/java/com/meta/cp4m/routing/Handler.java @@ -0,0 +1,19 @@ +/* + * + * 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.routing; + +import io.javalin.http.Context; + +import java.util.List; + +@FunctionalInterface +public interface Handler { + + void handle(Context ctx, I input); +} diff --git a/src/main/java/com/meta/cp4m/routing/Route.java b/src/main/java/com/meta/cp4m/routing/Route.java new file mode 100644 index 0000000..d50e3c6 --- /dev/null +++ b/src/main/java/com/meta/cp4m/routing/Route.java @@ -0,0 +1,15 @@ +/* + * + * 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.routing; + +import com.meta.cp4m.message.Message; +import io.javalin.http.HandlerType; + +public record Route( + String path, HandlerType handlerType, Acceptor acceptor, Handler handler) {} diff --git a/src/main/java/com/meta/cp4m/routing/RouteRegistrar.java b/src/main/java/com/meta/cp4m/routing/RouteRegistrar.java new file mode 100644 index 0000000..4869905 --- /dev/null +++ b/src/main/java/com/meta/cp4m/routing/RouteRegistrar.java @@ -0,0 +1,22 @@ +/* + * + * 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.routing; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class RouteRegistrar { + private final Map>> routeAcceptors = new HashMap<>(); + + void register(Route route) { + routeAcceptors.computeIfAbsent(route.path(), ignored -> new ArrayList<>()).add(route); + } +} diff --git a/src/test/java/com/meta/cp4m/llm/HuggingFaceLlamaPluginTest.java b/src/test/java/com/meta/cp4m/llm/HuggingFaceLlamaPluginTest.java index 4802584..4c525ba 100644 --- a/src/test/java/com/meta/cp4m/llm/HuggingFaceLlamaPluginTest.java +++ b/src/test/java/com/meta/cp4m/llm/HuggingFaceLlamaPluginTest.java @@ -308,7 +308,7 @@ void inPipeline() throws IOException, URISyntaxException, InterruptedException { // TODO: create test harness Request request = - FBMessageHandlerTest.createMessageRequest(FBMessageHandlerTest.SAMPLE_MESSAGE, runner); + FBMessageRouteDetailsTest.createMessageRequest(FBMessageRouteDetailsTest.SAMPLE_MESSAGE, runner); HttpResponse response = request.execute().returnResponse(); assertThat(response.getCode()).isEqualTo(200); @Nullable OutboundRequest or = HuggingFaceLlamaRequests.poll(500, TimeUnit.MILLISECONDS); diff --git a/src/test/java/com/meta/cp4m/llm/OpenAIPluginTest.java b/src/test/java/com/meta/cp4m/llm/OpenAIPluginTest.java index d11aee5..8f59d3b 100644 --- a/src/test/java/com/meta/cp4m/llm/OpenAIPluginTest.java +++ b/src/test/java/com/meta/cp4m/llm/OpenAIPluginTest.java @@ -251,7 +251,7 @@ void inPipeline() throws IOException, URISyntaxException, InterruptedException { // TODO: create test harness Request request = - FBMessageHandlerTest.createMessageRequest(FBMessageHandlerTest.SAMPLE_MESSAGE, runner); + FBMessageRouteDetailsTest.createMessageRequest(FBMessageRouteDetailsTest.SAMPLE_MESSAGE, runner); HttpResponse response = request.execute().returnResponse(); assertThat(response.getCode()).isEqualTo(200); @Nullable OutboundRequest or = openAIRequests.poll(500, TimeUnit.MILLISECONDS); diff --git a/src/test/java/com/meta/cp4m/message/FBMessageHandlerTest.java b/src/test/java/com/meta/cp4m/message/FBMessageRouteDetailsTest.java similarity index 95% rename from src/test/java/com/meta/cp4m/message/FBMessageHandlerTest.java rename to src/test/java/com/meta/cp4m/message/FBMessageRouteDetailsTest.java index 8b335c7..428d72f 100644 --- a/src/test/java/com/meta/cp4m/message/FBMessageHandlerTest.java +++ b/src/test/java/com/meta/cp4m/message/FBMessageRouteDetailsTest.java @@ -50,11 +50,12 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -public class FBMessageHandlerTest { +public class FBMessageRouteDetailsTest { /** Example message collected directly from the messenger webhook */ public static final String SAMPLE_MESSAGE = "{\"object\":\"page\",\"entry\":[{\"id\":\"106195825075770\",\"time\":1692813219204,\"messaging\":[{\"sender\":{\"id\":\"6357858494326947\"},\"recipient\":{\"id\":\"106195825075770\"},\"timestamp\":1692813218705,\"message\":{\"mid\":\"m_kT_mWOSYh_eK3kF8chtyCWfcD9-gomvu4mhaMFQl-gt4D3LjORi6k3BXD6_x9a-FOUt-D2LFuywJN6HfrpAnDg\",\"text\":\"asdfa\"}}]}]}"; + private static final ObjectMapper MAPPER = new ObjectMapper(); private static final String SAMPLE_MESSAGE_HMAC = "sha256=8620d18213fa2612d16117b65168ef97404fa13189528014c5362fec31215985"; @@ -89,7 +90,10 @@ private static Request createMessageRequest( URI uri = URIBuilder.localhost().setScheme("http").setPort(runner.port()).appendPath(path).build(); - Request request = Request.post(uri).bodyString(body, ContentType.APPLICATION_JSON); + Request request = + Request.post(uri) + .bodyString(body, ContentType.APPLICATION_JSON) + .setHeader("Content-Type", "application/json"); if (calculateHmac) { String hmac = messageHandler.hmac(body); request.setHeader("X-Hub-Signature-256", "sha256=" + hmac); @@ -134,7 +138,7 @@ static Stream requestFactory() throws JsonProcessingException { true), new TestArgument( "a non page object type", - 200, + 400, r -> createMessageRequest("{\"object\": \"not a page\"}", r), false), new TestArgument( @@ -201,14 +205,14 @@ static Stream requestFactory() throws JsonProcessingException { r), false), new TestArgument( - "missing hmac", 403, r -> createMessageRequest(SAMPLE_MESSAGE, r, false), false), + "missing hmac", 400, r -> createMessageRequest(SAMPLE_MESSAGE, r, false), false), new TestArgument( "invalid json", 400, r -> createMessageRequest("invalid_json.", r), false), new TestArgument( "valid json, invalid body", 400, r -> createMessageRequest("{}", r), false), new TestArgument( "invalid hmac", - 403, + 400, r -> createMessageRequest(SAMPLE_MESSAGE, r, false) .addHeader("X-Hub-Signature-256", "abcdef0123456789"), @@ -247,12 +251,13 @@ void tearDown() { @Test void validation() throws IOException, URISyntaxException { - String token = "243af3c6-9994-4869-ae13-ad61a38323f5"; // this is fake - int challenge = 1158201444; + final String pageToken = "243af3c6-9994-4869-ae13-ad61a38323f5"; // this is fake + final int challenge = 1158201444; + final String verifyToken = "oamnw9230rjadoia"; Service service = new Service<>( MemoryStoreConfig.of(1, 1).toStore(), - new FBMessageHandler("0", token, "dummy"), + new FBMessageHandler(verifyToken, pageToken, "dummy"), new DummyLLMPlugin("this is a dummy message"), "/testfbmessage"); final ServicesRunner runner = ServicesRunner.newInstance().service(service).port(0); @@ -262,7 +267,7 @@ void validation() throws IOException, URISyntaxException { ImmutableMap.builder() .put("hub.mode", "subscribe") .put("hub.challenge", Integer.toString(challenge)) - .put("hub.verify_token", token) + .put("hub.verify_token", verifyToken) .build(); response = getRequest("testfbmessage", runner.port(), params); } @@ -299,7 +304,7 @@ void invalidMessage( String token = "243af3c6-9994-4869-ae13-ad61a38323f5"; // this is fake don't worry String secret = "f74a638462f975e9eadfcbb84e4aa06b"; // it's been rolled don't worry FBMessageHandler messageHandler = new FBMessageHandler("0", token, secret); - DummyLLMPlugin llmHandler = new DummyLLMPlugin("this is a dummy message"); + DummyLLMPlugin llmHandler = new DummyLLMPlugin<>("this is a dummy message"); MemoryStore memoryStore = MemoryStoreConfig.of(1, 1).toStore(); Service service = new Service<>(memoryStore, messageHandler, llmHandler, path); final ServicesRunner runner = ServicesRunner.newInstance().service(service).port(0); diff --git a/src/test/java/com/meta/cp4m/message/WAMessageHandlerTest.java b/src/test/java/com/meta/cp4m/message/WAMessageRouteDetailsTest.java similarity index 97% rename from src/test/java/com/meta/cp4m/message/WAMessageHandlerTest.java rename to src/test/java/com/meta/cp4m/message/WAMessageRouteDetailsTest.java index 5d8ed75..14fbb29 100644 --- a/src/test/java/com/meta/cp4m/message/WAMessageHandlerTest.java +++ b/src/test/java/com/meta/cp4m/message/WAMessageRouteDetailsTest.java @@ -25,7 +25,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class WAMessageHandlerTest { +class WAMessageRouteDetailsTest { static final String VALID = """ @@ -106,7 +106,7 @@ class WAMessageHandlerTest { private final ServiceTestHarness harness = ServiceTestHarness.newWAServiceTestHarness(); - WAMessageHandlerTest() throws JsonProcessingException {} + WAMessageRouteDetailsTest() throws JsonProcessingException {} @BeforeEach void setUp() { From da8bcf6157843498249e8b6d9e6cbde8c1c882de Mon Sep 17 00:00:00 2001 From: Hunter Jackson Date: Wed, 20 Dec 2023 11:40:28 -0500 Subject: [PATCH 2/6] allow for using multiple services with the same path --- .../meta/cp4m/message/WAMessageHandler.java | 6 - .../java/com/meta/cp4m/DummyWebServer.java | 87 +++++++++++ .../cp4m/llm/HuggingFaceLlamaPluginTest.java | 2 +- .../com/meta/cp4m/llm/OpenAIPluginTest.java | 2 +- ...ilsTest.java => FBMessageHandlerTest.java} | 18 ++- .../meta/cp4m/message/HandlerTestUtils.java | 59 ++++++++ .../meta/cp4m/message/MultiServiceTest.java | 140 ++++++++++++++++++ .../meta/cp4m/message/ServiceTestHarness.java | 82 +--------- ...ilsTest.java => WAMessageHandlerTest.java} | 10 +- 9 files changed, 308 insertions(+), 98 deletions(-) create mode 100644 src/test/java/com/meta/cp4m/DummyWebServer.java rename src/test/java/com/meta/cp4m/message/{FBMessageRouteDetailsTest.java => FBMessageHandlerTest.java} (98%) create mode 100644 src/test/java/com/meta/cp4m/message/HandlerTestUtils.java create mode 100644 src/test/java/com/meta/cp4m/message/MultiServiceTest.java rename src/test/java/com/meta/cp4m/message/{WAMessageRouteDetailsTest.java => WAMessageHandlerTest.java} (92%) diff --git a/src/main/java/com/meta/cp4m/message/WAMessageHandler.java b/src/main/java/com/meta/cp4m/message/WAMessageHandler.java index 26f617c..17217de 100644 --- a/src/main/java/com/meta/cp4m/message/WAMessageHandler.java +++ b/src/main/java/com/meta/cp4m/message/WAMessageHandler.java @@ -70,12 +70,6 @@ public class WAMessageHandler implements MessageHandler { } }; - public WAMessageHandler(String verifyToken, String appSecret, String accessToken) { - this.verifyToken = verifyToken; - this.appSecret = appSecret; - this.accessToken = accessToken; - } - public WAMessageHandler(WAMessengerConfig config) { this.verifyToken = config.verifyToken(); this.accessToken = config.accessToken(); diff --git a/src/test/java/com/meta/cp4m/DummyWebServer.java b/src/test/java/com/meta/cp4m/DummyWebServer.java new file mode 100644 index 0000000..c0621cb --- /dev/null +++ b/src/test/java/com/meta/cp4m/DummyWebServer.java @@ -0,0 +1,87 @@ +/* + * + * 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; + +import io.javalin.Javalin; +import io.javalin.http.HandlerType; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class DummyWebServer implements AutoCloseable { + private final Javalin javalin; + private final BlockingQueue receivedRequests = new LinkedBlockingDeque<>(); + + private DummyWebServer() { + this.javalin = + Javalin.create() + .addHandler( + HandlerType.GET, + "/", + ctx -> + receivedRequests.put( + new ReceivedRequest( + ctx.path(), + ctx.body(), + ctx.contentType(), + ctx.headerMap(), + ctx.queryParamMap()))) + .addHandler( + HandlerType.POST, + "/", + ctx -> + receivedRequests.put( + new ReceivedRequest( + ctx.path(), + ctx.body(), + ctx.contentType(), + ctx.headerMap(), + ctx.queryParamMap()))) + .start(0); + } + + public static DummyWebServer create() { + return new DummyWebServer(); + } + + public @Nullable ReceivedRequest poll() { + return receivedRequests.poll(); + } + + public @Nullable ReceivedRequest poll(long milliseconds) throws InterruptedException { + return receivedRequests.poll(milliseconds, TimeUnit.MILLISECONDS); + } + + public ReceivedRequest take(long milliseconds) throws InterruptedException { + return Objects.requireNonNull(receivedRequests.poll(milliseconds, TimeUnit.MILLISECONDS)); + } + + public int port() { + return javalin.port(); + } + + public Javalin javalin() { + return javalin; + } + + @Override + public void close() { + javalin.close(); + } + + public record ReceivedRequest( + String path, + String body, + @Nullable String contentType, + Map headerMap, + Map> stringListMap) {} +} diff --git a/src/test/java/com/meta/cp4m/llm/HuggingFaceLlamaPluginTest.java b/src/test/java/com/meta/cp4m/llm/HuggingFaceLlamaPluginTest.java index 4c525ba..4802584 100644 --- a/src/test/java/com/meta/cp4m/llm/HuggingFaceLlamaPluginTest.java +++ b/src/test/java/com/meta/cp4m/llm/HuggingFaceLlamaPluginTest.java @@ -308,7 +308,7 @@ void inPipeline() throws IOException, URISyntaxException, InterruptedException { // TODO: create test harness Request request = - FBMessageRouteDetailsTest.createMessageRequest(FBMessageRouteDetailsTest.SAMPLE_MESSAGE, runner); + FBMessageHandlerTest.createMessageRequest(FBMessageHandlerTest.SAMPLE_MESSAGE, runner); HttpResponse response = request.execute().returnResponse(); assertThat(response.getCode()).isEqualTo(200); @Nullable OutboundRequest or = HuggingFaceLlamaRequests.poll(500, TimeUnit.MILLISECONDS); diff --git a/src/test/java/com/meta/cp4m/llm/OpenAIPluginTest.java b/src/test/java/com/meta/cp4m/llm/OpenAIPluginTest.java index 8f59d3b..d11aee5 100644 --- a/src/test/java/com/meta/cp4m/llm/OpenAIPluginTest.java +++ b/src/test/java/com/meta/cp4m/llm/OpenAIPluginTest.java @@ -251,7 +251,7 @@ void inPipeline() throws IOException, URISyntaxException, InterruptedException { // TODO: create test harness Request request = - FBMessageRouteDetailsTest.createMessageRequest(FBMessageRouteDetailsTest.SAMPLE_MESSAGE, runner); + FBMessageHandlerTest.createMessageRequest(FBMessageHandlerTest.SAMPLE_MESSAGE, runner); HttpResponse response = request.execute().returnResponse(); assertThat(response.getCode()).isEqualTo(200); @Nullable OutboundRequest or = openAIRequests.poll(500, TimeUnit.MILLISECONDS); diff --git a/src/test/java/com/meta/cp4m/message/FBMessageRouteDetailsTest.java b/src/test/java/com/meta/cp4m/message/FBMessageHandlerTest.java similarity index 98% rename from src/test/java/com/meta/cp4m/message/FBMessageRouteDetailsTest.java rename to src/test/java/com/meta/cp4m/message/FBMessageHandlerTest.java index 428d72f..7ef044b 100644 --- a/src/test/java/com/meta/cp4m/message/FBMessageRouteDetailsTest.java +++ b/src/test/java/com/meta/cp4m/message/FBMessageHandlerTest.java @@ -50,7 +50,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -public class FBMessageRouteDetailsTest { +public class FBMessageHandlerTest { /** Example message collected directly from the messenger webhook */ public static final String SAMPLE_MESSAGE = @@ -59,15 +59,19 @@ public class FBMessageRouteDetailsTest { private static final ObjectMapper MAPPER = new ObjectMapper(); private static final String SAMPLE_MESSAGE_HMAC = "sha256=8620d18213fa2612d16117b65168ef97404fa13189528014c5362fec31215985"; - private static JsonNode PARSED_SAMPLE_MESSAGE; - private Javalin app; - private BlockingQueue requests; + public static final JsonNode PARSED_SAMPLE_MESSAGE; - @BeforeAll - static void beforeAll() throws JsonProcessingException { - PARSED_SAMPLE_MESSAGE = MAPPER.readTree(SAMPLE_MESSAGE); + static { + try { + PARSED_SAMPLE_MESSAGE = MAPPER.readTree(SAMPLE_MESSAGE); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } } + private Javalin app; + private BlockingQueue requests; + private static HttpResponse getRequest(String path, int port, Map params) throws IOException, URISyntaxException { URIBuilder uriBuilder = diff --git a/src/test/java/com/meta/cp4m/message/HandlerTestUtils.java b/src/test/java/com/meta/cp4m/message/HandlerTestUtils.java new file mode 100644 index 0000000..32b236b --- /dev/null +++ b/src/test/java/com/meta/cp4m/message/HandlerTestUtils.java @@ -0,0 +1,59 @@ +/* + * + * 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; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.meta.cp4m.Identifier; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.util.function.Function; +import org.apache.hc.client5.http.fluent.Request; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.net.URIBuilder; + +public final class HandlerTestUtils { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + // static class, do not instantiate + private HandlerTestUtils() {} + + public static Function baseURLFactory(String path, int port) { + return identifier -> { + try { + return URIBuilder.localhost().setPort(port).appendPath(path).setScheme("http").build(); + } catch (UnknownHostException | URISyntaxException e) { + throw new RuntimeException(e); + } + }; + } + + public static Function MessageRequestFactory( + Method method, String path, String appSecret, int port) + throws UnknownHostException, URISyntaxException { + Request request = + Request.create( + method, + URIBuilder.localhost().setScheme("http").appendPath(path).setPort(port).build()); + return jn -> { + try { + String body = MAPPER.writeValueAsString(jn); + return request + .bodyString(body, ContentType.APPLICATION_JSON) + .setHeader("X-Hub-Signature-256", "sha256=" + MetaHandlerUtils.hmac(body, appSecret)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }; + } +} diff --git a/src/test/java/com/meta/cp4m/message/MultiServiceTest.java b/src/test/java/com/meta/cp4m/message/MultiServiceTest.java new file mode 100644 index 0000000..694df78 --- /dev/null +++ b/src/test/java/com/meta/cp4m/message/MultiServiceTest.java @@ -0,0 +1,140 @@ +/* + * + * 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; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.meta.cp4m.DummyWebServer; +import com.meta.cp4m.DummyWebServer.ReceivedRequest; +import com.meta.cp4m.Identifier; +import com.meta.cp4m.Service; +import com.meta.cp4m.ServicesRunner; +import com.meta.cp4m.llm.DummyLLMPlugin; +import com.meta.cp4m.store.MemoryStore; +import com.meta.cp4m.store.MemoryStoreConfig; +import java.net.URI; +import java.util.List; +import java.util.function.Function; +import org.apache.hc.client5.http.fluent.Request; +import org.apache.hc.core5.http.Method; +import org.junit.jupiter.api.Test; + +public class MultiServiceTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static final String META_PATH = "/meta"; + private final DummyWebServer metaWebServer = DummyWebServer.create(); + + private final Function baseURLFactory = + HandlerTestUtils.baseURLFactory(META_PATH, metaWebServer.port()); + + @Test + void waAndFBTest() throws Exception { + final String path = "/path"; + final String fb1VerifyToken = "fb1VerifyToken"; + final String fb1AppSecret = "fb1AppSecret"; + final String fb1PageAccessToken = "fb1PageAccessToken"; + + MemoryStore fb1Store = MemoryStoreConfig.of(1, 1).toStore(); + FBMessageHandler fb1Handler = + FBMessengerConfig.of(fb1VerifyToken, fb1AppSecret, fb1PageAccessToken) + .toMessageHandler() + .baseURLFactory(baseURLFactory); + DummyLLMPlugin fb1Plugin = new DummyLLMPlugin<>("i'm a fb1 dummy"); + Service fb1Service = new Service<>(fb1Store, fb1Handler, fb1Plugin, path); + + final String fb2VerifyToken = "fb2VerifyToken"; + final String fb2AppSecret = "fb2AppSecret"; + final String fb2PageAccessToken = "fb2PageAccessToken"; + + MemoryStore fb2Store = MemoryStoreConfig.of(1, 1).toStore(); + FBMessageHandler fb2Handler = + FBMessengerConfig.of(fb2VerifyToken, fb2AppSecret, fb2PageAccessToken) + .toMessageHandler() + .baseURLFactory(baseURLFactory); + DummyLLMPlugin fb2Plugin = new DummyLLMPlugin<>("i'm a fb2 dummy"); + Service fb2Service = new Service<>(fb2Store, fb2Handler, fb2Plugin, path); + + final String wa1VerifyToken = "wa1VerifyToken"; + final String wa1AppSecret = "wa1AppSecret"; + final String wa1PageAccessToken = "wa1PageAccessToken"; + MemoryStore wa1Store = MemoryStoreConfig.of(1, 1).toStore(); + WAMessageHandler wa1Handler = + WAMessengerConfig.of(wa1VerifyToken, wa1AppSecret, wa1PageAccessToken) + .toMessageHandler() + .baseUrlFactory(baseURLFactory); + DummyLLMPlugin wa1Plugin = new DummyLLMPlugin<>("i'm a wa1 dummy"); + Service wa1Service = new Service<>(wa1Store, wa1Handler, wa1Plugin, path); + + final String wa2VerifyToken = "wa2VerifyToken"; + final String wa2AppSecret = "wa2AppSecret"; + final String wa2PageAccessToken = "wa2PageAccessToken"; + MemoryStore wa2Store = MemoryStoreConfig.of(1, 1).toStore(); + WAMessageHandler wa2Handler = + WAMessengerConfig.of(wa2VerifyToken, wa2AppSecret, wa2PageAccessToken) + .toMessageHandler() + .baseUrlFactory(baseURLFactory); + DummyLLMPlugin wa2Plugin = new DummyLLMPlugin<>("i'm a wa2 dummy"); + Service wa2Service = new Service<>(wa2Store, wa2Handler, wa2Plugin, path); + + ServicesRunner runner = + ServicesRunner.newInstance() + .service(fb1Service) + .service(fb2Service) + .service(wa1Service) + .service(wa2Service) + .port(0) + .start(); + + // FB1 test + Function fb1RequestFactory = + HandlerTestUtils.MessageRequestFactory(Method.POST, path, fb1AppSecret, runner.port()); + fb1RequestFactory.apply(FBMessageHandlerTest.PARSED_SAMPLE_MESSAGE).execute(); + fb1Plugin.take(500); + ReceivedRequest receivedRequest = metaWebServer.take(500); + assertThat(receivedRequest.path()).isEqualTo(META_PATH); + assertThat(receivedRequest.body()).contains("i'm a fb1 dummy"); + + // FB2 test + Function fb2RequestFactory = + HandlerTestUtils.MessageRequestFactory(Method.POST, path, fb2AppSecret, runner.port()); + fb2RequestFactory.apply(FBMessageHandlerTest.PARSED_SAMPLE_MESSAGE).execute(); + fb2Plugin.take(500); + receivedRequest = metaWebServer.take(500); + assertThat(receivedRequest.path()).isEqualTo(META_PATH); + assertThat(receivedRequest.body()).contains("i'm a fb2 dummy"); + + // WA1 test + Function wa1RequestFactory = + HandlerTestUtils.MessageRequestFactory(Method.POST, path, wa1AppSecret, runner.port()); + wa1RequestFactory.apply(MAPPER.readTree(WAMessageHandlerTest.VALID)).execute(); + wa1Plugin.take(500); + receivedRequest = metaWebServer.take(500); + ReceivedRequest receivedRequest2 = metaWebServer.take(500); + assertThat(receivedRequest.path()).isEqualTo(META_PATH); + assertThat(receivedRequest2.path()).isEqualTo(META_PATH); + assertThat(List.of(receivedRequest, receivedRequest2)) + .satisfiesOnlyOnce(r -> assertThat(r.body()).contains("i'm a wa1 dummy")); + + // WA2 test + Function wa2RequestFactory = + HandlerTestUtils.MessageRequestFactory(Method.POST, path, wa2AppSecret, runner.port()); + wa2RequestFactory.apply(MAPPER.readTree(WAMessageHandlerTest.VALID)).execute(); + wa2Plugin.take(500); + receivedRequest = metaWebServer.take(500); + receivedRequest2 = metaWebServer.take(500); + assertThat(receivedRequest.path()).isEqualTo(META_PATH); + assertThat(receivedRequest2.path()).isEqualTo(META_PATH); + assertThat(List.of(receivedRequest, receivedRequest2)) + .satisfiesOnlyOnce(r -> assertThat(r.body()).contains("i'm a wa2 dummy")); + } +} diff --git a/src/test/java/com/meta/cp4m/message/ServiceTestHarness.java b/src/test/java/com/meta/cp4m/message/ServiceTestHarness.java index 58dad01..b126949 100644 --- a/src/test/java/com/meta/cp4m/message/ServiceTestHarness.java +++ b/src/test/java/com/meta/cp4m/message/ServiceTestHarness.java @@ -8,26 +8,21 @@ package com.meta.cp4m.message; +import com.meta.cp4m.DummyWebServer; +import com.meta.cp4m.DummyWebServer.ReceivedRequest; import com.meta.cp4m.Service; import com.meta.cp4m.ServicesRunner; import com.meta.cp4m.llm.DummyLLMPlugin; import com.meta.cp4m.llm.LLMPlugin; import com.meta.cp4m.store.ChatStore; import com.meta.cp4m.store.MemoryStoreConfig; -import io.javalin.Javalin; -import io.javalin.http.HandlerType; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; -import java.util.Map; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.TimeUnit; import org.apache.hc.client5.http.fluent.Request; 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 { @@ -36,13 +31,12 @@ public class ServiceTestHarness { private static final String APP_SECRET = "test_app_secret"; private static final String SERVICE_PATH = "/testservice"; private static final String WEBSERVER_PATH = "/testserver"; - private final BlockingQueue receivedRequests = new ArrayBlockingQueue<>(1000); private final ChatStore chatStore; private final MessageHandler handler; private final LLMPlugin llmPlugin; private final Service service; private final ServicesRunner runner; - private final Javalin javalin; + private final DummyWebServer dummyWebServer = DummyWebServer.create(); private ServiceTestHarness( ChatStore chatStore, MessageHandler handler, LLMPlugin llmPlugin) { @@ -51,7 +45,6 @@ private ServiceTestHarness( this.llmPlugin = llmPlugin; this.service = new Service<>(chatStore, handler, llmPlugin, SERVICE_PATH); this.runner = ServicesRunner.newInstance().service(service); - javalin = newJavalin(); } public static ServiceTestHarness newWAServiceTestHarness() { @@ -64,46 +57,6 @@ public static ServiceTestHarness newWAServiceTestHarness() { return harness; } - public Service service() { - return service; - } - - public ServicesRunner runner() { - return runner; - } - - public Javalin javalin() { - return javalin; - } - - private Javalin newJavalin() { - Javalin javalin = Javalin.create(); - javalin - .addHandler( - HandlerType.GET, - "/", - ctx -> - receivedRequests.put( - new ReceivedRequest( - ctx.path(), - ctx.body(), - ctx.contentType(), - ctx.headerMap(), - ctx.queryParamMap()))) - .addHandler( - HandlerType.POST, - "/", - ctx -> - receivedRequests.put( - new ReceivedRequest( - ctx.path(), - ctx.body(), - ctx.contentType(), - ctx.headerMap(), - ctx.queryParamMap()))); - return javalin; - } - public Request post() { return Request.post(serviceURI()); } @@ -146,23 +99,14 @@ public URI serviceURI() { } } - public @NewInstance ServiceTestHarness withLLMPlugin(LLMPlugin plugin) { - return new ServiceTestHarness<>(chatStore, handler, plugin); - } - - public @NewInstance ServiceTestHarness withChatStore(ChatStore chatStore) { - return new ServiceTestHarness<>(chatStore, handler, llmPlugin); - } - public @This ServiceTestHarness start() { - javalin.start(0); runner.port(0).start(); return this; } public @This ServiceTestHarness stop() { runner.close(); - javalin.close(); + dummyWebServer.close(); return this; } @@ -203,22 +147,10 @@ public int servicePort() { } public int webserverPort() { - return javalin.port(); + return dummyWebServer.port(); } - public ServiceTestHarness.@Nullable ReceivedRequest pollWebserver(long milliseconds) - throws InterruptedException { - return receivedRequests.poll(milliseconds, TimeUnit.MILLISECONDS); + public @Nullable ReceivedRequest pollWebserver(long milliseconds) throws InterruptedException { + return dummyWebServer.poll(milliseconds); } - - public ServiceTestHarness.@Nullable ReceivedRequest pollWebserver() { - return receivedRequests.poll(); - } - - public record ReceivedRequest( - String path, - String body, - @Nullable String contentType, - Map headerMap, - Map> stringListMap) {} } diff --git a/src/test/java/com/meta/cp4m/message/WAMessageRouteDetailsTest.java b/src/test/java/com/meta/cp4m/message/WAMessageHandlerTest.java similarity index 92% rename from src/test/java/com/meta/cp4m/message/WAMessageRouteDetailsTest.java rename to src/test/java/com/meta/cp4m/message/WAMessageHandlerTest.java index 14fbb29..ed4b5a2 100644 --- a/src/test/java/com/meta/cp4m/message/WAMessageRouteDetailsTest.java +++ b/src/test/java/com/meta/cp4m/message/WAMessageHandlerTest.java @@ -10,11 +10,9 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.fasterxml.jackson.core.JsonProcessingException; 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.ServiceTestHarness.ReceivedRequest; import com.meta.cp4m.message.webhook.whatsapp.Utils; import java.io.IOException; import java.util.Collection; @@ -25,7 +23,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class WAMessageRouteDetailsTest { +class WAMessageHandlerTest { static final String VALID = """ @@ -101,13 +99,9 @@ class WAMessageRouteDetailsTest { } """; private static final JsonMapper MAPPER = Utils.JSON_MAPPER; - private final ObjectNode validNode = (ObjectNode) MAPPER.readTree(VALID); - private final ServiceTestHarness harness = ServiceTestHarness.newWAServiceTestHarness(); - WAMessageRouteDetailsTest() throws JsonProcessingException {} - @BeforeEach void setUp() { harness.start(); From c9e81e03ad6afe47bdee4564414038759b1fc3e6 Mon Sep 17 00:00:00 2001 From: Hunter Jackson Date: Wed, 20 Dec 2023 13:31:02 -0500 Subject: [PATCH 3/6] finish migration to the multi service per path model --- src/main/java/com/meta/cp4m/Service.java | 17 - .../meta/cp4m/message/FBMessageHandler.java | 315 +++------ .../com/meta/cp4m/message/MessageHandler.java | 15 - .../meta/cp4m/message/WAMessageHandler.java | 82 +-- .../cp4m/message/FBMessageHandlerTest.java | 658 +++++++++--------- 5 files changed, 459 insertions(+), 628 deletions(-) diff --git a/src/main/java/com/meta/cp4m/Service.java b/src/main/java/com/meta/cp4m/Service.java index eadd21b..b085567 100644 --- a/src/main/java/com/meta/cp4m/Service.java +++ b/src/main/java/com/meta/cp4m/Service.java @@ -13,17 +13,13 @@ import com.meta.cp4m.message.MessageHandler; import com.meta.cp4m.message.RequestProcessor; import com.meta.cp4m.message.ThreadState; -import com.meta.cp4m.routing.Acceptor; -import com.meta.cp4m.routing.Handler; import com.meta.cp4m.routing.Route; import com.meta.cp4m.store.ChatStore; -import io.javalin.Javalin; import io.javalin.http.Context; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.concurrent.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,15 +42,6 @@ public Service( this.path = path; } - void handle(Context ctx) { - List messages = handler.processRequest(ctx); - // TODO: once we have a non-volatile store, on startup send stored but not replied to messages - for (T m : messages) { - ThreadState thread = store.add(m); - executorService.submit(() -> execute(thread)); - } - } - void handler(Context ctx, IN in, RequestProcessor processor) { List messages = null; try { @@ -74,10 +61,6 @@ void handler(Context ctx, IN in, RequestProcessor processor) { } } - public void register(Javalin app) { - handler.handlers().forEach(m -> app.addHandler(m, path, this::handle)); - } - public String path() { return path; } diff --git a/src/main/java/com/meta/cp4m/message/FBMessageHandler.java b/src/main/java/com/meta/cp4m/message/FBMessageHandler.java index d2e5d49..2d3704b 100644 --- a/src/main/java/com/meta/cp4m/message/FBMessageHandler.java +++ b/src/main/java/com/meta/cp4m/message/FBMessageHandler.java @@ -9,7 +9,6 @@ package com.meta.cp4m.message; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -17,14 +16,12 @@ import io.javalin.http.BadRequestResponse; import io.javalin.http.Context; import io.javalin.http.HandlerType; - import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.time.Instant; import java.util.*; import java.util.function.Function; - import org.apache.hc.client5.http.fluent.Request; import org.apache.hc.client5.http.fluent.Response; import org.apache.hc.core5.http.ContentType; @@ -38,166 +35,66 @@ public class FBMessageHandler implements MessageHandler { - private static final String API_VERSION = "v17.0"; - private static final JsonMapper MAPPER = new JsonMapper(); - private static final Logger LOGGER = LoggerFactory.getLogger(FBMessageHandler.class); - private static final TextChunker CHUNKER = TextChunker.standard(2000); - - private final String verifyToken; - private final String appSecret; - - private final String accessToken; - private final @Nullable String connectedFacebookPageForInstagram; - - private final Deduplicator messageDeduplicator = new Deduplicator<>(10_000); - private Function baseURLFactory = - pageId -> { - try { - return new URIBuilder() - .setScheme("https") - .setHost("graph.facebook.com") - .appendPath(API_VERSION) - .appendPath(pageId.toString()) - .appendPath("messages") - .build(); - } catch (URISyntaxException e) { - // this should be impossible - throw new RuntimeException(e); - } - }; - - public FBMessageHandler(String verifyToken, String pageAccessToken, String appSecret, @Nullable String connectedFacebookPageForInstagram) { - this.verifyToken = verifyToken; - this.appSecret = appSecret; - this.accessToken = pageAccessToken; - this.connectedFacebookPageForInstagram = connectedFacebookPageForInstagram; - } + private static final String API_VERSION = "v17.0"; + private static final JsonMapper MAPPER = new JsonMapper(); + private static final Logger LOGGER = LoggerFactory.getLogger(FBMessageHandler.class); + private static final TextChunker CHUNKER = TextChunker.standard(2000); - public FBMessageHandler(String verifyToken, String pageAccessToken, String appSecret) { - this.verifyToken = verifyToken; - this.appSecret = appSecret; - this.accessToken = pageAccessToken; - this.connectedFacebookPageForInstagram = null; - } + private final String verifyToken; + private final String appSecret; - FBMessageHandler(FBMessengerConfig config) { - this.verifyToken = config.verifyToken(); - this.appSecret = config.appSecret(); - this.accessToken = config.pageAccessToken(); - this.connectedFacebookPageForInstagram = config.connectedFacebookPageForInstagram().isPresent() ? config.connectedFacebookPageForInstagram().get() : null; - } + private final String accessToken; + private final @Nullable String connectedFacebookPageForInstagram; - @Override - public List processRequest(Context ctx) { + private final Deduplicator messageDeduplicator = new Deduplicator<>(10_000); + private Function baseURLFactory = + pageId -> { try { - switch (ctx.handlerType()) { - case GET -> { - return getHandler(ctx); - } - case POST -> { - return postHandler(ctx); - } - } - } catch (JsonProcessingException | NullPointerException e) { - LOGGER - .atWarn() - .setMessage("Unable to parse message from Meta webhook") - .setCause(e) - .addKeyValue("body", ctx.body()) - .addKeyValue("headers", ctx.headerMap()) - .log(); - throw new BadRequestResponse("Invalid body"); - } catch (RuntimeException e) { - LOGGER.error(e.getMessage(), e); - throw e; - } catch (Exception e) { - LOGGER.error(e.getMessage(), e); - throw new RuntimeException(e); - } - throw new UnsupportedOperationException("Only accepting get and post methods"); - } - - private List getHandler(Context ctx) { - MetaHandlerUtils.subscriptionVerification(ctx, verifyToken); - LOGGER.debug("Meta verified callback url successfully"); - return Collections.emptyList(); - } - - @TestOnly - String hmac(String body) { - // TODO: refactor test so we don't need this - return MetaHandlerUtils.hmac(body, appSecret); - } - - private List postHandler(Context ctx) throws JsonProcessingException { - MetaHandlerUtils.postHeaderValidator(ctx, appSecret); - - String bodyString = ctx.body(); - JsonNode body = MAPPER.readTree(bodyString); - String object = body.get("object").textValue(); - if (!object.equals("page") && !object.equals("instagram")) { - LOGGER - .atWarn() - .setMessage("received body with value of " + object + " for 'object', expected 'page' or 'instagram'") - .addKeyValue("body", bodyString) - .log(); - return Collections.emptyList(); + return new URIBuilder() + .setScheme("https") + .setHost("graph.facebook.com") + .appendPath(API_VERSION) + .appendPath(pageId.toString()) + .appendPath("messages") + .build(); + } catch (URISyntaxException e) { + // this should be impossible + throw new RuntimeException(e); } - // TODO: need better validation - JsonNode entries = body.get("entry"); - ArrayList output = new ArrayList<>(); - for (JsonNode entry : entries) { - @Nullable JsonNode messaging = entry.get("messaging"); - if (messaging == null) { - continue; - } - for (JsonNode message : messaging) { - - Identifier senderId = Identifier.from(message.get("sender").get("id").asLong()); - Identifier recipientId = Identifier.from(message.get("recipient").get("id").asLong()); - Instant timestamp = Instant.ofEpochMilli(message.get("timestamp").asLong()); - @Nullable JsonNode messageObject = message.get("message"); - if (messageObject != null) { - if (messageObject.has("is_echo") && messageObject.get("is_echo").asText().equals("true")) { - return Collections.emptyList(); - } + }; + + public FBMessageHandler( + String verifyToken, + String pageAccessToken, + String appSecret, + @Nullable String connectedFacebookPageForInstagram) { + this.verifyToken = verifyToken; + this.appSecret = appSecret; + this.accessToken = pageAccessToken; + this.connectedFacebookPageForInstagram = connectedFacebookPageForInstagram; + } - // https://developers.facebook.com/docs/messenger-platform/reference/webhook-events/messages - Identifier messageId = Identifier.from(messageObject.get("mid").textValue()); - if (messageDeduplicator.addAndGetIsDuplicate(messageId)) { - continue; - } + public FBMessageHandler(String verifyToken, String pageAccessToken, String appSecret) { + this.verifyToken = verifyToken; + this.appSecret = appSecret; + this.accessToken = pageAccessToken; + this.connectedFacebookPageForInstagram = null; + } - @Nullable JsonNode textObject = messageObject.get("text"); - if (textObject != null && textObject.isTextual()) { - FBMessage m = - new FBMessage( - timestamp, - messageId, - senderId, - recipientId, - textObject.textValue(), - Message.Role.USER); - output.add(m); - } else { - LOGGER - .atWarn() - .setMessage("received message without text, unable to handle this") - .addKeyValue("body", bodyString) - .log(); - } - } else { - LOGGER - .atWarn() - .setMessage( - "received a message without a 'message' key, unable to handle this message type") - .addKeyValue("body", bodyString) - .log(); - } - } - } + FBMessageHandler(FBMessengerConfig config) { + this.verifyToken = config.verifyToken(); + this.appSecret = config.appSecret(); + this.accessToken = config.pageAccessToken(); + this.connectedFacebookPageForInstagram = + config.connectedFacebookPageForInstagram().isPresent() + ? config.connectedFacebookPageForInstagram().get() + : null; + } - return output; + @TestOnly + String hmac(String body) { + // TODO: refactor test so we don't need this + return MetaHandlerUtils.hmac(body, appSecret); } private List postHandler(Context ctx, JsonNode body) { @@ -215,6 +112,11 @@ private List postHandler(Context ctx, JsonNode body) { Instant timestamp = Instant.ofEpochMilli(message.get("timestamp").asLong()); @Nullable JsonNode messageObject = message.get("message"); if (messageObject != null) { + if (messageObject.has("is_echo") + && messageObject.get("is_echo").asText().equals("true")) { + return Collections.emptyList(); + } + // https://developers.facebook.com/docs/messenger-platform/reference/webhook-events/messages Identifier messageId = Identifier.from(messageObject.get("mid").textValue()); if (messageDeduplicator.addAndGetIsDuplicate(messageId)) { @@ -250,63 +152,62 @@ private List postHandler(Context ctx, JsonNode body) { } } - return output; - } + return output; + } - @TestOnly - public @This FBMessageHandler baseURLFactory(Function baseURLFactory) { - this.baseURLFactory = Objects.requireNonNull(baseURLFactory); - return this; - } + @TestOnly + public @This FBMessageHandler baseURLFactory(Function baseURLFactory) { + this.baseURLFactory = Objects.requireNonNull(baseURLFactory); + return this; + } - @Override - public void respond(FBMessage message) throws IOException { - List chunkedText = CHUNKER.chunks(message.message()).toList(); - for (String text : chunkedText) { - send(text, message.recipientId(), message.senderId()); - } + @Override + public void respond(FBMessage message) throws IOException { + List chunkedText = CHUNKER.chunks(message.message()).toList(); + for (String text : chunkedText) { + send(text, message.recipientId(), message.senderId()); } + } - private void send(String message, Identifier recipient, Identifier sender) throws IOException { - URI url; - ObjectNode body = MAPPER.createObjectNode(); - body.put("messaging_type", "RESPONSE").putObject("recipient").put("id", recipient.toString()); - body.putObject("message").put("text", message); - String bodyString; - try { - bodyString = MAPPER.writeValueAsString(body); - url = - new URIBuilder(baseURLFactory.apply(connectedFacebookPageForInstagram == null ? sender : Identifier.from(connectedFacebookPageForInstagram))) - .addParameter("access_token", accessToken) - .build(); - } catch (JsonProcessingException | URISyntaxException e) { - // should be impossible - throw new RuntimeException(e); - } - - Response response = - Request.post(url).bodyString(bodyString, ContentType.APPLICATION_JSON).execute(); - HttpResponse responseContent = response.returnResponse(); - if (responseContent.getCode() != 200) { - String errorMessage = - "received a " - + responseContent.getCode() - + " error code when attempting to reply. " - + responseContent.getReasonPhrase(); - - LOGGER.atError().addKeyValue("body", bodyString).setMessage(errorMessage).log(); - throw new IOException( - "received a " - + responseContent.getCode() - + " error code when attempting to reply. " - + responseContent.getReasonPhrase()); - } + private void send(String message, Identifier recipient, Identifier sender) throws IOException { + URI url; + ObjectNode body = MAPPER.createObjectNode(); + body.put("messaging_type", "RESPONSE").putObject("recipient").put("id", recipient.toString()); + body.putObject("message").put("text", message); + String bodyString; + try { + bodyString = MAPPER.writeValueAsString(body); + url = + new URIBuilder( + baseURLFactory.apply( + connectedFacebookPageForInstagram == null + ? sender + : Identifier.from(connectedFacebookPageForInstagram))) + .addParameter("access_token", accessToken) + .build(); + } catch (JsonProcessingException | URISyntaxException e) { + // should be impossible + throw new RuntimeException(e); } - @Override - public Collection handlers() { - return List.of(HandlerType.GET, HandlerType.POST); + Response response = + Request.post(url).bodyString(bodyString, ContentType.APPLICATION_JSON).execute(); + HttpResponse responseContent = response.returnResponse(); + if (responseContent.getCode() != 200) { + String errorMessage = + "received a " + + responseContent.getCode() + + " error code when attempting to reply. " + + responseContent.getReasonPhrase(); + + LOGGER.atError().addKeyValue("body", bodyString).setMessage(errorMessage).log(); + throw new IOException( + "received a " + + responseContent.getCode() + + " error code when attempting to reply. " + + responseContent.getReasonPhrase()); } + } @Override public List> routeDetails() { @@ -325,8 +226,10 @@ public List> routeDetails() { throw new BadRequestResponse("unable to parse body"); } // TODO: need better validation + String expectedObjectValue = + connectedFacebookPageForInstagram == null ? "page" : "instagram"; @Nullable JsonNode objectNode = body.get("object"); - if (objectNode != null && objectNode.textValue().equals("page")) { + if (objectNode != null && objectNode.textValue().equals(expectedObjectValue)) { return Optional.of(body); } } diff --git a/src/main/java/com/meta/cp4m/message/MessageHandler.java b/src/main/java/com/meta/cp4m/message/MessageHandler.java index 1ab16ee..ececbbf 100644 --- a/src/main/java/com/meta/cp4m/message/MessageHandler.java +++ b/src/main/java/com/meta/cp4m/message/MessageHandler.java @@ -9,24 +9,14 @@ package com.meta.cp4m.message; import com.meta.cp4m.routing.Acceptor; -import io.javalin.http.Context; import io.javalin.http.HandlerType; import java.io.IOException; -import java.util.Collection; import java.util.List; public interface MessageHandler { record RouteDetails( HandlerType handlerType, Acceptor acceptor, RequestProcessor requestProcessor) {} - /** - * Process incoming requests from the messaging service, including messages from the user. - * - * @param ctx the context corresponding to an incoming request - * @return return a {@link Message} object if appropriate - */ - List processRequest(Context ctx); - /** * The method needed to respond to a message from a user * @@ -34,10 +24,5 @@ record RouteDetails( */ void respond(T message) throws IOException; - /** - * @return The different {@link HandlerType}s that this handler expects to receive - */ - Collection handlers(); - List> routeDetails(); } diff --git a/src/main/java/com/meta/cp4m/message/WAMessageHandler.java b/src/main/java/com/meta/cp4m/message/WAMessageHandler.java index 17217de..00f5da5 100644 --- a/src/main/java/com/meta/cp4m/message/WAMessageHandler.java +++ b/src/main/java/com/meta/cp4m/message/WAMessageHandler.java @@ -76,30 +76,6 @@ public WAMessageHandler(WAMessengerConfig config) { this.appSecret = config.appSecret(); } - @Override - public List processRequest(Context ctx) { - - try { - switch (ctx.handlerType()) { - case GET -> { - MetaHandlerUtils.subscriptionVerification(ctx, verifyToken); - LOGGER.debug("Meta verified callback url successfully"); - return Collections.emptyList(); - } - case POST -> { - return postHandler(ctx); - } - } - } catch (RuntimeException e) { - LOGGER.error(e.getMessage(), e); - throw e; - } catch (Exception e) { - LOGGER.error(e.getMessage(), e); - throw new RuntimeException(e); - } - throw new UnsupportedOperationException("Only accepting get and post methods"); - } - private List post(Context ctx, WebhookPayload payload) { List waMessages = new ArrayList<>(); payload.entry().stream() @@ -133,55 +109,6 @@ private List post(Context ctx, WebhookPayload payload) { return waMessages; } - List postHandler(Context ctx) { - MetaHandlerUtils.postHeaderValidator(ctx, appSecret); - String bodyString = ctx.body(); - WebhookPayload payload; - try { - payload = MAPPER.readValue(bodyString, WebhookPayload.class); - } catch (Exception e) { - LOGGER - .atWarn() - .addKeyValue("body", bodyString) - .setMessage( - "unable to process message, server may be subscribed to a 'webhook field' it cannot process") - .log(); - throw new RuntimeException(e); - } - - List waMessages = new ArrayList<>(); - payload.entry().stream() - .flatMap(e -> e.changes().stream()) - .forEach( - change -> { - Identifier phoneNumberId = change.value().metadata().phoneNumberId(); - for (WebhookMessage message : change.value().messages()) { - if (messageDeduplicator.addAndGetIsDuplicate(message.id())) { - continue; // message is a duplicate - } - if (message.type() != WebhookMessage.WebhookMessageType.TEXT) { - LOGGER.warn( - "received message of type '" - + message.type() - + "', only able to handle text messages at this time"); - continue; - } - TextWebhookMessage textMessage = (TextWebhookMessage) message; - waMessages.add( - new WAMessage( - message.timestamp(), - message.id(), - message.from(), - phoneNumberId, - textMessage.text().body(), - Message.Role.USER)); - readExecutor.execute(() -> markRead(phoneNumberId, textMessage.id().toString())); - } - }); - - return waMessages; - } - @TestOnly @This WAMessageHandler baseUrlFactory(Function baseURLFactory) { @@ -213,11 +140,6 @@ private void send(Identifier recipient, Identifier sender, String text) throws I .execute(); } - @Override - public Collection handlers() { - return List.of(HandlerType.GET, HandlerType.POST); - } - @Override public List> routeDetails() { RouteDetails postDetails = @@ -225,8 +147,8 @@ public List> routeDetails() { HandlerType.POST, ctx -> { @Nullable String contentType = ctx.contentType(); - if (contentType != null && - ContentType.parse(contentType).isSameMimeType(ContentType.APPLICATION_JSON) + if (contentType != null + && ContentType.parse(contentType).isSameMimeType(ContentType.APPLICATION_JSON) && MetaHandlerUtils.postHeaderValid(ctx, appSecret)) { String bodyString = ctx.body(); WebhookPayload payload; diff --git a/src/test/java/com/meta/cp4m/message/FBMessageHandlerTest.java b/src/test/java/com/meta/cp4m/message/FBMessageHandlerTest.java index 9c8d2ce..7bcd33e 100644 --- a/src/test/java/com/meta/cp4m/message/FBMessageHandlerTest.java +++ b/src/test/java/com/meta/cp4m/message/FBMessageHandlerTest.java @@ -15,6 +15,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.meta.cp4m.Identifier; import com.meta.cp4m.Service; @@ -25,7 +26,6 @@ import com.meta.cp4m.store.MemoryStoreConfig; import io.javalin.Javalin; import io.javalin.http.HandlerType; - import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -39,7 +39,6 @@ import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; - import org.apache.hc.client5.http.fluent.Request; import org.apache.hc.client5.http.fluent.Response; import org.apache.hc.core5.http.ContentType; @@ -53,14 +52,14 @@ import org.junit.jupiter.params.provider.MethodSource; public class FBMessageHandlerTest { - /** - * Example message collected directly from the messenger webhook - */ - public static final String SAMPLE_MESSAGE = - "{\"object\":\"page\",\"entry\":[{\"id\":\"106195825075770\",\"time\":1692813219204,\"messaging\":[{\"sender\":{\"id\":\"6357858494326947\"},\"recipient\":{\"id\":\"106195825075770\"},\"timestamp\":1692813218705,\"message\":{\"mid\":\"m_kT_mWOSYh_eK3kF8chtyCWfcD9-gomvu4mhaMFQl-gt4D3LjORi6k3BXD6_x9a-FOUt-D2LFuywJN6HfrpAnDg\",\"text\":\"asdfa\"}}]}]}"; - public static final String SAMPLE_IG_MESSAGE = - "{\"object\":\"instagram\",\"entry\":[{\"time\":1692813219204,\"id\":\"106195825075770\",\"messaging\":[{\"sender\":{\"id\":\"6357858494326947\"},\"recipient\":{\"id\":\"106195825075770\"},\"timestamp\":1692813218705,\"message\":{\"mid\":\"m_kT_mWOSYh_eK3kF8chtyCWfcD9-gomvu4mhaMFQl-gt4D3LjORi6k3BXD6_x9a-FOUt-D2LFuywJN6HfrpAnDg\",\"text\":\"asdfa\"}}]}]}"; - public static final String SAMPLE_IG_MESSAGE_ECHO = "{\"object\":\"instagram\",\"entry\":[{\"time\":1700539232059,\"id\":\"90010138419114\",\"messaging\":[{\"sender\":{\"id\":\"90010138419114\"},\"recipient\":{\"id\":\"7069898263033409\"},\"timestamp\":1700539231795,\"message\":{\"mid\":\"aWdfZAG1faXRlbToxOklHTWVzc2FnZAUlEOjkwMDEwMTM4NDE5MTE0OjM0MDI4MjM2Njg0MTcxMDMwMTI0NDI1OTQ1NTA3ODk2MDQ3NzQ1MDozMTM2OTQxMTk5NjIzMzQ1ODM2OTgxMjI5OTgwNTM2MDEyOAZDZD\",\"text\":\"This is a reply\",\"is_echo\":true}}]}]}"; + /** Example message collected directly from the messenger webhook */ + public static final String SAMPLE_MESSAGE = + "{\"object\":\"page\",\"entry\":[{\"id\":\"106195825075770\",\"time\":1692813219204,\"messaging\":[{\"sender\":{\"id\":\"6357858494326947\"},\"recipient\":{\"id\":\"106195825075770\"},\"timestamp\":1692813218705,\"message\":{\"mid\":\"m_kT_mWOSYh_eK3kF8chtyCWfcD9-gomvu4mhaMFQl-gt4D3LjORi6k3BXD6_x9a-FOUt-D2LFuywJN6HfrpAnDg\",\"text\":\"asdfa\"}}]}]}"; + + public static final String SAMPLE_IG_MESSAGE = + "{\"object\":\"instagram\",\"entry\":[{\"time\":1692813219204,\"id\":\"106195825075770\",\"messaging\":[{\"sender\":{\"id\":\"6357858494326947\"},\"recipient\":{\"id\":\"106195825075770\"},\"timestamp\":1692813218705,\"message\":{\"mid\":\"m_kT_mWOSYh_eK3kF8chtyCWfcD9-gomvu4mhaMFQl-gt4D3LjORi6k3BXD6_x9a-FOUt-D2LFuywJN6HfrpAnDg\",\"text\":\"asdfa\"}}]}]}"; + public static final String SAMPLE_IG_MESSAGE_ECHO = + "{\"object\":\"instagram\",\"entry\":[{\"time\":1700539232059,\"id\":\"90010138419114\",\"messaging\":[{\"sender\":{\"id\":\"90010138419114\"},\"recipient\":{\"id\":\"7069898263033409\"},\"timestamp\":1700539231795,\"message\":{\"mid\":\"aWdfZAG1faXRlbToxOklHTWVzc2FnZAUlEOjkwMDEwMTM4NDE5MTE0OjM0MDI4MjM2Njg0MTcxMDMwMTI0NDI1OTQ1NTA3ODk2MDQ3NzQ1MDozMTM2OTQxMTk5NjIzMzQ1ODM2OTgxMjI5OTgwNTM2MDEyOAZDZD\",\"text\":\"This is a reply\",\"is_echo\":true}}]}]}"; private static final ObjectMapper MAPPER = new ObjectMapper(); private static final String SAMPLE_MESSAGE_HMAC = @@ -78,7 +77,7 @@ public class FBMessageHandlerTest { private Javalin app; private BlockingQueue requests; - private static HttpResponse getRequest(String path, int port, Map params) + private static HttpResponse getRequest(String path, int port, Map params) throws IOException, URISyntaxException { URIBuilder uriBuilder = URIBuilder.loopbackAddress().setScheme("http").setPort(port).appendPath(path); @@ -86,327 +85,366 @@ private static HttpResponse getRequest(String path, int port, Map service = - (Service) runner.services().stream().findAny().orElseThrow(); - String path = service.path(); + private static Request createMessageRequest( + String body, ServicesRunner runner, boolean calculateHmac) + throws IOException, URISyntaxException { + @SuppressWarnings("unchecked") // for the scope of this test this is guaranteed + Service service = + (Service) runner.services().stream().findAny().orElseThrow(); + String path = service.path(); - // for the scope of this test this is guaranteed - FBMessageHandler messageHandler = (FBMessageHandler) service.messageHandler(); + // for the scope of this test this is guaranteed + FBMessageHandler messageHandler = (FBMessageHandler) service.messageHandler(); - URI uri = - URIBuilder.localhost().setScheme("http").setPort(runner.port()).appendPath(path).build(); + URI uri = + URIBuilder.localhost().setScheme("http").setPort(runner.port()).appendPath(path).build(); - Request request = + Request request = Request.post(uri) .bodyString(body, ContentType.APPLICATION_JSON) .setHeader("Content-Type", "application/json"); - if (calculateHmac) { - String hmac = messageHandler.hmac(body); - request.setHeader("X-Hub-Signature-256", "sha256=" + hmac); - } - - return request; + if (calculateHmac) { + String hmac = messageHandler.hmac(body); + request.setHeader("X-Hub-Signature-256", "sha256=" + hmac); } - public static Request createMessageRequest(String body, ServicesRunner runner) - throws IOException, URISyntaxException { - return createMessageRequest(body, runner, true); - } + return request; + } - static Stream requestFactory() throws JsonProcessingException { - JsonNode duplicateMessage = MAPPER.readTree(SAMPLE_MESSAGE); - ArrayNode messagingArray = (ArrayNode) duplicateMessage.get("entry").get(0).get("messaging"); - messagingArray.add(messagingArray.get(0)); - - JsonNode duplicateEntry = MAPPER.readTree(SAMPLE_MESSAGE); - ArrayNode entryArray = (ArrayNode) duplicateMessage.get("entry"); - entryArray.add(entryArray.get(0)); - Stream arguments = - Stream.of( - new TestArgument( - "valid sample", - 200, - r -> - createMessageRequest(SAMPLE_MESSAGE, r, false) - .addHeader("X-Hub-Signature-256", SAMPLE_MESSAGE_HMAC), - true), - new TestArgument( - // if this fails the test hmac calculator is flawed - "valid sample with hmac calculation", - 200, - r -> createMessageRequest(SAMPLE_MESSAGE, r), - true), - new TestArgument( - // if this fails the test hmac calculator is flawed - "test the send it twice scenario", - 200, - r -> createMessageRequest(SAMPLE_MESSAGE, r), - true), - new TestArgument( - "valid instagram message", 200, r -> createMessageRequest(SAMPLE_IG_MESSAGE, r), true), - new TestArgument( - "instagram message echo", 200, r -> createMessageRequest(SAMPLE_IG_MESSAGE_ECHO, r), false), - new TestArgument( - "a non page object type", - 400, - r -> createMessageRequest("{\"object\": \"not a page\"}", r), - false), - new TestArgument( - "an invalid page object", - 400, - r -> createMessageRequest("{\"object\": \"page\"}", r), - false), - new TestArgument( - "valid body with no entries/messages", - 200, - r -> createMessageRequest("{\"object\": \"page\", \"entry\": []}", r), - false), - new TestArgument( - "unknown entry objects", - 200, - r -> createMessageRequest("{\"object\": \"page\", \"entry\": [{}]}", r), - false), - new TestArgument( - "valid body without any messaging objects", - 200, - r -> - createMessageRequest( - "{\"object\": \"page\", \"entry\": [{\"messaging\": []}]}", r), - false), - new TestArgument( - "valid body with duplicate message", - 200, - r -> createMessageRequest(MAPPER.writeValueAsString(duplicateMessage), r), - true), - new TestArgument( - "valid body with duplicate entry", - 200, - r -> createMessageRequest(MAPPER.writeValueAsString(duplicateEntry), r), - true), - new TestArgument( - "malformed messaging object", - 400, - r -> - createMessageRequest( - "{\"object\": \"page\", \"entry\": [{\"messaging\": [{}]}]}", r), - false), - new TestArgument( - "messaging object with no messages", - 200, - r -> - createMessageRequest( - "{\"object\": \"page\", \"entry\": [{\"messaging\": [{\"recipient\": {\"id\": 123}, \"sender\": {\"id\": 123}, \"timestamp\": 0}]}]}", - r), - false), - new TestArgument( - "invalid message object", - 400, - r -> - createMessageRequest( - "{\"object\": \"page\", \"entry\": [{\"messaging\": [{\"recipient\": {\"id\": 123}, \"sender\": {\"id\": 123}, \"timestamp\": 0, \"message\": {}}]}]}", - r), - false), - new TestArgument( - "message object without text", - 200, - r -> - createMessageRequest( - "{\"object\": \"page\", \"entry\": [{\"messaging\": [{\"recipient\": {\"id\": 123}, \"sender\": {\"id\": 123}, \"timestamp\": 0, \"message\": {\"mid\": \"abc123789\"}}]}]}", - r), - false), - new TestArgument( - "missing hmac", 400, r -> createMessageRequest(SAMPLE_MESSAGE, r, false), false), - new TestArgument( - "invalid json", 400, r -> createMessageRequest("invalid_json.", r), false), - new TestArgument( - "valid json, invalid body", 400, r -> createMessageRequest("{}", r), false), - new TestArgument( - "invalid hmac", - 400, - r -> - createMessageRequest(SAMPLE_MESSAGE, r, false) - .addHeader("X-Hub-Signature-256", "abcdef0123456789"), - false)); - - return arguments.map( - a -> - Arguments.of( - Named.of(a.name, a.expectedReturnCode), - Named.of("_", a.requestFactory), - Named.of("_", a.messageExpected), - Named.of("_", a.timesToSendMessage()))); - } + public static Request createMessageRequest(String body, ServicesRunner runner) + throws IOException, URISyntaxException { + return createMessageRequest(body, runner, true); + } - @BeforeEach - void setUp() { - - app = Javalin.create(); - app.addHandler( - HandlerType.GET, "*", ctx -> fail("the pipeline should only be sending post requests")); - app.addHandler( - HandlerType.DELETE, "*", ctx -> fail("the pipeline should only be sending post requests")); - app.addHandler( - HandlerType.PUT, "*", ctx -> fail("the pipeline should only be sending post requests")); - requests = new LinkedBlockingQueue<>(); - app.addHandler( - HandlerType.POST, - "/", - ctx -> requests.add(new OutboundRequest(ctx.body(), ctx.headerMap(), ctx.queryParamMap()))); - } + static Stream requestFactory() throws JsonProcessingException { + JsonNode duplicateMessage = MAPPER.readTree(SAMPLE_MESSAGE); + ArrayNode messagingArray = (ArrayNode) duplicateMessage.get("entry").get(0).get("messaging"); + messagingArray.add(messagingArray.get(0)); + + JsonNode duplicateEntry = MAPPER.readTree(SAMPLE_MESSAGE); + ArrayNode entryArray = (ArrayNode) duplicateMessage.get("entry"); + entryArray.add(entryArray.get(0)); + Stream arguments = + Stream.of( + new TestArgument( + "valid sample", + 200, + r -> + createMessageRequest(SAMPLE_MESSAGE, r, false) + .addHeader("X-Hub-Signature-256", SAMPLE_MESSAGE_HMAC), + true), + new TestArgument( + // if this fails the test hmac calculator is flawed + "valid sample with hmac calculation", + 200, + r -> createMessageRequest(SAMPLE_MESSAGE, r), + true), + new TestArgument( + // if this fails the test hmac calculator is flawed + "test the send it twice scenario", + 200, + r -> createMessageRequest(SAMPLE_MESSAGE, r), + true, + 2), + new TestArgument( + "valid instagram message", + 200, + r -> createMessageRequest(SAMPLE_IG_MESSAGE, r), + true, + true), + new TestArgument( + "instagram message echo", + 200, + r -> createMessageRequest(SAMPLE_IG_MESSAGE_ECHO, r), + false, + true), + new TestArgument( + "a non page object type", + 400, + r -> createMessageRequest("{\"object\": \"not a page\"}", r), + false), + new TestArgument( + "an invalid page object", + 400, + r -> createMessageRequest("{\"object\": \"page\"}", r), + false), + new TestArgument( + "valid body with no entries/messages", + 200, + r -> createMessageRequest("{\"object\": \"page\", \"entry\": []}", r), + false), + new TestArgument( + "unknown entry objects", + 200, + r -> createMessageRequest("{\"object\": \"page\", \"entry\": [{}]}", r), + false), + new TestArgument( + "valid body without any messaging objects", + 200, + r -> + createMessageRequest( + "{\"object\": \"page\", \"entry\": [{\"messaging\": []}]}", r), + false), + new TestArgument( + "valid body with duplicate message", + 200, + r -> createMessageRequest(MAPPER.writeValueAsString(duplicateMessage), r), + true), + new TestArgument( + "valid body with duplicate entry", + 200, + r -> createMessageRequest(MAPPER.writeValueAsString(duplicateEntry), r), + true), + new TestArgument( + "malformed messaging object", + 400, + r -> + createMessageRequest( + "{\"object\": \"page\", \"entry\": [{\"messaging\": [{}]}]}", r), + false), + new TestArgument( + "messaging object with no messages", + 200, + r -> + createMessageRequest( + "{\"object\": \"page\", \"entry\": [{\"messaging\": [{\"recipient\": {\"id\": 123}, \"sender\": {\"id\": 123}, \"timestamp\": 0}]}]}", + r), + false), + new TestArgument( + "invalid message object", + 400, + r -> + createMessageRequest( + "{\"object\": \"page\", \"entry\": [{\"messaging\": [{\"recipient\": {\"id\": 123}, \"sender\": {\"id\": 123}, \"timestamp\": 0, \"message\": {}}]}]}", + r), + false), + new TestArgument( + "message object without text", + 200, + r -> + createMessageRequest( + "{\"object\": \"page\", \"entry\": [{\"messaging\": [{\"recipient\": {\"id\": 123}, \"sender\": {\"id\": 123}, \"timestamp\": 0, \"message\": {\"mid\": \"abc123789\"}}]}]}", + r), + false), + new TestArgument( + "missing hmac", 400, r -> createMessageRequest(SAMPLE_MESSAGE, r, false), false), + new TestArgument( + "invalid json", 400, r -> createMessageRequest("invalid_json.", r), false), + new TestArgument( + "valid json, invalid body", 400, r -> createMessageRequest("{}", r), false), + new TestArgument( + "invalid hmac", + 400, + r -> + createMessageRequest(SAMPLE_MESSAGE, r, false) + .addHeader("X-Hub-Signature-256", "abcdef0123456789"), + false)); + + return arguments.map( + a -> + Arguments.of( + Named.of(a.name, a.expectedReturnCode), + Named.of("_", a.requestFactory), + Named.of("_", a.messageExpected), + Named.of("_", a.timesToSendMessage()), + Named.of("_", a.isInstagram()))); + } - @AfterEach - void tearDown() { - app.close(); - } + @BeforeEach + void setUp() { + + app = Javalin.create(); + app.addHandler( + HandlerType.GET, "*", ctx -> fail("the pipeline should only be sending post requests")); + app.addHandler( + HandlerType.DELETE, "*", ctx -> fail("the pipeline should only be sending post requests")); + app.addHandler( + HandlerType.PUT, "*", ctx -> fail("the pipeline should only be sending post requests")); + requests = new LinkedBlockingQueue<>(); + app.addHandler( + HandlerType.POST, + "/", + ctx -> requests.add(new OutboundRequest(ctx.body(), ctx.headerMap(), ctx.queryParamMap()))); + } - @Test - void validation() throws IOException, URISyntaxException { - final String pageToken = "243af3c6-9994-4869-ae13-ad61a38323f5"; // this is fake - final int challenge = 1158201444; + @AfterEach + void tearDown() { + app.close(); + } + + @Test + void validation() throws IOException, URISyntaxException { + final String pageToken = "243af3c6-9994-4869-ae13-ad61a38323f5"; // this is fake + final int challenge = 1158201444; final String verifyToken = "oamnw9230rjadoia"; - Service service = - new Service<>( - MemoryStoreConfig.of(1, 1).toStore(), - new FBMessageHandler(verifyToken, pageToken, "dummy"), - new DummyLLMPlugin("this is a dummy message"), - "/testfbmessage"); - final ServicesRunner runner = ServicesRunner.newInstance().service(service).port(0); - HttpResponse response; - try (ServicesRunner ignored = runner.start()) { - ImmutableMap params = - ImmutableMap.builder() - .put("hub.mode", "subscribe") - .put("hub.challenge", Integer.toString(challenge)) - .put("hub.verify_token", verifyToken) - .build(); - response = getRequest("testfbmessage", runner.port(), params); - } - assertThat(response.getCode()).isEqualTo(200); - InputStream inputStream = ((BasicClassicHttpResponse) response).getEntity().getContent(); - byte[] input = new byte[inputStream.available()]; - inputStream.read(input); - String text = new String(input, StandardCharsets.UTF_8); - assertThat(text).isEqualTo(Integer.toString(challenge)); + Service service = + new Service<>( + MemoryStoreConfig.of(1, 1).toStore(), + new FBMessageHandler(verifyToken, pageToken, "dummy"), + new DummyLLMPlugin<>("this is a dummy message"), + "/testfbmessage"); + final ServicesRunner runner = ServicesRunner.newInstance().service(service).port(0); + HttpResponse response; + try (ServicesRunner ignored = runner.start()) { + ImmutableMap params = + ImmutableMap.builder() + .put("hub.mode", "subscribe") + .put("hub.challenge", Integer.toString(challenge)) + .put("hub.verify_token", verifyToken) + .build(); + response = getRequest("testfbmessage", runner.port(), params); } + assertThat(response.getCode()).isEqualTo(200); + InputStream inputStream = ((BasicClassicHttpResponse) response).getEntity().getContent(); + byte[] input = new byte[inputStream.available()]; + inputStream.read(input); + String text = new String(input, StandardCharsets.UTF_8); + assertThat(text).isEqualTo(Integer.toString(challenge)); + } - private Function testURLFactoryFactory(Identifier pageId) { - return p -> { - assertThat(p).isEqualTo(pageId); - try { - return URIBuilder.localhost().setScheme("http").setPort(app.port()).build(); - } catch (URISyntaxException | UnknownHostException e) { - fail("failed building url"); - throw new RuntimeException(e); - } - }; - } + private Function testURLFactoryFactory(Identifier pageId) { + return p -> { + Preconditions.checkArgument(p.equals(pageId)); + try { + return URIBuilder.localhost().setScheme("http").setPort(app.port()).build(); + } catch (URISyntaxException | UnknownHostException e) { + fail("failed building url"); + throw new RuntimeException(e); + } + }; + } - @ParameterizedTest - @MethodSource("requestFactory") - void invalidMessage( - int expectedReturnCode, - ThrowableFunction requestFactory, - boolean messageExpected, - int timesToSendMessage) - throws Exception { - String path = "/testfbmessage"; - Identifier pageId = Identifier.from(106195825075770L); - String token = "243af3c6-9994-4869-ae13-ad61a38323f5"; // this is fake don't worry - String secret = "f74a638462f975e9eadfcbb84e4aa06b"; // it's been rolled don't worry - FBMessageHandler messageHandler = new FBMessageHandler("0", token, secret); - DummyLLMPlugin llmHandler = new DummyLLMPlugin<>("this is a dummy message"); - MemoryStore memoryStore = MemoryStoreConfig.of(1, 1).toStore(); - Service service = new Service<>(memoryStore, messageHandler, llmHandler, path); - final ServicesRunner runner = ServicesRunner.newInstance().service(service).port(0); - - app.start(0); - runner.start(); - messageHandler.baseURLFactory(testURLFactoryFactory(pageId)); - - @Nullable Response response = null; - for (int ignored = 0; ignored < timesToSendMessage; ignored++) { - Request request = requestFactory.apply(runner); - response = request.execute(); - } - assert response != null; - assertThat(response.returnResponse().getCode()).isEqualTo(expectedReturnCode); - if (!messageExpected) { - assertThat(llmHandler.poll()) - .isNull(); // make sure the message wasn't processed and send to the llm handler - assertThat(memoryStore.size()) - .isEqualTo(0); // make sure the message wasn't processed and stored - assertThat(requests).hasSize(0); - } else { - ThreadState thread = llmHandler.take(500); - JsonNode messageObject = PARSED_SAMPLE_MESSAGE.get("entry").get(0).get("messaging").get(0); - String messageText = messageObject.get("message").get("text").textValue(); - String mid = messageObject.get("message").get("mid").textValue(); - Identifier recipientId = - Identifier.from(messageObject.get("recipient").get("id").textValue()); - Identifier senderId = Identifier.from(messageObject.get("sender").get("id").textValue()); - Instant timestamp = Instant.ofEpochMilli(messageObject.get("timestamp").longValue()); - assertThat(thread.messages()) - .hasSize(1) - .allSatisfy(m -> assertThat(m.message()).isEqualTo(messageText)) - .allSatisfy(m -> assertThat(m.instanceId().toString()).isEqualTo(mid)) - .allSatisfy(m -> assertThat(m.role()).isSameAs(Role.USER)) - .allSatisfy(m -> assertThat(m.timestamp()).isEqualTo(timestamp)) - .allSatisfy(m -> assertThat(m.recipientId()).isEqualTo(recipientId)) - .allSatisfy(m -> assertThat(m.senderId()).isEqualTo(senderId)); - - @Nullable OutboundRequest r = requests.poll(500, TimeUnit.MILLISECONDS); - assertThat(r).isNotNull(); - assertThat(r.queryParamMap().get("access_token")) - .hasSize(1) - .allSatisfy(t -> assertThat(t).isEqualTo(token)); - JsonNode body = MAPPER.readTree(r.body); - assertThat(body.get("messaging_type").textValue()).isEqualTo("RESPONSE"); - assertThat(body.get("recipient").get("id").textValue()).isEqualTo(senderId.toString()); - assertThat(body.get("message").get("text").textValue()).isEqualTo(llmHandler.dummyResponse()); - } + @ParameterizedTest + @MethodSource("requestFactory") + void invalidMessage( + int expectedReturnCode, + ThrowableFunction requestFactory, + boolean messageExpected, + int timesToSendMessage, + boolean isInstagram) + throws Exception { + String path = "/testfbmessage"; + Identifier pageId = Identifier.from(106195825075770L); + String token = "243af3c6-9994-4869-ae13-ad61a38323f5"; // this is fake don't worry + String secret = "f74a638462f975e9eadfcbb84e4aa06b"; // it's been rolled don't worry + + FBMessageHandler messageHandler; + if (isInstagram) { + messageHandler = new FBMessageHandler("0", token, secret, pageId.toString()); + } else { + messageHandler = new FBMessageHandler("0", token, secret); } - - @Test - void chunkingHappens() throws IOException { - app.start(0); - Identifier pageId = Identifier.from(106195825075770L); - String token = "243af3c6-9994-4869-ae13-ad61a38323f5"; // this is fake don't worry - String secret = "f74a638462f975e9eadfcbb84e4aa06b"; // it's been rolled don't worry - FBMessageHandler messageHandler = - new FBMessageHandler("0", token, secret).baseURLFactory(testURLFactoryFactory(pageId)); - - String bigText = - Stream.generate(() -> "0123456789.").limit(300).collect(Collectors.joining(" ")); - FBMessage bigMessage = - new FBMessage( - Instant.now(), Identifier.random(), pageId, Identifier.random(), bigText, Role.USER); - messageHandler.respond(bigMessage); - assertThat(requests.size()).isEqualTo(300); - assertThat(requests).allSatisfy(m -> assertThat(m.body()).contains("0123456789")); + DummyLLMPlugin llmHandler = new DummyLLMPlugin<>("this is a dummy message"); + MemoryStore memoryStore = MemoryStoreConfig.of(1, 1).toStore(); + Service service = new Service<>(memoryStore, messageHandler, llmHandler, path); + final ServicesRunner runner = ServicesRunner.newInstance().service(service).port(0); + + app.start(0); + runner.start(); + messageHandler.baseURLFactory(testURLFactoryFactory(pageId)); + + @Nullable Response response = null; + for (int ignored = 0; ignored < timesToSendMessage; ignored++) { + Request request = requestFactory.apply(runner); + response = request.execute(); } - - @FunctionalInterface - private interface ThrowableFunction { - R apply(T in) throws Exception; + assert response != null; + assertThat(response.returnResponse().getCode()).isEqualTo(expectedReturnCode); + if (!messageExpected) { + assertThat(llmHandler.poll()) + .isNull(); // make sure the message wasn't processed and send to the llm handler + assertThat(memoryStore.size()) + .isEqualTo(0); // make sure the message wasn't processed and stored + assertThat(requests).hasSize(0); + } else { + ThreadState thread = llmHandler.take(500); + JsonNode messageObject = PARSED_SAMPLE_MESSAGE.get("entry").get(0).get("messaging").get(0); + String messageText = messageObject.get("message").get("text").textValue(); + String mid = messageObject.get("message").get("mid").textValue(); + Identifier recipientId = + Identifier.from(messageObject.get("recipient").get("id").textValue()); + Identifier senderId = Identifier.from(messageObject.get("sender").get("id").textValue()); + Instant timestamp = Instant.ofEpochMilli(messageObject.get("timestamp").longValue()); + assertThat(thread.messages()) + .hasSize(1) + .allSatisfy(m -> assertThat(m.message()).isEqualTo(messageText)) + .allSatisfy(m -> assertThat(m.instanceId().toString()).isEqualTo(mid)) + .allSatisfy(m -> assertThat(m.role()).isSameAs(Role.USER)) + .allSatisfy(m -> assertThat(m.timestamp()).isEqualTo(timestamp)) + .allSatisfy(m -> assertThat(m.recipientId()).isEqualTo(recipientId)) + .allSatisfy(m -> assertThat(m.senderId()).isEqualTo(senderId)); + + @Nullable OutboundRequest r = requests.poll(500, TimeUnit.MILLISECONDS); + assertThat(r).isNotNull(); + assertThat(r.queryParamMap().get("access_token")) + .hasSize(1) + .allSatisfy(t -> assertThat(t).isEqualTo(token)); + JsonNode body = MAPPER.readTree(r.body); + assertThat(body.get("messaging_type").textValue()).isEqualTo("RESPONSE"); + assertThat(body.get("recipient").get("id").textValue()).isEqualTo(senderId.toString()); + assertThat(body.get("message").get("text").textValue()).isEqualTo(llmHandler.dummyResponse()); } + } + + @Test + void chunkingHappens() throws IOException { + app.start(0); + Identifier pageId = Identifier.from(106195825075770L); + String token = "243af3c6-9994-4869-ae13-ad61a38323f5"; // this is fake don't worry + String secret = "f74a638462f975e9eadfcbb84e4aa06b"; // it's been rolled don't worry + FBMessageHandler messageHandler = + new FBMessageHandler("0", token, secret).baseURLFactory(testURLFactoryFactory(pageId)); + + String bigText = + Stream.generate(() -> "0123456789.").limit(300).collect(Collectors.joining(" ")); + FBMessage bigMessage = + new FBMessage( + Instant.now(), Identifier.random(), pageId, Identifier.random(), bigText, Role.USER); + messageHandler.respond(bigMessage); + assertThat(requests.size()).isEqualTo(300); + assertThat(requests).allSatisfy(m -> assertThat(m.body()).contains("0123456789")); + } - private record OutboundRequest( - String body, Map headerMap, Map> queryParamMap) { + @FunctionalInterface + private interface ThrowableFunction { + R apply(T in) throws Exception; + } + + private record OutboundRequest( + String body, Map headerMap, Map> queryParamMap) {} + + private record TestArgument( + String name, + int expectedReturnCode, + ThrowableFunction requestFactory, + boolean messageExpected, + int timesToSendMessage, + boolean isInstagram) { + + private TestArgument( + String name, + int expectedReturnCode, + ThrowableFunction requestFactory, + boolean messageExpected, + boolean isInstagram) { + this(name, expectedReturnCode, requestFactory, messageExpected, 1, isInstagram); } - private record TestArgument( - String name, - int expectedReturnCode, - ThrowableFunction requestFactory, - boolean messageExpected) { + private TestArgument( + String name, + int expectedReturnCode, + ThrowableFunction requestFactory, + boolean messageExpected, + int timesToSendMessage) { + this(name, expectedReturnCode, requestFactory, messageExpected, timesToSendMessage, false); + } - // hacky way to try the send twice scenario - int timesToSendMessage() { - return name.contains("send it twice") ? 2 : 1; - } + private TestArgument( + String name, + int expectedReturnCode, + ThrowableFunction requestFactory, + boolean messageExpected) { + this(name, expectedReturnCode, requestFactory, messageExpected, 1); } + } } From 81305c029568604297bf80642de96a173dc7b5c6 Mon Sep 17 00:00:00 2001 From: Hunter Jackson Date: Tue, 16 Jan 2024 16:28:45 -0500 Subject: [PATCH 4/6] correct logger name --- src/main/java/com/meta/cp4m/message/WAMessageHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/meta/cp4m/message/WAMessageHandler.java b/src/main/java/com/meta/cp4m/message/WAMessageHandler.java index 00f5da5..825e924 100644 --- a/src/main/java/com/meta/cp4m/message/WAMessageHandler.java +++ b/src/main/java/com/meta/cp4m/message/WAMessageHandler.java @@ -37,7 +37,7 @@ public class WAMessageHandler implements MessageHandler { private static final String API_VERSION = "v17.0"; private static final JsonMapper MAPPER = Utils.JSON_MAPPER; - private static final Logger LOGGER = LoggerFactory.getLogger(FBMessageHandler.class); + private static final Logger LOGGER = LoggerFactory.getLogger(WAMessageHandler.class); /** * Date: Tue, 16 Jan 2024 16:30:05 -0500 Subject: [PATCH 5/6] run formatter same as main --- .github/workflows/ci.yml | 21 +- docs/blog/2019-05-28-first-blog-post.md | 3 +- docs/blog/2019-05-29-long-blog-post.md | 48 +- docs/blog/2021-08-26-welcome/index.md | 3 +- docs/docs/intro.md | 9 +- docs/docs/tutorial-basics/congratulations.md | 6 +- docs/docs/tutorial-basics/deploy-your-site.md | 3 +- .../tutorial-extras/translate-your-site.md | 3 +- docs/package.json | 2 +- docs/static/img/logo.svg | 37 +- docs/static/img/meta_opensource_logo.svg | 111 +++- .../img/meta_opensource_logo_negative.svg | 111 +++- .../static/img/undraw_docusaurus_mountain.svg | 522 ++++++++++++------ docs/static/img/undraw_docusaurus_react.svg | 511 +++++++++++------ docs/static/img/undraw_docusaurus_tree.svg | 105 ++-- node_modules/yarn/README.md | 29 +- .../java/com/meta/cp4m/ServicesRunner.java | 4 +- .../meta/cp4m/llm/HuggingFaceLlamaPlugin.java | 79 +-- .../meta/cp4m/llm/HuggingFaceLlamaPrompt.java | 139 +++-- .../java/com/meta/cp4m/llm/OpenAIPlugin.java | 6 +- .../meta/cp4m/message/FBMessengerConfig.java | 5 +- .../meta/cp4m/message/MetaHandlerUtils.java | 4 +- .../meta/cp4m/message/RequestProcessor.java | 1 - .../webhook/whatsapp/AudioWebhookMessage.java | 1 + .../whatsapp/ButtonWebhookMessage.java | 1 + .../whatsapp/InteractiveWebhookMessage.java | 1 + .../webhook/whatsapp/OrderWebhookMessage.java | 1 + .../java/com/meta/cp4m/routing/Acceptor.java | 3 - .../java/com/meta/cp4m/routing/Handler.java | 2 - .../java/com/meta/cp4m/routing/Route.java | 1 - .../java/com/meta/cp4m/store/ChatStore.java | 4 +- .../meta/cp4m/llm/HuggingFaceConfigTest.java | 160 +++--- .../com/meta/cp4m/llm/OpenAIPluginTest.java | 2 +- 33 files changed, 1306 insertions(+), 632 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dd8077..297436e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,6 @@ - name: Continuous Integration -on: [push] +on: [ push ] jobs: build: @@ -9,13 +8,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up JDK - uses: actions/setup-java@v3 - with: - java-version: '20' - distribution: 'temurin' - cache: maven - - name: Build with Maven - run: mvn -B package --file pom.xml + - uses: actions/checkout@v3 + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '20' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn -B package --file pom.xml diff --git a/docs/blog/2019-05-28-first-blog-post.md b/docs/blog/2019-05-28-first-blog-post.md index 02f3f81..2bf1285 100644 --- a/docs/blog/2019-05-28-first-blog-post.md +++ b/docs/blog/2019-05-28-first-blog-post.md @@ -9,4 +9,5 @@ authors: tags: [hola, docusaurus] --- -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum +tempor eros aliquam consequat. Lorem ipsum dolor sit amet diff --git a/docs/blog/2019-05-29-long-blog-post.md b/docs/blog/2019-05-29-long-blog-post.md index 26ffb1b..8fb5b19 100644 --- a/docs/blog/2019-05-29-long-blog-post.md +++ b/docs/blog/2019-05-29-long-blog-post.md @@ -11,34 +11,50 @@ Use a `` comment to limit blog post size in the list view. -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum +tempor eros aliquam consequat. Lorem ipsum dolor sit amet -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum +tempor eros aliquam consequat. Lorem ipsum dolor sit amet -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum +tempor eros aliquam consequat. Lorem ipsum dolor sit amet -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum +tempor eros aliquam consequat. Lorem ipsum dolor sit amet -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum +tempor eros aliquam consequat. Lorem ipsum dolor sit amet -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum +tempor eros aliquam consequat. Lorem ipsum dolor sit amet -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum +tempor eros aliquam consequat. Lorem ipsum dolor sit amet -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum +tempor eros aliquam consequat. Lorem ipsum dolor sit amet -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum +tempor eros aliquam consequat. Lorem ipsum dolor sit amet -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum +tempor eros aliquam consequat. Lorem ipsum dolor sit amet -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum +tempor eros aliquam consequat. Lorem ipsum dolor sit amet -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum +tempor eros aliquam consequat. Lorem ipsum dolor sit amet -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum +tempor eros aliquam consequat. Lorem ipsum dolor sit amet -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum +tempor eros aliquam consequat. Lorem ipsum dolor sit amet -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum +tempor eros aliquam consequat. Lorem ipsum dolor sit amet -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum +tempor eros aliquam consequat. Lorem ipsum dolor sit amet diff --git a/docs/blog/2021-08-26-welcome/index.md b/docs/blog/2021-08-26-welcome/index.md index 9455168..4490343 100644 --- a/docs/blog/2021-08-26-welcome/index.md +++ b/docs/blog/2021-08-26-welcome/index.md @@ -5,7 +5,8 @@ authors: [slorber, yangshun] tags: [facebook, hello, docusaurus] --- -[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog). +[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by +the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog). Simply add Markdown files (or folders) to the `blog` directory. diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 8a2e69d..db8dff0 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -27,7 +27,8 @@ The classic template will automatically be added to your project after you run t npm init docusaurus@latest my-website classic ``` -You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code editor. +You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code +editor. The command also installs all necessary dependencies you need to run Docusaurus. @@ -40,8 +41,10 @@ cd my-website npm run start ``` -The `cd` command changes the directory you're working with. In order to work with your newly created Docusaurus site, you'll need to navigate the terminal there. +The `cd` command changes the directory you're working with. In order to work with your newly created Docusaurus site, +you'll need to navigate the terminal there. -The `npm run start` command builds your website locally and serves it through a development server, ready for you to view at http://localhost:3000/. +The `npm run start` command builds your website locally and serves it through a development server, ready for you to +view at http://localhost:3000/. Open `docs/intro.md` (this page) and edit some lines: the site **reloads automatically** and displays your changes. diff --git a/docs/docs/tutorial-basics/congratulations.md b/docs/docs/tutorial-basics/congratulations.md index 04771a0..67e8778 100644 --- a/docs/docs/tutorial-basics/congratulations.md +++ b/docs/docs/tutorial-basics/congratulations.md @@ -8,9 +8,11 @@ You have just learned the **basics of Docusaurus** and made some changes to the Docusaurus has **much more to offer**! -Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and **[i18n](../tutorial-extras/translate-your-site.md)**. +Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and * +*[i18n](../tutorial-extras/translate-your-site.md)**. -Anything **unclear** or **buggy** in this tutorial? [Please report it!](https://github.com/facebook/docusaurus/discussions/4610) +Anything **unclear** or **buggy** in this +tutorial? [Please report it!](https://github.com/facebook/docusaurus/discussions/4610) ## What's next? diff --git a/docs/docs/tutorial-basics/deploy-your-site.md b/docs/docs/tutorial-basics/deploy-your-site.md index 1c50ee0..104930b 100644 --- a/docs/docs/tutorial-basics/deploy-your-site.md +++ b/docs/docs/tutorial-basics/deploy-your-site.md @@ -28,4 +28,5 @@ npm run serve The `build` folder is now served at [http://localhost:3000/](http://localhost:3000/). -You can now deploy the `build` folder **almost anywhere** easily, **for free** or very small cost (read the **[Deployment Guide](https://docusaurus.io/docs/deployment)**). +You can now deploy the `build` folder **almost anywhere** easily, **for free** or very small cost (read the * +*[Deployment Guide](https://docusaurus.io/docs/deployment)**). diff --git a/docs/docs/tutorial-extras/translate-your-site.md b/docs/docs/tutorial-extras/translate-your-site.md index caeaffb..c0685ca 100644 --- a/docs/docs/tutorial-extras/translate-your-site.md +++ b/docs/docs/tutorial-extras/translate-your-site.md @@ -39,7 +39,8 @@ Start your site on the French locale: npm run start -- --locale fr ``` -Your localized site is accessible at [http://localhost:3000/fr/](http://localhost:3000/fr/) and the `Getting Started` page is translated. +Your localized site is accessible at [http://localhost:3000/fr/](http://localhost:3000/fr/) and the `Getting Started` +page is translated. :::caution diff --git a/docs/package.json b/docs/package.json index f4ae5b6..ee92058 100644 --- a/docs/package.json +++ b/docs/package.json @@ -53,7 +53,7 @@ "engines": { "node": ">=16.14" }, - "overrides":{ + "overrides": { "trim": "^0.0.3", "got": "^11.8.5" } diff --git a/docs/static/img/logo.svg b/docs/static/img/logo.svg index 9db6d0d..5d3da46 100644 --- a/docs/static/img/logo.svg +++ b/docs/static/img/logo.svg @@ -1 +1,36 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/static/img/meta_opensource_logo.svg b/docs/static/img/meta_opensource_logo.svg index 83a2175..25aebf5 100644 --- a/docs/static/img/meta_opensource_logo.svg +++ b/docs/static/img/meta_opensource_logo.svg @@ -1 +1,110 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/static/img/meta_opensource_logo_negative.svg b/docs/static/img/meta_opensource_logo_negative.svg index b36a423..70db12b 100644 --- a/docs/static/img/meta_opensource_logo_negative.svg +++ b/docs/static/img/meta_opensource_logo_negative.svg @@ -1 +1,110 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/static/img/undraw_docusaurus_mountain.svg b/docs/static/img/undraw_docusaurus_mountain.svg index af961c4..b0b7682 100644 --- a/docs/static/img/undraw_docusaurus_mountain.svg +++ b/docs/static/img/undraw_docusaurus_mountain.svg @@ -1,171 +1,361 @@ + + - Easy to Use - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Easy to Use + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/static/img/undraw_docusaurus_react.svg b/docs/static/img/undraw_docusaurus_react.svg index 94b5cf0..4efce08 100644 --- a/docs/static/img/undraw_docusaurus_react.svg +++ b/docs/static/img/undraw_docusaurus_react.svg @@ -1,170 +1,353 @@ + + - Powered by React - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Powered by React + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/static/img/undraw_docusaurus_tree.svg b/docs/static/img/undraw_docusaurus_tree.svg index d9161d3..2dbba0f 100644 --- a/docs/static/img/undraw_docusaurus_tree.svg +++ b/docs/static/img/undraw_docusaurus_tree.svg @@ -1,40 +1,69 @@ + + - Focus on What Matters - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Focus on What Matters + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/node_modules/yarn/README.md b/node_modules/yarn/README.md index 1305b93..2a3d878 100644 --- a/node_modules/yarn/README.md +++ b/node_modules/yarn/README.md @@ -18,24 +18,30 @@ --- -**Fast:** Yarn caches every package it has downloaded, so it never needs to download the same package again. It also does almost everything concurrently to maximize resource utilization. This means even faster installs. +**Fast:** Yarn caches every package it has downloaded, so it never needs to download the same package again. It also +does almost everything concurrently to maximize resource utilization. This means even faster installs. -**Reliable:** Using a detailed but concise lockfile format and a deterministic algorithm for install operations, Yarn is able to guarantee that any installation that works on one system will work exactly the same on another system. +**Reliable:** Using a detailed but concise lockfile format and a deterministic algorithm for install operations, Yarn is +able to guarantee that any installation that works on one system will work exactly the same on another system. **Secure:** Yarn uses checksums to verify the integrity of every installed package before its code is executed. ## Features -* **Offline Mode.** If you've installed a package before, then you can install it again without an internet connection. -* **Deterministic.** The same dependencies will be installed in the same exact way on any machine, regardless of installation order. -* **Network Performance.** Yarn efficiently queues requests and avoids request waterfalls in order to maximize network utilization. -* **Network Resilience.** A single request that fails will not cause the entire installation to fail. Requests are automatically retried upon failure. +* **Offline Mode.** If you've installed a package before, then you can install it again without an internet connection. +* **Deterministic.** The same dependencies will be installed in the same exact way on any machine, regardless of + installation order. +* **Network Performance.** Yarn efficiently queues requests and avoids request waterfalls in order to maximize network + utilization. +* **Network Resilience.** A single request that fails will not cause the entire installation to fail. Requests are + automatically retried upon failure. * **Flat Mode.** Yarn resolves mismatched versions of dependencies to a single version to avoid creating duplicates. * **More emojis.** 🐈 ## Installing Yarn -Read the [Installation Guide](https://yarnpkg.com/en/docs/install) on our website for detailed instructions on how to install Yarn. +Read the [Installation Guide](https://yarnpkg.com/en/docs/install) on our website for detailed instructions on how to +install Yarn. ## Using Yarn @@ -43,7 +49,8 @@ Read the [Usage Guide](https://yarnpkg.com/en/docs/usage) on our website for det ## Contributing to Yarn -Contributions are always welcome, no matter how large or small. Substantial feature requests should be proposed as an [RFC](https://github.com/yarnpkg/rfcs). Before contributing, please read the [code of conduct](CODE_OF_CONDUCT.md). +Contributions are always welcome, no matter how large or small. Substantial feature requests should be proposed as +an [RFC](https://github.com/yarnpkg/rfcs). Before contributing, please read the [code of conduct](CODE_OF_CONDUCT.md). See [Contributing](https://yarnpkg.com/org/contributing/). @@ -51,9 +58,9 @@ See [Contributing](https://yarnpkg.com/org/contributing/). Yarn wouldn't exist if it wasn't for excellent prior art. Yarn has been inspired by the following projects: - - [Bundler](https://github.com/bundler/bundler) - - [Cargo](https://github.com/rust-lang/cargo) - - [npm](https://github.com/npm/cli) +- [Bundler](https://github.com/bundler/bundler) +- [Cargo](https://github.com/rust-lang/cargo) +- [npm](https://github.com/npm/cli) ## Credits diff --git a/src/main/java/com/meta/cp4m/ServicesRunner.java b/src/main/java/com/meta/cp4m/ServicesRunner.java index b01c4db..e70c5e4 100644 --- a/src/main/java/com/meta/cp4m/ServicesRunner.java +++ b/src/main/java/com/meta/cp4m/ServicesRunner.java @@ -11,12 +11,10 @@ import com.google.common.base.Preconditions; import com.meta.cp4m.routing.Route; import io.javalin.Javalin; - -import java.util.*; - import io.javalin.http.BadRequestResponse; import io.javalin.http.Context; import io.javalin.http.HandlerType; +import java.util.*; import org.checkerframework.common.returnsreceiver.qual.This; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/com/meta/cp4m/llm/HuggingFaceLlamaPlugin.java b/src/main/java/com/meta/cp4m/llm/HuggingFaceLlamaPlugin.java index 4105a3f..d1538f9 100644 --- a/src/main/java/com/meta/cp4m/llm/HuggingFaceLlamaPlugin.java +++ b/src/main/java/com/meta/cp4m/llm/HuggingFaceLlamaPlugin.java @@ -24,53 +24,54 @@ public class HuggingFaceLlamaPlugin implements LLMPlugin { - private static final ObjectMapper MAPPER = new ObjectMapper(); - private final HuggingFaceConfig config; - private final HuggingFaceLlamaPrompt promptCreator; + private static final ObjectMapper MAPPER = new ObjectMapper(); + private final HuggingFaceConfig config; + private final HuggingFaceLlamaPrompt promptCreator; - private URI endpoint; + private URI endpoint; - public HuggingFaceLlamaPlugin(HuggingFaceConfig config) { - this.config = config; - this.endpoint = this.config.endpoint(); - promptCreator = new HuggingFaceLlamaPrompt<>(config.systemMessage(), config.maxInputTokens()); - } + public HuggingFaceLlamaPlugin(HuggingFaceConfig config) { + this.config = config; + this.endpoint = this.config.endpoint(); + promptCreator = new HuggingFaceLlamaPrompt<>(config.systemMessage(), config.maxInputTokens()); + } - @Override - public T handle(ThreadState threadState) throws IOException { - ObjectNode body = MAPPER.createObjectNode(); - ObjectNode params = MAPPER.createObjectNode(); + @Override + public T handle(ThreadState threadState) throws IOException { + ObjectNode body = MAPPER.createObjectNode(); + ObjectNode params = MAPPER.createObjectNode(); - config.topP().ifPresent(v -> params.put("top_p", v)); - config.temperature().ifPresent(v -> params.put("temperature", v)); - config.maxOutputTokens().ifPresent(v -> params.put("max_new_tokens", v)); + config.topP().ifPresent(v -> params.put("top_p", v)); + config.temperature().ifPresent(v -> params.put("temperature", v)); + config.maxOutputTokens().ifPresent(v -> params.put("max_new_tokens", v)); - body.set("parameters", params); + body.set("parameters", params); - Optional prompt = promptCreator.createPrompt(threadState); - if (prompt.isEmpty()) { - return threadState.newMessageFromBot(Instant.now(), "I'm sorry but that request was too long for me."); - } + Optional prompt = promptCreator.createPrompt(threadState); + if (prompt.isEmpty()) { + return threadState.newMessageFromBot( + Instant.now(), "I'm sorry but that request was too long for me."); + } - body.put("inputs", prompt.get()); + body.put("inputs", prompt.get()); - String bodyString; - try { - bodyString = MAPPER.writeValueAsString(body); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); // this should be impossible - } - Response response = - Request.post(endpoint) - .bodyString(bodyString, ContentType.APPLICATION_JSON) - .setHeader("Authorization", "Bearer " + config.apiKey()) - .execute(); + String bodyString; + try { + bodyString = MAPPER.writeValueAsString(body); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); // this should be impossible + } + Response response = + Request.post(endpoint) + .bodyString(bodyString, ContentType.APPLICATION_JSON) + .setHeader("Authorization", "Bearer " + config.apiKey()) + .execute(); - JsonNode responseBody = MAPPER.readTree(response.returnContent().asBytes()); - String allGeneratedText = responseBody.get(0).get("generated_text").textValue(); - String llmResponse = allGeneratedText.strip().replace(prompt.get().strip(), ""); - Instant timestamp = Instant.now(); + JsonNode responseBody = MAPPER.readTree(response.returnContent().asBytes()); + String allGeneratedText = responseBody.get(0).get("generated_text").textValue(); + String llmResponse = allGeneratedText.strip().replace(prompt.get().strip(), ""); + Instant timestamp = Instant.now(); - return threadState.newMessageFromBot(timestamp, llmResponse); - } + return threadState.newMessageFromBot(timestamp, llmResponse); + } } diff --git a/src/main/java/com/meta/cp4m/llm/HuggingFaceLlamaPrompt.java b/src/main/java/com/meta/cp4m/llm/HuggingFaceLlamaPrompt.java index 317f9b7..061c6e2 100644 --- a/src/main/java/com/meta/cp4m/llm/HuggingFaceLlamaPrompt.java +++ b/src/main/java/com/meta/cp4m/llm/HuggingFaceLlamaPrompt.java @@ -21,88 +21,81 @@ public class HuggingFaceLlamaPrompt { - private final String systemMessage; - private final long maxInputTokens; - private final HuggingFaceTokenizer tokenizer; - - public HuggingFaceLlamaPrompt(String systemMessage, long maxInputTokens) { - - this.systemMessage = systemMessage; - this.maxInputTokens = maxInputTokens; - URL llamaTokenizerUrl = - Objects.requireNonNull( - HuggingFaceLlamaPrompt.class.getClassLoader().getResource("llamaTokenizer.json")); - URI llamaTokenizer; - try { - llamaTokenizer = llamaTokenizerUrl.toURI(); - tokenizer = HuggingFaceTokenizer.newInstance(Paths.get(llamaTokenizer)); - - } catch (URISyntaxException | IOException e) { - // this should be impossible - throw new RuntimeException(e); - } + private final String systemMessage; + private final long maxInputTokens; + private final HuggingFaceTokenizer tokenizer; + + public HuggingFaceLlamaPrompt(String systemMessage, long maxInputTokens) { + + this.systemMessage = systemMessage; + this.maxInputTokens = maxInputTokens; + URL llamaTokenizerUrl = + Objects.requireNonNull( + HuggingFaceLlamaPrompt.class.getClassLoader().getResource("llamaTokenizer.json")); + URI llamaTokenizer; + try { + llamaTokenizer = llamaTokenizerUrl.toURI(); + tokenizer = HuggingFaceTokenizer.newInstance(Paths.get(llamaTokenizer)); + + } catch (URISyntaxException | IOException e) { + // this should be impossible + throw new RuntimeException(e); } + } - public Optional createPrompt(ThreadState threadState) { - - PromptBuilder builder = new PromptBuilder(); - - int totalTokens = tokenCount(this.systemMessage) + 5; // Account for closing tokens - builder.addSystem(this.systemMessage); - - for (int i = threadState.messages().size() - 1; i >= 0; i--) { - Message m = threadState.messages().get(i); - totalTokens += tokenCount(m.message()); - if (totalTokens > maxInputTokens) { - if (i == threadState.messages().size() - 1){ - return Optional.empty(); - } - break; - } - switch (m.role()) { - case USER -> builder.addUser(m.message()); - case ASSISTANT -> builder.addAssistant(m.message()); - } - } + public Optional createPrompt(ThreadState threadState) { - return Optional.of(builder.build()); - } + PromptBuilder builder = new PromptBuilder(); + + int totalTokens = tokenCount(this.systemMessage) + 5; // Account for closing tokens + builder.addSystem(this.systemMessage); - private int tokenCount(String message) { - Encoding encoding = tokenizer.encode(message); - return encoding.getTokens().length; + for (int i = threadState.messages().size() - 1; i >= 0; i--) { + Message m = threadState.messages().get(i); + totalTokens += tokenCount(m.message()); + if (totalTokens > maxInputTokens) { + if (i == threadState.messages().size() - 1) { + return Optional.empty(); + } + break; + } + switch (m.role()) { + case USER -> builder.addUser(m.message()); + case ASSISTANT -> builder.addAssistant(m.message()); + } } - private static class PromptBuilder { - - StringBuilder promptStringBuilder = new StringBuilder(); - StringBuilder messagesStringBuilder = new StringBuilder(); + return Optional.of(builder.build()); + } - void addSystem(String message) { - promptStringBuilder - .append("[INST] <>\n") - .append(message) - .append("\n<>\n\n"); - } + private int tokenCount(String message) { + Encoding encoding = tokenizer.encode(message); + return encoding.getTokens().length; + } - void addAssistant(String message) { - StringBuilder tempBuilder = new StringBuilder(); - tempBuilder - .append(message) - .append(" [INST] "); - messagesStringBuilder.append(tempBuilder.reverse()); - } + private static class PromptBuilder { - void addUser(String message) { - StringBuilder tempBuilder = new StringBuilder(); - tempBuilder - .append(message) - .append(" [/INST] "); - messagesStringBuilder.append(tempBuilder.reverse()); - } + StringBuilder promptStringBuilder = new StringBuilder(); + StringBuilder messagesStringBuilder = new StringBuilder(); - String build() { - return promptStringBuilder.append(messagesStringBuilder.reverse()).toString().strip(); - } + void addSystem(String message) { + promptStringBuilder.append("[INST] <>\n").append(message).append("\n<>\n\n"); + } + + void addAssistant(String message) { + StringBuilder tempBuilder = new StringBuilder(); + tempBuilder.append(message).append(" [INST] "); + messagesStringBuilder.append(tempBuilder.reverse()); + } + + void addUser(String message) { + StringBuilder tempBuilder = new StringBuilder(); + tempBuilder.append(message).append(" [/INST] "); + messagesStringBuilder.append(tempBuilder.reverse()); + } + + String build() { + return promptStringBuilder.append(messagesStringBuilder.reverse()).toString().strip(); } + } } diff --git a/src/main/java/com/meta/cp4m/llm/OpenAIPlugin.java b/src/main/java/com/meta/cp4m/llm/OpenAIPlugin.java index 87e9fbe..1260ca9 100644 --- a/src/main/java/com/meta/cp4m/llm/OpenAIPlugin.java +++ b/src/main/java/com/meta/cp4m/llm/OpenAIPlugin.java @@ -149,9 +149,9 @@ public T handle(ThreadState threadState) throws IOException { ArrayNode messages = MAPPER.createArrayNode(); messages - .addObject() - .put("role", Role.SYSTEM.toString().toLowerCase()) - .put("content", config.systemMessage()); + .addObject() + .put("role", Role.SYSTEM.toString().toLowerCase()) + .put("content", config.systemMessage()); for (T message : threadState.messages()) { messages .addObject() diff --git a/src/main/java/com/meta/cp4m/message/FBMessengerConfig.java b/src/main/java/com/meta/cp4m/message/FBMessengerConfig.java index dc47a47..98289f2 100644 --- a/src/main/java/com/meta/cp4m/message/FBMessengerConfig.java +++ b/src/main/java/com/meta/cp4m/message/FBMessengerConfig.java @@ -10,10 +10,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.base.Preconditions; -import org.checkerframework.checker.nullness.qual.Nullable; - import java.util.Optional; import java.util.UUID; +import org.checkerframework.checker.nullness.qual.Nullable; public class FBMessengerConfig implements HandlerConfig { @@ -82,4 +81,4 @@ public String pageAccessToken() { public Optional connectedFacebookPageForInstagram() { return Optional.ofNullable(connectedFacebookPageForInstagram); } -} \ No newline at end of file +} diff --git a/src/main/java/com/meta/cp4m/message/MetaHandlerUtils.java b/src/main/java/com/meta/cp4m/message/MetaHandlerUtils.java index 6fc44d1..562332a 100644 --- a/src/main/java/com/meta/cp4m/message/MetaHandlerUtils.java +++ b/src/main/java/com/meta/cp4m/message/MetaHandlerUtils.java @@ -10,17 +10,15 @@ import io.javalin.http.Context; import io.javalin.http.ForbiddenResponse; +import io.javalin.http.HandlerType; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.OptionalInt; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; - -import io.javalin.http.HandlerType; import org.apache.hc.client5.http.utils.Hex; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/src/main/java/com/meta/cp4m/message/RequestProcessor.java b/src/main/java/com/meta/cp4m/message/RequestProcessor.java index ba29c01..44cc781 100644 --- a/src/main/java/com/meta/cp4m/message/RequestProcessor.java +++ b/src/main/java/com/meta/cp4m/message/RequestProcessor.java @@ -9,7 +9,6 @@ package com.meta.cp4m.message; import io.javalin.http.Context; - import java.util.List; @FunctionalInterface diff --git a/src/main/java/com/meta/cp4m/message/webhook/whatsapp/AudioWebhookMessage.java b/src/main/java/com/meta/cp4m/message/webhook/whatsapp/AudioWebhookMessage.java index d6986c8..9681f8e 100644 --- a/src/main/java/com/meta/cp4m/message/webhook/whatsapp/AudioWebhookMessage.java +++ b/src/main/java/com/meta/cp4m/message/webhook/whatsapp/AudioWebhookMessage.java @@ -26,6 +26,7 @@ public class AudioWebhookMessage implements ReferableWebhookMessage { private final Identifier id; private final Instant timestamp; private final @Nullable Referral referral; + @JsonCreator public AudioWebhookMessage( @JsonProperty("audio") Audio audio, diff --git a/src/main/java/com/meta/cp4m/message/webhook/whatsapp/ButtonWebhookMessage.java b/src/main/java/com/meta/cp4m/message/webhook/whatsapp/ButtonWebhookMessage.java index eb21b12..b713fa2 100644 --- a/src/main/java/com/meta/cp4m/message/webhook/whatsapp/ButtonWebhookMessage.java +++ b/src/main/java/com/meta/cp4m/message/webhook/whatsapp/ButtonWebhookMessage.java @@ -25,6 +25,7 @@ public class ButtonWebhookMessage implements WebhookMessage { private final Identifier from; private final Identifier id; private final Instant timestamp; + @JsonCreator public ButtonWebhookMessage( @JsonProperty("button") Button button, diff --git a/src/main/java/com/meta/cp4m/message/webhook/whatsapp/InteractiveWebhookMessage.java b/src/main/java/com/meta/cp4m/message/webhook/whatsapp/InteractiveWebhookMessage.java index 643fe17..18e0d25 100644 --- a/src/main/java/com/meta/cp4m/message/webhook/whatsapp/InteractiveWebhookMessage.java +++ b/src/main/java/com/meta/cp4m/message/webhook/whatsapp/InteractiveWebhookMessage.java @@ -26,6 +26,7 @@ public class InteractiveWebhookMessage implements WebhookMessage { private final Identifier from; private final Identifier id; private final Instant timestamp; + @JsonCreator public InteractiveWebhookMessage( @JsonProperty("interactive") ObjectNode interactive, diff --git a/src/main/java/com/meta/cp4m/message/webhook/whatsapp/OrderWebhookMessage.java b/src/main/java/com/meta/cp4m/message/webhook/whatsapp/OrderWebhookMessage.java index e21b046..a304479 100644 --- a/src/main/java/com/meta/cp4m/message/webhook/whatsapp/OrderWebhookMessage.java +++ b/src/main/java/com/meta/cp4m/message/webhook/whatsapp/OrderWebhookMessage.java @@ -25,6 +25,7 @@ public class OrderWebhookMessage implements WebhookMessage { private final Identifier from; private final Identifier id; private final Instant timestamp; + @JsonCreator public OrderWebhookMessage( @JsonProperty("order") Order order, diff --git a/src/main/java/com/meta/cp4m/routing/Acceptor.java b/src/main/java/com/meta/cp4m/routing/Acceptor.java index f62cdfa..afca464 100644 --- a/src/main/java/com/meta/cp4m/routing/Acceptor.java +++ b/src/main/java/com/meta/cp4m/routing/Acceptor.java @@ -8,10 +8,7 @@ package com.meta.cp4m.routing; -import com.fasterxml.jackson.databind.JsonNode; -import com.meta.cp4m.message.Message; import io.javalin.http.Context; - import java.util.Optional; @FunctionalInterface diff --git a/src/main/java/com/meta/cp4m/routing/Handler.java b/src/main/java/com/meta/cp4m/routing/Handler.java index 1fabbb2..eb3e4a8 100644 --- a/src/main/java/com/meta/cp4m/routing/Handler.java +++ b/src/main/java/com/meta/cp4m/routing/Handler.java @@ -10,8 +10,6 @@ import io.javalin.http.Context; -import java.util.List; - @FunctionalInterface public interface Handler { diff --git a/src/main/java/com/meta/cp4m/routing/Route.java b/src/main/java/com/meta/cp4m/routing/Route.java index d50e3c6..3478713 100644 --- a/src/main/java/com/meta/cp4m/routing/Route.java +++ b/src/main/java/com/meta/cp4m/routing/Route.java @@ -8,7 +8,6 @@ package com.meta.cp4m.routing; -import com.meta.cp4m.message.Message; import io.javalin.http.HandlerType; public record Route( diff --git a/src/main/java/com/meta/cp4m/store/ChatStore.java b/src/main/java/com/meta/cp4m/store/ChatStore.java index 48d4ef5..54e3bf5 100644 --- a/src/main/java/com/meta/cp4m/store/ChatStore.java +++ b/src/main/java/com/meta/cp4m/store/ChatStore.java @@ -16,8 +16,8 @@ * This class is in charge of both maintaining a chat history and managing a queue of conversations * that require a response. * - *

Adding a message from {@link com.meta.cp4m.message.Message.Role#USER} will indicate that - * the conversation needs a response. + *

Adding a message from {@link com.meta.cp4m.message.Message.Role#USER} will indicate that the + * conversation needs a response. * * @param the type of message being passed */ diff --git a/src/test/java/com/meta/cp4m/llm/HuggingFaceConfigTest.java b/src/test/java/com/meta/cp4m/llm/HuggingFaceConfigTest.java index 91c7ab6..1db5e9e 100644 --- a/src/test/java/com/meta/cp4m/llm/HuggingFaceConfigTest.java +++ b/src/test/java/com/meta/cp4m/llm/HuggingFaceConfigTest.java @@ -28,7 +28,7 @@ class HuggingFaceConfigTest { - private static final ObjectMapper MAPPER = ConfigurationUtils.jsonMapper(); + private static final ObjectMapper MAPPER = ConfigurationUtils.jsonMapper(); static final Collection CONFIG_ITEMS = ImmutableList.of( new ConfigItem( @@ -90,92 +90,94 @@ class HuggingFaceConfigTest { false, LongNode.valueOf(2000), List.of(LongNode.valueOf(-1), LongNode.valueOf(100_000)))); - private ObjectNode minimalConfig; + private ObjectNode minimalConfig; - static Stream configItems() { - return CONFIG_ITEMS.stream(); - } + static Stream configItems() { + return CONFIG_ITEMS.stream(); + } - static Stream invalidValues() { - return configItems() - .flatMap(c -> c.invalidValues().stream().map(t -> Arguments.of(c.key(), t))); - } + static Stream invalidValues() { + return configItems() + .flatMap(c -> c.invalidValues().stream().map(t -> Arguments.of(c.key(), t))); + } - static Stream requiredKeys() { - return configItems().filter(ConfigItem::required).map(ConfigItem::key); - } + static Stream requiredKeys() { + return configItems().filter(ConfigItem::required).map(ConfigItem::key); + } - @BeforeEach - void setUp() { - minimalConfig = MAPPER.createObjectNode(); - CONFIG_ITEMS.forEach( - t -> { - if (t.required()) { - minimalConfig.set(t.key(), t.validValue()); - } - }); - } + @BeforeEach + void setUp() { + minimalConfig = MAPPER.createObjectNode(); + CONFIG_ITEMS.forEach( + t -> { + if (t.required()) { + minimalConfig.set(t.key(), t.validValue()); + } + }); + } - @Test - void maximalValidConfig() throws JsonProcessingException { - ObjectNode body = MAPPER.createObjectNode(); - CONFIG_ITEMS.forEach(t -> body.set(t.key(), t.validValue())); - HuggingFaceConfig config = MAPPER.readValue(MAPPER.writeValueAsString(body), HuggingFaceConfig.class); - assertThat(config.temperature().isPresent()).isTrue(); - assertThat(config.frequencyPenalty().isPresent()).isTrue(); - assertThat(config.topP().isPresent()).isTrue(); - assertThat(config.maxOutputTokens().isPresent()).isTrue(); - assertThat(config.presencePenalty().isPresent()).isTrue(); - assertThat(config.frequencyPenalty().isPresent()).isTrue(); - assertThat(config.logitBias().isEmpty()).isFalse(); - } + @Test + void maximalValidConfig() throws JsonProcessingException { + ObjectNode body = MAPPER.createObjectNode(); + CONFIG_ITEMS.forEach(t -> body.set(t.key(), t.validValue())); + HuggingFaceConfig config = + MAPPER.readValue(MAPPER.writeValueAsString(body), HuggingFaceConfig.class); + assertThat(config.temperature().isPresent()).isTrue(); + assertThat(config.frequencyPenalty().isPresent()).isTrue(); + assertThat(config.topP().isPresent()).isTrue(); + assertThat(config.maxOutputTokens().isPresent()).isTrue(); + assertThat(config.presencePenalty().isPresent()).isTrue(); + assertThat(config.frequencyPenalty().isPresent()).isTrue(); + assertThat(config.logitBias().isEmpty()).isFalse(); + } - @Test - void minimalValidConfig() throws JsonProcessingException { - ObjectNode body = MAPPER.createObjectNode(); - CONFIG_ITEMS.forEach( - t -> { - if (t.required()) { - body.set(t.key(), t.validValue()); - } - }); - HuggingFaceConfig config = MAPPER.readValue(MAPPER.writeValueAsString(body), HuggingFaceConfig.class); - assertThat(config.temperature().isEmpty()).isTrue(); - assertThat(config.frequencyPenalty().isEmpty()).isTrue(); - assertThat(config.topP().isEmpty()).isTrue(); - assertThat(config.maxOutputTokens().isEmpty()).isTrue(); - assertThat(config.presencePenalty().isEmpty()).isTrue(); - assertThat(config.frequencyPenalty().isEmpty()).isTrue(); - assertThat(config.logitBias().isEmpty()).isTrue(); - } + @Test + void minimalValidConfig() throws JsonProcessingException { + ObjectNode body = MAPPER.createObjectNode(); + CONFIG_ITEMS.forEach( + t -> { + if (t.required()) { + body.set(t.key(), t.validValue()); + } + }); + HuggingFaceConfig config = + MAPPER.readValue(MAPPER.writeValueAsString(body), HuggingFaceConfig.class); + assertThat(config.temperature().isEmpty()).isTrue(); + assertThat(config.frequencyPenalty().isEmpty()).isTrue(); + assertThat(config.topP().isEmpty()).isTrue(); + assertThat(config.maxOutputTokens().isEmpty()).isTrue(); + assertThat(config.presencePenalty().isEmpty()).isTrue(); + assertThat(config.frequencyPenalty().isEmpty()).isTrue(); + assertThat(config.logitBias().isEmpty()).isTrue(); + } - @ParameterizedTest - @MethodSource("configItems") - void nullValues(ConfigItem item) throws JsonProcessingException { - minimalConfig.putNull(item.key()); - String bodyString = MAPPER.writeValueAsString(minimalConfig); - assertThatThrownBy(() -> MAPPER.readValue(bodyString, HuggingFaceConfig.class)) - .isInstanceOf(Exception.class); - } + @ParameterizedTest + @MethodSource("configItems") + void nullValues(ConfigItem item) throws JsonProcessingException { + minimalConfig.putNull(item.key()); + String bodyString = MAPPER.writeValueAsString(minimalConfig); + assertThatThrownBy(() -> MAPPER.readValue(bodyString, HuggingFaceConfig.class)) + .isInstanceOf(Exception.class); + } - @ParameterizedTest - @MethodSource("invalidValues") - void invalidValues(String key, JsonNode value) throws JsonProcessingException { - minimalConfig.set(key, value); - String bodyString = MAPPER.writeValueAsString(minimalConfig); - assertThatThrownBy(() -> MAPPER.readValue(bodyString, HuggingFaceConfig.class)) - .isInstanceOf(Exception.class); - } + @ParameterizedTest + @MethodSource("invalidValues") + void invalidValues(String key, JsonNode value) throws JsonProcessingException { + minimalConfig.set(key, value); + String bodyString = MAPPER.writeValueAsString(minimalConfig); + assertThatThrownBy(() -> MAPPER.readValue(bodyString, HuggingFaceConfig.class)) + .isInstanceOf(Exception.class); + } - @ParameterizedTest - @MethodSource("requiredKeys") - void requiredKeysMissing(String key) throws JsonProcessingException { - minimalConfig.remove(key); - String bodyString = MAPPER.writeValueAsString(minimalConfig); - assertThatThrownBy(() -> MAPPER.readValue(bodyString, HuggingFaceConfig.class)) - .isInstanceOf(Exception.class); - } + @ParameterizedTest + @MethodSource("requiredKeys") + void requiredKeysMissing(String key) throws JsonProcessingException { + minimalConfig.remove(key); + String bodyString = MAPPER.writeValueAsString(minimalConfig); + assertThatThrownBy(() -> MAPPER.readValue(bodyString, HuggingFaceConfig.class)) + .isInstanceOf(Exception.class); + } - record ConfigItem( - String key, boolean required, JsonNode validValue, List invalidValues) {} + record ConfigItem( + String key, boolean required, JsonNode validValue, List invalidValues) {} } diff --git a/src/test/java/com/meta/cp4m/llm/OpenAIPluginTest.java b/src/test/java/com/meta/cp4m/llm/OpenAIPluginTest.java index d11aee5..9168690 100644 --- a/src/test/java/com/meta/cp4m/llm/OpenAIPluginTest.java +++ b/src/test/java/com/meta/cp4m/llm/OpenAIPluginTest.java @@ -212,7 +212,7 @@ void orderedCorrectly() throws IOException, InterruptedException { for (int i = 0; i < thread.messages().size(); i++) { FBMessage threadMessage = thread.messages().get(i); - JsonNode sentMessage = body.get("messages").get(i + 1); // system message is not in the stack + JsonNode sentMessage = body.get("messages").get(i + 1); // system message is not in the stack assertSoftly( s -> s.assertThat(threadMessage.message()) From 107f90cf0ae6f22deec74488528db7c5d46fc7a2 Mon Sep 17 00:00:00 2001 From: Hunter Jackson Date: Tue, 16 Jan 2024 16:40:16 -0500 Subject: [PATCH 6/6] fix introduced typo --- CODE_OF_CONDUCT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 1b4e4bd..08b500a 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -33,7 +33,7 @@ Examples of unacceptable behavior by participants include: ## Our Responsibilities -Project maintainers are responsible for clarifying the standards of acceptor +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.