Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Instagram Direct Message handling #21

Merged
merged 16 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ build/
### custom ###
src/main/java/com/meta/cp4m/mccloskeytest.java
/dependency-reduced-pom.xml
CP4M.toml
17 changes: 12 additions & 5 deletions src/main/java/com/meta/cp4m/message/FBMessageHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public class FBMessageHandler implements MessageHandler<FBMessage> {
private final String appSecret;

private final String accessToken;
private final Boolean isInstagram;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in general you want to use primitives bool where possible because it is not possible for them to be null


private final Deduplicator<Identifier> messageDeduplicator = new Deduplicator<>(10_000);
private Function<Identifier, URI> baseURLFactory =
Expand All @@ -62,16 +63,18 @@ public class FBMessageHandler implements MessageHandler<FBMessage> {
}
};

public FBMessageHandler(String verifyToken, String pageAccessToken, String appSecret) {
public FBMessageHandler(String verifyToken, String pageAccessToken, String appSecret, Boolean isInstagram) {
this.verifyToken = verifyToken;
this.appSecret = appSecret;
this.accessToken = pageAccessToken;
this.isInstagram = isInstagram;
}

FBMessageHandler(FBMessengerConfig config) {
this.verifyToken = config.verifyToken();
this.appSecret = config.appSecret();
this.accessToken = config.pageAccessToken();
this.isInstagram = config.isInstagram();
}

@Override
Expand All @@ -88,7 +91,7 @@ public List<FBMessage> 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())
Expand Down Expand Up @@ -122,10 +125,10 @@ private List<FBMessage> postHandler(Context ctx) throws JsonProcessingException
String bodyString = ctx.body();
JsonNode body = MAPPER.readTree(bodyString);
String object = body.get("object").textValue();
if (!object.equals("page")) {
if (!object.equals("page") && !object.equals("instagram")) {
LOGGER
.atWarn()
.setMessage("received body that has a different value for 'object' than 'page'")
.setMessage("received body with value of " + object + " for 'object', expected 'page' or 'instagram'")
.addKeyValue("body", bodyString)
.log();
return Collections.emptyList();
Expand All @@ -145,6 +148,10 @@ private List<FBMessage> postHandler(Context ctx) throws JsonProcessingException
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)) {
Expand Down Expand Up @@ -206,7 +213,7 @@ private void send(String message, Identifier recipient, Identifier sender) throw
try {
bodyString = MAPPER.writeValueAsString(body);
url =
new URIBuilder(baseURLFactory.apply(sender))
new URIBuilder(baseURLFactory.apply(isInstagram ? Identifier.from("me") : sender))
.addParameter("access_token", accessToken)
.build();
} catch (JsonProcessingException | URISyntaxException e) {
Expand Down
96 changes: 52 additions & 44 deletions src/main/java/com/meta/cp4m/message/FBMessengerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,60 +10,68 @@

import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;

import java.util.UUID;

public class FBMessengerConfig implements HandlerConfig {

private final String name;
private final String verifyToken;
private final String appSecret;
private final String pageAccessToken;
private final String name;
private final String verifyToken;
private final String appSecret;
private final String pageAccessToken;
private final Boolean isInstagram;

private FBMessengerConfig(
@JsonProperty("name") String name,
@JsonProperty("verify_token") String verifyToken,
@JsonProperty("app_secret") String appSecret,
@JsonProperty("page_access_token") String pageAccessToken,
@JsonProperty("is_instagram") Boolean isInstagram) {

private FBMessengerConfig(
@JsonProperty("name") String name,
@JsonProperty("verify_token") String verifyToken,
@JsonProperty("app_secret") String appSecret,
@JsonProperty("page_access_token") String pageAccessToken) {
Preconditions.checkArgument(name != null && !name.isBlank(), "name cannot be blank");
Preconditions.checkArgument(
verifyToken != null && !verifyToken.isBlank(), "verify_token cannot be blank");
Preconditions.checkArgument(
appSecret != null && !appSecret.isBlank(), "app_secret cannot be blank");
Preconditions.checkArgument(
pageAccessToken != null && !pageAccessToken.isBlank(), "page_access_token cannot be blank");

Preconditions.checkArgument(name != null && !name.isBlank(), "name cannot be blank");
Preconditions.checkArgument(
verifyToken != null && !verifyToken.isBlank(), "verify_token cannot be blank");
Preconditions.checkArgument(
appSecret != null && !appSecret.isBlank(), "app_secret cannot be blank");
Preconditions.checkArgument(
pageAccessToken != null && !pageAccessToken.isBlank(), "page_access_token cannot be blank");
this.name = name;
this.verifyToken = verifyToken;
this.appSecret = appSecret;
this.pageAccessToken = pageAccessToken;
this.isInstagram = isInstagram != null ? isInstagram : false;
}

this.name = name;
this.verifyToken = verifyToken;
this.appSecret = appSecret;
this.pageAccessToken = pageAccessToken;
}
public static FBMessengerConfig of(String verifyToken, String appSecret, String pageAccessToken, Boolean isInstagram) {
// human readability of the name only matters when it's coming from a config
return new FBMessengerConfig(
UUID.randomUUID().toString(), verifyToken, appSecret, pageAccessToken, isInstagram);
}

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

@Override
public String name() {
return name;
}
public String verifyToken() {
return verifyToken;
}

public String verifyToken() {
return verifyToken;
}
public String appSecret() {
return appSecret;
}

public String appSecret() {
return appSecret;
}
@Override
public FBMessageHandler toMessageHandler() {
return new FBMessageHandler(this);
}

@Override
public FBMessageHandler toMessageHandler() {
return new FBMessageHandler(this);
}
public String pageAccessToken() {
return pageAccessToken;
}

public String pageAccessToken() {
return pageAccessToken;
}
}
public Boolean isInstagram() {
return isInstagram;
}
}
2 changes: 1 addition & 1 deletion src/test/java/com/meta/cp4m/llm/DummyLLMPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public DummyLLMPlugin(String dummyLLMResponse) {
public ThreadState<T> take(int waitMs) throws InterruptedException {
@Nullable ThreadState<T> value = receivedThreadStates.poll(waitMs, TimeUnit.MILLISECONDS);
if (value == null) {
throw new RuntimeException("unable to remove item form queue in under " + waitMs + "ms");
throw new RuntimeException("unable to remove item from queue in under " + waitMs + "ms");
}
return value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ void inPipeline() throws IOException, URISyntaxException, InterruptedException {
metaRequests.put(
new OutboundRequest(ctx.body(), ctx.headerMap(), ctx.queryParamMap())));
FBMessageHandler handler =
new FBMessageHandler(verifyToken, accessToken, appSecret)
new FBMessageHandler(verifyToken, accessToken, appSecret, false)
.baseURLFactory(ignored -> messageReceiver);

String apiKey = "api key";
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/com/meta/cp4m/llm/OpenAIPluginTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ void inPipeline() throws IOException, URISyntaxException, InterruptedException {
metaRequests.put(
new OutboundRequest(ctx.body(), ctx.headerMap(), ctx.queryParamMap())));
FBMessageHandler handler =
new FBMessageHandler(verifyToken, accessToken, appSecret)
new FBMessageHandler(verifyToken, accessToken, appSecret, false)
.baseURLFactory(ignored -> messageReceiver);

String apiKey = "api key";
Expand Down
Loading