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

com.slack.api.socket_mode.SocketModeClient Maven / Gradle / Ivy

There is a newer version: 1.39.0
Show newest version
package com.slack.api.socket_mode;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.slack.api.Slack;
import com.slack.api.socket_mode.listener.EnvelopeListener;
import com.slack.api.socket_mode.listener.WebSocketCloseListener;
import com.slack.api.socket_mode.listener.WebSocketErrorListener;
import com.slack.api.socket_mode.listener.WebSocketMessageListener;
import com.slack.api.socket_mode.queue.SocketModeMessageQueue;
import com.slack.api.socket_mode.request.EventsApiEnvelope;
import com.slack.api.socket_mode.request.InteractiveEnvelope;
import com.slack.api.socket_mode.request.SlashCommandsEnvelope;
import com.slack.api.socket_mode.response.SocketModeResponse;
import com.slack.api.util.json.GsonFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Closeable;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Socket Mode Client
 */
public interface SocketModeClient extends Closeable {

    /**
     * Built-in backend supports. The default os Tyrus.
     */
    enum Backend {
        /**
         * org.glassfish.tyrus.bundles:tyrus-standalone-client
         */
        Tyrus,
        /**
         * org.java-websocket:Java-WebSocket
         */
        JavaWebSocket
    }

    /**
     * Connects to the current WSS endpoint and starts a new WebSocket session.
     */
    void connect() throws IOException;

    /**
     * Returns true if this client is connected to the Socket Mode server.
     */
    boolean verifyConnection();

    /**
     * Connects to a new WSS endpoint and starts a new WebSocket session.
     */
    default void connectToNewEndpoint() throws IOException {
        try {
            setWssUri(new URI(getSlack().issueSocketModeUrl(getAppToken())));
            connect();
        } catch (URISyntaxException e) {
            throw new IOException(e);
        }
    }

    /**
     * Disconnects from the wss endpoint and abandons the current session.
     */
    void disconnect() throws IOException;

    /**
     * Closes this Socket Mode client.
     * After calling this method, the instance is no longer available to use.
     */
    @Override
    default void close() throws IOException {
        setAutoReconnectEnabled(false);
        disconnect();
        for (Runnable neverCommencedExecution : getMessageProcessorExecutor().shutdownNow()) {
            neverCommencedExecution.run();
        }
        if (getSessionMonitorExecutor().isPresent()) {
            List neverCommencedExecutions = getSessionMonitorExecutor().get().shutdownNow();
            if (neverCommencedExecutions != null && neverCommencedExecutions.size() > 0) {
                getLogger().info("This client is going to be terminated. {} executions in SessionStateMonitorExecutor did not begin.", neverCommencedExecutions.size());
            }
        }
    }

    // ---------------------------------
    // Configurable attributes
    // ---------------------------------

    Slack getSlack();

    void setSlack(Slack slack);

    String getAppToken();

    void setAppToken(String appToken);

    /**
     * Returns the current WSS URI.
     */
    URI getWssUri();

    void setWssUri(URI wssUri);

    /**
     * Tries to reconnect to the Socket Mode server if true.
     */
    boolean isAutoReconnectEnabled();

    void setAutoReconnectEnabled(boolean autoReconnectEnabled);

    /**
     * A background job for session maintenance works if true.
     */
    boolean isSessionMonitorEnabled();

    void setSessionMonitorEnabled(boolean sessionMonitorEnabled);

    Optional getSessionMonitorExecutor();

    void setSessionMonitorExecutor(Optional executorService);

    /**
     * Returns the message queue for message processor workers.
     */
    SocketModeMessageQueue getMessageQueue();

    void setMessageQueue(SocketModeMessageQueue messageQueue);

    ScheduledExecutorService getMessageProcessorExecutor();

    void setMessageProcessorExecutor(ScheduledExecutorService executorService);

    int DEFAULT_MESSAGE_PROCESSOR_CONCURRENCY = 10;

    default void initializeMessageProcessorExecutor(int concurrency) {
        String processorName = getExecutorGroupNamePrefix() + "-message-processor";
        ScheduledExecutorService messageProcessorExecutor = getSlack()
                .getConfig()
                .getExecutorServiceProvider()
                .createThreadScheduledExecutor(processorName);
        for (int i = 0; i < concurrency; i++) {
            int num = i;
            messageProcessorExecutor.scheduleAtFixedRate(() -> {
                try {
                    String message = getMessageQueue().poll();
                    if (message != null) {
                        processMessage(message);
                    }
                } catch (Exception e) {
                    getLogger().error("Failed to poll a message or run processMessage (error: {})", e.getMessage(), e);
                }
            }, 0, 10, TimeUnit.MILLISECONDS);
        }
        setMessageProcessorExecutor(messageProcessorExecutor);
    }

    long DEFAULT_SESSION_MONITOR_INTERVAL_MILLISECONDS = 5_000L;

    default void initializeSessionMonitorExecutor(long intervalMillis) {
        if (isSessionMonitorEnabled()) {
            String groupName = getExecutorGroupNamePrefix() + "-session-monitor";
            ScheduledExecutorService executorService = getSlack()
                    .getConfig()
                    .getExecutorServiceProvider()
                    .createThreadScheduledExecutor(groupName);
            final AtomicLong nextInvocationMillis = new AtomicLong(System.currentTimeMillis());
            executorService.scheduleWithFixedDelay(() -> {
                long startMillis = System.currentTimeMillis();
                if (getLogger().isDebugEnabled()) {
                    getLogger().debug("Checking the current session...");
                }
                if (isAutoReconnectEnabled() && nextInvocationMillis.get() <= System.currentTimeMillis()) {
                    nextInvocationMillis.set(maintainCurrentSession());
                }
                if (getLogger().isDebugEnabled()) {
                    long spentMillis = System.currentTimeMillis() - startMillis;
                    getLogger().debug("The session maintenance completed in {} milliseconds", spentMillis);
                }
            }, 5_000L, intervalMillis, TimeUnit.MILLISECONDS);
            if (getLogger().isDebugEnabled()) {
                getLogger().debug("SessionStateMonitorExecutor started.");
            }
            setSessionMonitorExecutor(Optional.of(executorService));
        } else {
            if (getLogger().isDebugEnabled()) {
                getLogger().debug("SessionStateMonitorExecutor is disabled.");
            }
            setSessionMonitorExecutor(Optional.empty());
        }
    }

    // ---------------------------------
    // Sending Messages
    // ---------------------------------

    default void sendSocketModeResponse(SocketModeResponse response) {
        sendSocketModeResponse(getGson().toJson(response));
    }

    default void sendSocketModeResponse(String message) {
        debugLogResponse(message);
        sendWebSocketMessage(message);
    }

    /**
     * Sends a text message to the Socket Mode server via the current WebSocket connection.
     */
    void sendWebSocketMessage(String message);

    // ---------------------------------
    // onError
    // ---------------------------------

    List getWebSocketErrorListeners();

    default void addWebSocketErrorListener(WebSocketErrorListener listener) {
        getWebSocketErrorListeners().add(listener);
    }

    default void removeWebSocketErrorListener(WebSocketErrorListener listener) {
        getWebSocketErrorListeners().remove(listener);
    }

    default void runErrorListeners(Throwable reason) {
        for (WebSocketErrorListener listener : getWebSocketErrorListeners()) {
            listener.handle(reason);
        }
    }

    // ---------------------------------
    // onClose
    // ---------------------------------

    List getWebSocketCloseListeners();

    default void addWebSocketCloseListener(WebSocketCloseListener listener) {
        getWebSocketCloseListeners().add(listener);
    }

    default void removeWebSocketCloseListener(WebSocketCloseListener listener) {
        getWebSocketCloseListeners().remove(listener);
    }

    default void runCloseListenersAndAutoReconnectAsNecessary(Integer code, String reason) {
        for (WebSocketCloseListener listener : getWebSocketCloseListeners()) {
            listener.handle(code, reason);
        }
        if (isAutoReconnectEnabled() && !verifyConnection()) {
            try {
                connectToNewEndpoint();
            } catch (IOException e) {
                getLogger().error("Failed to reconnect to the Socket Mode server: {}", e.getMessage(), e);
            }
        }
    }

    // ---------------------------------
    // onMessage
    // ---------------------------------

    default void enqueueMessage(String message) {
        debugLogRequest(message);
        if (message.startsWith("{")) {
            getMessageQueue().add(message);
        }
    }

    List getWebSocketMessageListeners();

    default void addWebSocketMessageListener(WebSocketMessageListener listener) {
        getWebSocketMessageListeners().add(listener);
    }

    default void removeWebSocketMessageListener(WebSocketMessageListener listener) {
        getWebSocketMessageListeners().remove(listener);
    }

    // ---------------------------------
    // Typed Envelope Listeners
    // ---------------------------------

    List> getEventsApiEnvelopeListeners();

    List> getInteractiveEnvelopeListeners();

    List> getSlashCommandsEnvelopeListeners();

    default void addEventsApiEnvelopeListener(EnvelopeListener listener) {
        getEventsApiEnvelopeListeners().add(listener);
    }

    default void removeEventsApiEnvelopeListener(EnvelopeListener listener) {
        getEventsApiEnvelopeListeners().remove(listener);
    }

    default void addInteractiveEnvelopeListener(EnvelopeListener listener) {
        getInteractiveEnvelopeListeners().add(listener);
    }

    default void removeInteractiveEnvelopeListener(EnvelopeListener listener) {
        getInteractiveEnvelopeListeners().remove(listener);
    }

    default void addSlashCommandsEnvelopeListener(EnvelopeListener listener) {
        getSlashCommandsEnvelopeListeners().add(listener);
    }

    default void removeSlashCommandsEnvelopeListener(EnvelopeListener listener) {
        getSlashCommandsEnvelopeListeners().remove(listener);
    }

    // ---------------------------------
    // Other Internals
    // ---------------------------------

    Logger LOGGER = LoggerFactory.getLogger(SocketModeClient.class);
    Gson GSON = GsonFactory.createSnakeCase();

    default Logger getLogger() {
        return LOGGER;
    }

    default Gson getGson() {
        return GSON;
    }

    default void processMessage(String message) throws IOException {
        if (!message.startsWith("{")) {
            return;
        }
        Gson gson = getGson();
        JsonElement messageObj = gson.fromJson(message, JsonElement.class);
        if (!messageObj.isJsonObject()) {
            return;
        }
        JsonObject messageJson = messageObj.getAsJsonObject();
        JsonElement typeJson = messageJson.get("type");
        if (typeJson == null || typeJson.isJsonNull()) {
            return;
        }
        String type = typeJson.getAsString();
        if ("disconnect".equals(type)) {
            connectToNewEndpoint();
            return;
        }

        for (WebSocketMessageListener listener : getWebSocketMessageListeners()) {
            listener.handle(message);
        }
        if (type.equals(EventsApiEnvelope.TYPE)) {
            for (EnvelopeListener listener : getEventsApiEnvelopeListeners()) {
                listener.handle(gson.fromJson(message, EventsApiEnvelope.class));
            }
        }
        if (type.equals(InteractiveEnvelope.TYPE)) {
            for (EnvelopeListener listener : getInteractiveEnvelopeListeners()) {
                listener.handle(gson.fromJson(message, InteractiveEnvelope.class));
            }
        }
        if (type.equals(SlashCommandsEnvelope.TYPE)) {
            for (EnvelopeListener listener : getSlashCommandsEnvelopeListeners()) {
                listener.handle(gson.fromJson(message, SlashCommandsEnvelope.class));
            }
        }
    }

    Gson PRETTY_PRINTING = new GsonBuilder().setPrettyPrinting().create();

    default void debugLogRequest(String message) {
        if (getLogger().isDebugEnabled() && message != null) {
            if (message.startsWith("{")) {
                JsonElement json = getGson().fromJson(message, JsonElement.class);
                getLogger().debug("Socket Mode Request:\n\n{}\n", PRETTY_PRINTING.toJson(json));
            } else {
                getLogger().debug("Socket Mode Request:\n\n{}\n", message);
            }
        }
    }

    default void debugLogResponse(String message) {
        if (getLogger().isDebugEnabled()) {
            if (message.startsWith("{")) {
                JsonElement json = getGson().fromJson(message, JsonElement.class);
                getLogger().debug("Socket Mode Response:\n\n{}\n", PRETTY_PRINTING.toJson(json));
            } else {
                getLogger().debug("Socket Mode Response:\n\n{}\n", message);
            }
        }
    }

    String EXECUTOR_GROUP_NAME_PREFIX = "socket-mode";

    default String getExecutorGroupNamePrefix() {
        return EXECUTOR_GROUP_NAME_PREFIX;
    }

    /**
     * Maintains the current session in a background job.
     * 

* see also: initializeSessionMonitorExecutor * * @return unix time to check next time */ long maintainCurrentSession(); }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy