All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.marsbits.restfbmessenger.DefaultMessenger Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2015-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.marsbits.restfbmessenger;

import com.github.marsbits.restfbmessenger.send.DefaultSendOperations;
import com.github.marsbits.restfbmessenger.send.SendOperations;
import com.github.marsbits.restfbmessenger.webhook.CallbackHandler;
import com.restfb.Connection;
import com.restfb.DefaultFacebookClient;
import com.restfb.FacebookClient;
import com.restfb.Parameter;
import com.restfb.Version;
import com.restfb.exception.FacebookException;
import com.restfb.types.User;
import com.restfb.types.send.CallToAction;
import com.restfb.types.send.DomainActionTypeEnum;
import com.restfb.types.send.Greeting;
import com.restfb.types.send.Message;
import com.restfb.types.send.PageMessageTag;
import com.restfb.types.send.SendResponse;
import com.restfb.types.send.SettingTypeEnum;
import com.restfb.types.send.ThreadStateEnum;
import com.restfb.types.webhook.WebhookObject;
import com.restfb.util.EncodingUtils;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.WARNING;

/**
 * Default implementation of the {@link Messenger} interface.
 *
 * @author Marcel Overdijk
 * @since 1.0.0
 */
public class DefaultMessenger implements Messenger {

    private static final Logger logger = Logger.getLogger(DefaultMessenger.class.getName());

    public static final Version DEFAULT_API_VERSION = Version.VERSION_2_11;

    public static final String THREAD_SETTINGS_PATH = "me/thread_settings";
    public static final String PAGE_MESSAGE_TAGS_PATH = "page_message_tags";

    public static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
    public static final String SIGNATURE_PREFIX = "sha1=";

    public static final String OBJECT_PAGE_VALUE = "page";

    public static final String USER_FIELDS_PARAM_NAME = "fields";
    public static final String USER_FIELDS_DEFAULT_VALUE = "first_name,last_name,profile_pic,locale,timezone,gender";

    public static final String SETTING_TYPE_PARAM_NAME = "setting_type";
    public static final String GREETING_PARAM_NAME = "greeting";
    public static final String THREAD_STATE_PARAM_NAME = "thread_state";
    public static final String CALL_TO_ACTIONS_PARAM_NAME = "call_to_actions";
    public static final String ACCOUNT_LINKING_URL_PARAM_NAME = "account_linking_url";
    public static final String WHITELISTED_DOMAINS_PARAM_NAME = "whitelisted_domains";
    public static final String DOMAIN_ACTION_TYPE_PARAM_NAME = "domain_action_type";

    protected String verifyToken;
    protected String appSecret;

    protected FacebookClient facebookClient;

    protected SendOperations sendOperations;
    protected CallbackHandler callbackHandler;

    /**
     * Creates a {@code DefaultMessenger} instance. If the app secret is not provided ({@code null} the callback signature verification will
     * be disabled.
     *
     * @param verifyToken     the verify token
     * @param accessToken     the access token
     * @param appSecret       the app secret
     * @param callbackHandler the callback handler
     */
    public DefaultMessenger(String verifyToken, String accessToken, String appSecret, CallbackHandler callbackHandler) {
        this(verifyToken, accessToken, appSecret, callbackHandler, DEFAULT_API_VERSION);
    }

    /**
     * Creates a {@code DefaultMessenger} instance.
     *
     * If the app secret is not provided ({@code null} the callback signature verification will be disabled.
     *
     * @param verifyToken     the verify token
     * @param accessToken     the access token
     * @param appSecret       the app secret
     * @param callbackHandler the callback handler
     * @param apiVersion      the api version
     */
    public DefaultMessenger(String verifyToken, String accessToken, String appSecret, CallbackHandler callbackHandler, Version apiVersion) {
        this(verifyToken, appSecret, callbackHandler, new DefaultFacebookClient(accessToken, appSecret, apiVersion));
    }

    /**
     * Creates a {@code DefaultMessenger} instance.
     *
     * If the app secret is not provided ({@code null} the callback signature verification will be disabled.
     *
     * The access token and api version need to be configured on the provided facebook client.
     *
     * @param verifyToken     the verify token
     * @param appSecret       the app secret
     * @param callbackHandler the callback handler
     * @param facebookClient  the facebook client
     */
    public DefaultMessenger(String verifyToken, String appSecret, CallbackHandler callbackHandler, FacebookClient facebookClient) {
        this.verifyToken = verifyToken;
        this.appSecret = appSecret;
        this.callbackHandler = callbackHandler;
        this.facebookClient = facebookClient;
        this.sendOperations = new DefaultSendOperations(facebookClient);
        if (appSecret == null) {
            if (logger.isLoggable(WARNING)) {
                logger.warning("App secret not configured; webhook signature will not be verified");
            }
        }
        if (callbackHandler == null) {
            if (logger.isLoggable(WARNING)) {
                logger.warning("Webhook handler not configured; webhook callbacks will not be handled");
            }
        }
    }

    @Override
    public boolean verifyToken(String token) {
        return token != null && token.equals(verifyToken);
    }

    @Override
    public void handleCallback(String payload, String signature) {
        if (callbackHandler == null) {
            if (logger.isLoggable(FINE)) {
                logger.fine("Webhook received but no webhook handler configured");
            }
        } else {
            if (logger.isLoggable(FINE)) {
                logger.fine(format("Handling webhook for payload: %s, signature: %s", payload, signature));
            }
            if (appSecret != null) {
                if (!verifySignature(payload, signature)) {
                    if (logger.isLoggable(FINE)) {
                        logger.fine("Invalid signature received; webhook handler not invoked");
                    }
                    return;
                }
            }
            WebhookObject webhookObject = facebookClient.getJsonMapper().toJavaObject(payload, WebhookObject.class);
            if (!OBJECT_PAGE_VALUE.equals(webhookObject.getObject())) {
                if (logger.isLoggable(FINE)) {
                    logger.fine(format("Ignoring webhook object: %s; webhook handler not invoked", webhookObject.getObject()));
                }
                return;
            }
            callbackHandler.onCallback(this, webhookObject);
        }
    }


    protected boolean verifySignature(String payload, String signature) {
        if (signature == null || !signature.startsWith(SIGNATURE_PREFIX)) {
            if (logger.isLoggable(FINE)) {
                logger.fine(format("Invalid signature: %s", signature));
            }
            return false;
        }
        String signatureHash = signature.substring(SIGNATURE_PREFIX.length());
        String expectedHash = generateHmac(payload);
        return expectedHash.equals(signatureHash);
    }

    private String generateHmac(String payload) {
        try {
            SecretKeySpec signingKey = new SecretKeySpec(appSecret.getBytes(), HMAC_SHA1_ALGORITHM);
            Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
            mac.init(signingKey);
            byte[] hmac = mac.doFinal(payload.getBytes());
            return new String(EncodingUtils.encodeHex(hmac));
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException(
                    format("%s algorithm not supported", HMAC_SHA1_ALGORITHM));
        } catch (InvalidKeyException e) {
            throw new IllegalStateException("Signing key is inappropriate");
        }
    }

    @Override
    public User getUserProfile(String userId) throws FacebookException {
        return getUserProfile(userId, USER_FIELDS_DEFAULT_VALUE);
    }

    @Override
    public User getUserProfile(String userId, String... fields) throws FacebookException {
        StringBuilder sb = new StringBuilder();
        String sep = "";
        for (String field : fields) {
            sb.append(sep).append(field);
            sep = ",";
        }
        return getUserProfile(userId, sb.toString());
    }

    @Override
    public User getUserProfile(String userId, String fields) throws FacebookException {
        requireNonNull(userId, "'userId' must not be null");
        return facebookClient.fetchObject(userId, User.class, Parameter.with(USER_FIELDS_PARAM_NAME, fields));
    }

    @Override
    public SendOperations send() {
        return sendOperations;
    }

    @Override
    public void setGreeting(String greeting) throws FacebookException {
        requireNonNull(greeting, "'greeting' must not be null");
        setGreeting(new Greeting(greeting));
    }

    @Override
    public void setGreeting(Greeting greeting) throws FacebookException {
        requireNonNull(greeting, "'greeting' must not be null");
        facebookClient.publish(THREAD_SETTINGS_PATH, SendResponse.class,
                Parameter.with(SETTING_TYPE_PARAM_NAME, SettingTypeEnum.greeting),
                Parameter.with(GREETING_PARAM_NAME, greeting));
    }

    @Override
    public void removeGreeting() throws FacebookException {
        facebookClient.deleteObject(THREAD_SETTINGS_PATH,
                Parameter.with(SETTING_TYPE_PARAM_NAME, SettingTypeEnum.greeting));
    }

    @Override
    public void setGetStartedButton(String payload) throws FacebookException {
        requireNonNull(payload, "'payload' must not be null");
        CallToAction callToAction = new CallToAction(payload);
        setGetStartedButton(callToAction);
    }

    @Override
    public void setGetStartedButton(CallToAction callToAction) throws FacebookException {
        requireNonNull(callToAction, "'callToAction' must not be null");
        List callToActions = Arrays.asList(callToAction);
        facebookClient.publish(THREAD_SETTINGS_PATH, SendResponse.class,
                Parameter.with(SETTING_TYPE_PARAM_NAME, SettingTypeEnum.call_to_actions),
                Parameter.with(THREAD_STATE_PARAM_NAME, ThreadStateEnum.new_thread),
                Parameter.with(CALL_TO_ACTIONS_PARAM_NAME, callToActions));
    }

    @Override
    public void removeGetStartedButton() throws FacebookException {
        facebookClient.deleteObject(THREAD_SETTINGS_PATH,
                Parameter.with(SETTING_TYPE_PARAM_NAME, SettingTypeEnum.call_to_actions),
                Parameter.with(THREAD_STATE_PARAM_NAME, ThreadStateEnum.new_thread));
    }

    @Override
    public void setPersistentMenu(List callToActions) throws FacebookException {
        requireNonNull(callToActions, "'callToActions' must not be null");
        facebookClient.publish(THREAD_SETTINGS_PATH, SendResponse.class,
                Parameter.with(SETTING_TYPE_PARAM_NAME, SettingTypeEnum.call_to_actions),
                Parameter.with(THREAD_STATE_PARAM_NAME, ThreadStateEnum.existing_thread),
                Parameter.with(CALL_TO_ACTIONS_PARAM_NAME, callToActions));
    }

    @Override
    public void removePersistentMenu() throws FacebookException {
        facebookClient.deleteObject(THREAD_SETTINGS_PATH,
                Parameter.with(SETTING_TYPE_PARAM_NAME, SettingTypeEnum.call_to_actions),
                Parameter.with(THREAD_STATE_PARAM_NAME, ThreadStateEnum.existing_thread));
    }

    @Override
    public void setAccountLinkingUrl(String url) throws FacebookException {
        requireNonNull(url, "'url' must not be null");
        facebookClient.publish(THREAD_SETTINGS_PATH, SendResponse.class,
                Parameter.with(SETTING_TYPE_PARAM_NAME, SettingTypeEnum.account_linking),
                Parameter.with(ACCOUNT_LINKING_URL_PARAM_NAME, url));
    }

    @Override
    public void removeAccountLinkingUrl() throws FacebookException {
        facebookClient.deleteObject(THREAD_SETTINGS_PATH,
                Parameter.with(SETTING_TYPE_PARAM_NAME, SettingTypeEnum.account_linking));
    }

    @Override
    public void addDomainWhitelisting(String url) throws FacebookException {
        requireNonNull(url, "'url' must not be null");
        addDomainWhitelisting(Arrays.asList(url));
    }

    @Override
    public void addDomainWhitelisting(List urls) throws FacebookException {
        requireNonNull(urls, "'urls' must not be null");
        facebookClient.publish(THREAD_SETTINGS_PATH, SendResponse.class,
                Parameter.with(SETTING_TYPE_PARAM_NAME, SettingTypeEnum.domain_whitelisting),
                Parameter.with(WHITELISTED_DOMAINS_PARAM_NAME, urls),
                Parameter.with(DOMAIN_ACTION_TYPE_PARAM_NAME, DomainActionTypeEnum.add));
    }

    @Override
    public void removeDomainWhitelisting(String url) throws FacebookException {
        requireNonNull(url, "'url' must not be null");
        removeDomainWhitelisting(Arrays.asList(url));
    }

    @Override
    public void removeDomainWhitelisting(List urls) throws FacebookException {
        requireNonNull(urls, "'urls' must not be null");
        facebookClient.publish(THREAD_SETTINGS_PATH, SendResponse.class,
                Parameter.with(SETTING_TYPE_PARAM_NAME, SettingTypeEnum.domain_whitelisting),
                Parameter.with(WHITELISTED_DOMAINS_PARAM_NAME, urls),
                Parameter.with(DOMAIN_ACTION_TYPE_PARAM_NAME, DomainActionTypeEnum.remove));
    }

    @Override
    public List getMessageTags() throws FacebookException {
        return facebookClient.fetchConnection(PAGE_MESSAGE_TAGS_PATH, PageMessageTag.class).getData();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy