Skip to content

Commit

Permalink
Merge pull request #66 from xatkit-bot-platform/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
taidopurason authored Nov 8, 2020
2 parents b54e57d + ddbfdef commit 3afb449
Show file tree
Hide file tree
Showing 34 changed files with 1,098 additions and 72 deletions.
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
package com.xatkit.plugins.messenger.platform;

import com.google.gson.JsonElement;
import com.xatkit.core.XatkitBot;
import com.xatkit.core.platform.RuntimePlatform;
import com.xatkit.core.server.*;
import com.xatkit.execution.StateContext;
import com.xatkit.plugins.messenger.platform.action.*;
import com.xatkit.plugins.messenger.platform.entity.Message;
import com.xatkit.plugins.messenger.platform.entity.Messaging;
import com.xatkit.plugins.messenger.platform.entity.Recipient;
import com.xatkit.plugins.messenger.platform.entity.SenderAction;
import com.xatkit.plugins.messenger.platform.entity.*;
import com.xatkit.plugins.messenger.platform.entity.payloads.AttachmentIdPayload;
import com.xatkit.plugins.messenger.platform.entity.response.ErrorResponse;
import com.xatkit.plugins.messenger.platform.entity.response.Response;
import com.xatkit.plugins.messenger.platform.entity.response.SendResponse;
import com.xatkit.plugins.rest.platform.RestPlatform;
import com.xatkit.plugins.rest.platform.action.JsonRestRequest;
import com.xatkit.plugins.rest.platform.utils.ApiResponse;
import lombok.NonNull;
import fr.inria.atlanmod.commons.log.Log;
import lombok.val;
import lombok.var;
import org.apache.commons.configuration2.Configuration;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.apache.http.entity.StringEntity;

import java.nio.charset.StandardCharsets;

import static java.util.Objects.requireNonNull;

// TODO: Add javadocs
Expand All @@ -28,69 +36,110 @@ public class MessengerPlatform extends RestPlatform {
private String verifyToken;
private String accessToken;
private String appSecret;
private int messagesSent;
private int messagesSentSuccessfully;

@Override
public void start(@NonNull XatkitBot xatkitBot, @NonNull Configuration configuration) {
verifyToken = requireNonNull(configuration.getString(MessengerUtils.VERIFY_TOKEN_KEY));
accessToken = requireNonNull(configuration.getString(MessengerUtils.ACCESS_TOKEN_KEY));
appSecret = requireNonNull(configuration.getString(MessengerUtils.APP_SECRET_KEY));
messagesSent = 0;
messagesSentSuccessfully = 0;
super.start(xatkitBot, configuration);

xatkitBot.getXatkitServer().registerRestEndpoint(HttpMethod.GET, "/messenger/webhook",
xatkitBot.getXatkitServer().registerRestEndpoint(HttpMethod.GET, MessengerUtils.WEBHOOK_URI,
RestHandlerFactory.createEmptyContentRestHandler((headers, params, content) -> {
val mode = requireNonNull(HttpUtils.getParameterValue("hub.mode", params), "Missing mode");
val token = requireNonNull(HttpUtils.getParameterValue("hub.verify_token", params), "Missing token");
val challenge = requireNonNull(HttpUtils.getParameterValue("hub.challenge", params), "Missing challenge");
if (!mode.equals("subscribe")) {
throw new RestHandlerException(403, "Mode is not 'subscribe'");
throw new RestHandlerException(HttpStatus.SC_FORBIDDEN, "Mode is not 'subscribe'");
}
if (!token.equals(verifyToken)) {
throw new RestHandlerException(403, "Token does not match verify token.");
throw new RestHandlerException(HttpStatus.SC_FORBIDDEN, "Token does not match verify token.");
}
return new StringEntity(challenge, "UTF-8");
return new StringEntity(challenge, StandardCharsets.UTF_8);
}));
}

public void markSeen(@NonNull StateContext context) {
sendAction(context, SenderAction.markSeen);
public Response markSeen(@NonNull StateContext context) {
return sendAction(context, SenderAction.markSeen);
}

public Response sendAction(@NonNull StateContext context, @NonNull SenderAction senderAction) {
val recipientId = MessengerUtils.extractContextId(context.getContextId());
Log.debug("Replying to {0} with a sender_action {1}", recipientId, senderAction.name());
val messaging = new Messaging(new Recipient(recipientId), senderAction);
return reply(new Reply(this, context, messaging));
}

public Response uploadFile(@NonNull StateContext context, File file) {
return excecuteRequest(new FileReply(this, context, file));
}

public Response sendFile(@NonNull StateContext context, @NonNull String attachmentId, @NonNull Attachment.AttachmentType attachmentType) {
val recipientId = MessengerUtils.extractContextId(context.getContextId());
val messaging = new Messaging(
new Recipient(recipientId),
new Message(new Attachment(attachmentType, new AttachmentIdPayload(attachmentId))));
Log.debug("SENDING FILE TO: {0}", recipientId);
return reply(new Reply(this, context, messaging)
);
}

public Response sendFile(@NonNull StateContext context, @NonNull File file) {
var attachmentId = file.getAttachmentId(); //I did not use the custom extractContextId here, so this is a potential error place
if (StringUtils.isEmpty(attachmentId)) {
val response = uploadFile(context, file);
if (!(response instanceof SendResponse)) {
Log.error("Could not upload the file.");
return response;
}
attachmentId = ((SendResponse) response).getAttachmentId();
}

return sendFile(context, attachmentId, file.getAttachment().getType());
}

public void sendAction(@NonNull StateContext context, @NonNull SenderAction senderAction) {
val senderId = context.getContextId();
Log.debug("Replying to {0} with a sender_action {1}", senderId, senderAction.name());
val messaging = new Messaging(new Recipient(senderId), senderAction);
excecuteReply(new Reply(this, context, messaging));
public Response reply(@NonNull StateContext context, @NonNull String text) {
return reply(context, new Message(text));
}

public void reply(@NonNull StateContext context, @NonNull String text) {
reply(context, new Message(text));
public Response reply(@NonNull StateContext context, @NonNull Message message) {
val recipientId = MessengerUtils.extractContextId(context.getContextId());
Log.debug("REPLYING TO: {0}", recipientId);
val messaging = new Messaging(new Recipient(recipientId), message);
return reply(new MessageReply(this, context, messaging));
}

public void reply(@NonNull StateContext context, @NonNull Message message) {
val senderId = context.getContextId();
Log.debug("REPLYING TO: {0}", senderId);
val messaging = new Messaging(new Recipient(senderId), message);
excecuteReply(new MessageReply(
this,
context,
messaging));
private Response reply(@NonNull Reply reply) {
return excecuteRequest(reply);
}

private void excecuteReply(Reply reply) {
messagesSent++;
val result = reply.call().getResult();
private Response excecuteRequest(JsonRestRequest<JsonElement> request) {
val result = request.call().getResult();

if (result instanceof ApiResponse) {
val apiResponse = (ApiResponse<?>) result;
val responseBody = ((JsonElement) apiResponse.getBody()).getAsJsonObject();
val status = apiResponse.getStatus();
if (status < 200 || status > 299) {
Log.error("REPLY RESPONSE STATUS: {0} {1}\n BODY: {2}", apiResponse.getStatus(), apiResponse.getStatusText(), apiResponse.getBody().toString());
if (responseBody.has("error")) {
val error = responseBody.get("error").getAsJsonObject();
val code = error.has("code") ? error.get("code").getAsInt() : null;
val subcode = error.has("error_subcode") ? error.get("error_subcode").getAsInt() : null;
val fbtraceId = error.has("fbtrace_id") ? error.get("fbtrace_id").getAsString() : null;
val message = error.has("message") ? error.get("message").getAsString() : null;
return new ErrorResponse(status, code, subcode, fbtraceId, message);
}
return null;
}
Log.debug("REPLY RESPONSE STATUS: {0} {1}\n BODY: {2}", apiResponse.getStatus(), apiResponse.getStatusText(), apiResponse.getBody().toString());
if (apiResponse.getStatus() == 200) messagesSentSuccessfully++;
} else {
Log.debug("Unexpected reply result: {0}", result);
val recipientId = responseBody.has("recipient_id") ? responseBody.get("recipient_id").getAsString() : null;
val attachmentId = responseBody.has("attachment_id") ? responseBody.get("attachment_id").getAsString() : null;
val messageId = responseBody.has("message_id") ? responseBody.get("message_id").getAsString() : null;
return new SendResponse(status, recipientId, messageId, attachmentId);
}
Log.error("Unexpected reply result: {0}", result);
return null;
}

public String getAppSecret() {
Expand All @@ -100,6 +149,4 @@ public String getAppSecret() {
public String getAccessToken() {
return accessToken;
}

public int getMessagesSent() { return messagesSent; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public class MessengerUtils {
public static final String VERIFY_TOKEN_KEY = MESSENGER_CONTEXT + "verify_token";
public static final String ACCESS_TOKEN_KEY = MESSENGER_CONTEXT + "access_token";
public static final String APP_SECRET_KEY = MESSENGER_CONTEXT + "app_secret";
public static final String INTENT_FROM_POSTBACK = MESSENGER_CONTEXT + "intent_from_postback";
public static final String INTENT_FROM_REACTION = MESSENGER_CONTEXT + "intent_from_reaction";
public static final String HANDLE_REACTIONS_KEY = MESSENGER_CONTEXT + "handle_reactions";
public static final String HANDLE_DELIVERIES_KEY = MESSENGER_CONTEXT + "handle_deliveries";
public static final String HANDLE_READ_KEY = MESSENGER_CONTEXT + "handle_read";
Expand All @@ -21,10 +23,18 @@ public class MessengerUtils {
public static final String MESSAGE_ID_KEY = "mid";
public static final String MESSAGE_IDS_KEY = "mids";
public static final String WATERMARK_KEY = "watermark";
public static final String POSTBACK_TITLE_KEY = "title";
public static final String POSTBACK_PAYLOAD_KEY = "payload";
public static final String POSTBACK_REFFERAL_REF_KEY = "refferal.ref";
public static final String POSTBACK_REFFERAL_SOURCE_KEY = "refferal.source";
public static final String POSTBACK_REFFERAL_TYPE_KEY = "refferal.type";
public static final String EMOJI_KEY = "emoji";
public static final String REACTION_KEY = "reaction";
public static final String WEBHOOK_URI = "/messenger/webhook";
public static final String SEND_API_URL = "https://graph.facebook.com/v8.0/me/messages";
public static final String ATTACHMENT_UPLOAD_API_URL = "https://graph.facebook.com/v8.0/me/message_attachments";
public static final String USE_REACTION_TEXT = "use_reaction_text";
public static final String USE_TITLE_TEXT = "use_title_text";


public static String calculateRFC2104HMAC(String data, String key) throws NoSuchAlgorithmException, InvalidKeyException {
Expand All @@ -33,4 +43,11 @@ public static String calculateRFC2104HMAC(String data, String key) throws NoSuch
mac.init(signingKey);
return new String(Hex.encodeHex(mac.doFinal(data.getBytes())));
}

//If one adds dialogflow to the bot, dialogflow will add some prefix to the context id
public static String extractContextId(String contextIdWithDialogflow) {
String[] senderIdSplit = contextIdWithDialogflow.split("/"); //This removes the useless part and leaves only the id
return senderIdSplit[senderIdSplit.length - 1]; //A better solution is more than welcome

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.xatkit.plugins.messenger.platform.action;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.request.HttpRequest;
import com.xatkit.execution.StateContext;
import com.xatkit.plugins.messenger.platform.MessengerPlatform;
import com.xatkit.plugins.messenger.platform.MessengerUtils;
import com.xatkit.plugins.messenger.platform.entity.File;
import com.xatkit.plugins.messenger.platform.entity.Message;
import com.xatkit.plugins.rest.platform.action.JsonRestRequest;
import lombok.val;
import org.apache.http.HttpHeaders;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

public class FileReply extends JsonRestRequest<JsonElement> {
private static final Gson gson = new Gson();
private final File file;

/**
* Constructs a POST Json request with form data parameters
*
* @param platform the {@link MessengerPlatform} containing this action
* @param context the {@link StateContext} associated to this action
* @param file the information related to the file to be sent;
*/
public FileReply(MessengerPlatform platform, StateContext context, File file) {
super(platform, context, MethodKind.POST, MessengerUtils.ATTACHMENT_UPLOAD_API_URL, null, null, null, generateHeaders(platform), generateParams(file));
this.file = file;
}

private static Map<String, String> generateHeaders(MessengerPlatform platform) {
val headers = new HashMap<String, String>();
headers.put(HttpHeaders.AUTHORIZATION, "Bearer " + platform.getAccessToken());
return headers;
}

private static Map<String, Object> generateParams(File file) {
Map<String, Object> params = new LinkedHashMap<>();
params.put("message", gson.toJsonTree(new Message(file.getAttachment())));
return params;
}

protected HttpRequest buildRequest() {
return Unirest
.post(this.restEndpoint)
.headers(this.headers)
.fields(this.formParameters)
.field("filedata", file.getFile(), file.getMimeType())
.getHttpRequest();
}
}
Loading

0 comments on commit 3afb449

Please sign in to comment.