diff --git a/bolt-socket-mode/src/main/java/com/slack/api/bolt/socket_mode/SocketModeApp.java b/bolt-socket-mode/src/main/java/com/slack/api/bolt/socket_mode/SocketModeApp.java index 2bf837be5..5f89e994b 100644 --- a/bolt-socket-mode/src/main/java/com/slack/api/bolt/socket_mode/SocketModeApp.java +++ b/bolt-socket-mode/src/main/java/com/slack/api/bolt/socket_mode/SocketModeApp.java @@ -14,57 +14,68 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.function.Supplier; @Slf4j public class SocketModeApp { - private boolean clientStopped = false; - private final SocketModeClient client; + private boolean clientStopped = true; private final App app; + private final Supplier clientFactory; + private SocketModeClient client; - private static SocketModeClient buildSocketModeClient( + // ------------------------------------------- + + private static Supplier buildSocketModeClientFactory( App app, String appToken, SocketModeClient.Backend backend - ) throws IOException { - final SocketModeClient client = app.slack().socketMode(appToken, backend); - final SocketModeRequestParser requestParser = new SocketModeRequestParser(app.config()); - final Gson gson = GsonFactory.createSnakeCase(app.slack().getConfig()); - client.addWebSocketMessageListener(message -> { - long startMillis = System.currentTimeMillis(); - SocketModeRequest req = requestParser.parse(message); - if (req != null) { - try { - Response boltResponse = app.run(req.getBoltRequest()); - if (boltResponse.getStatusCode() != 200) { - log.warn("Unsuccessful Bolt app execution (status: {}, body: {})", - boltResponse.getStatusCode(), boltResponse.getBody()); - return; - } - if (boltResponse.getBody() != null) { - Map response = new HashMap<>(); - if (boltResponse.getContentType().startsWith("application/json")) { - response.put("envelope_id", req.getEnvelope().getEnvelopeId()); - response.put("payload", gson.fromJson(boltResponse.getBody(), JsonElement.class)); - } else { - response.put("envelope_id", req.getEnvelope().getEnvelopeId()); - Map payload = new HashMap<>(); - payload.put("text", boltResponse.getBody()); - response.put("payload", payload); + ) { + return () -> { + try { + final SocketModeClient client = app.slack().socketMode(appToken, backend); + final SocketModeRequestParser requestParser = new SocketModeRequestParser(app.config()); + final Gson gson = GsonFactory.createSnakeCase(app.slack().getConfig()); + client.addWebSocketMessageListener(message -> { + long startMillis = System.currentTimeMillis(); + SocketModeRequest req = requestParser.parse(message); + if (req != null) { + try { + Response boltResponse = app.run(req.getBoltRequest()); + if (boltResponse.getStatusCode() != 200) { + log.warn("Unsuccessful Bolt app execution (status: {}, body: {})", + boltResponse.getStatusCode(), boltResponse.getBody()); + return; + } + if (boltResponse.getBody() != null) { + Map response = new HashMap<>(); + if (boltResponse.getContentType().startsWith("application/json")) { + response.put("envelope_id", req.getEnvelope().getEnvelopeId()); + response.put("payload", gson.fromJson(boltResponse.getBody(), JsonElement.class)); + } else { + response.put("envelope_id", req.getEnvelope().getEnvelopeId()); + Map payload = new HashMap<>(); + payload.put("text", boltResponse.getBody()); + response.put("payload", payload); + } + client.sendSocketModeResponse(gson.toJson(response)); + } else { + client.sendSocketModeResponse(new AckResponse(req.getEnvelope().getEnvelopeId())); + } + long spentMillis = System.currentTimeMillis() - startMillis; + log.debug("Response time: {} milliseconds", spentMillis); + return; + } catch (Exception e) { + log.error("Failed to handle a request: {}", e.getMessage(), e); + return; } - client.sendSocketModeResponse(gson.toJson(response)); - } else { - client.sendSocketModeResponse(new AckResponse(req.getEnvelope().getEnvelopeId())); } - long spentMillis = System.currentTimeMillis() - startMillis; - log.debug("Response time: {} milliseconds", spentMillis); - return; - } catch (Exception e) { - log.error("Failed to handle a request: {}", e.getMessage(), e); - return; - } + }); + return client; + } catch (IOException e) { + log.error("Failed to start a new Socket Mode client (error: {})", e.getMessage(), e); + return null; } - }); - return client; + }; } public SocketModeApp(App app) throws IOException { @@ -80,14 +91,32 @@ public SocketModeApp( SocketModeClient.Backend backend, App app ) throws IOException { - this(buildSocketModeClient(app, appToken, backend), app); + this(buildSocketModeClientFactory(app, appToken, backend), app); + } + + public SocketModeApp(Supplier clientFactory, App app) { + this.clientFactory = clientFactory; + this.app = app; } + /** + * If you would like to synchronously detect the connection error as an exception when bootstrapping, + * use this constructor. The first line can throw an exception + * in the case where either the token or network settings are valid. + * + * + * SocketModeClient client = Slack.getInstance().socketMode(appToken); + * SocketModeApp socketModeApp = new SocketModeApp(client, app); + * + */ public SocketModeApp(SocketModeClient socketModeClient, App app) { this.client = socketModeClient; + this.clientFactory = () -> socketModeClient; this.app = app; } + // ------------------------------------------- + public void start() throws Exception { run(true); } @@ -97,21 +126,48 @@ public void startAsync() throws Exception { } public void run(boolean blockCurrentThread) throws Exception { - app.start(); - if (clientStopped) { - client.connectToNewEndpoint(); + this.app.start(); + if (this.client == null) { + this.client = clientFactory.get(); + } + if (this.isClientStopped()) { + this.client.connectToNewEndpoint(); } else { - client.connect(); + this.client.connect(); } - client.setAutoReconnectEnabled(true); + this.client.setAutoReconnectEnabled(true); + this.clientStopped = false; if (blockCurrentThread) { Thread.sleep(Long.MAX_VALUE); } } public void stop() throws Exception { - client.disconnect(); - clientStopped = true; - app.stop(); + if (this.client != null && this.client.verifyConnection()) { + this.client.disconnect(); + } + this.clientStopped = true; + this.app.stop(); + } + + public void close() throws Exception { + this.stop(); + this.client = null; + } + + // ------------------------------------------- + // Accessors + // ------------------------------------------- + + public boolean isClientStopped() { + return clientStopped; + } + + public SocketModeClient getClient() { + return client; + } + + public App getApp() { + return app; } } diff --git a/bolt-socket-mode/src/test/java/test_locally/SocketModeAppTest.java b/bolt-socket-mode/src/test/java/test_locally/SocketModeAppTest.java index 3f0c327da..d763de1c5 100644 --- a/bolt-socket-mode/src/test/java/test_locally/SocketModeAppTest.java +++ b/bolt-socket-mode/src/test/java/test_locally/SocketModeAppTest.java @@ -16,7 +16,7 @@ import java.util.concurrent.atomic.AtomicBoolean; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; public class SocketModeAppTest { @@ -78,6 +78,26 @@ public void payloadHandling() throws Exception { } } + @Test + public void issue_937_it_should_not_be_connected_before_start_method_call() throws Exception { + App app = new App(AppConfig.builder() + .slack(slack) + .singleTeamBotToken(VALID_BOT_TOKEN) + .build()); + SocketModeApp socketModeApp = new SocketModeApp(VALID_APP_TOKEN, app); + try { + assertTrue(socketModeApp.isClientStopped()); + assertNull(socketModeApp.getClient()); + + // Check again to make sure if the auto-connection does not work in this scenario + Thread.sleep(3_000L); + assertTrue(socketModeApp.isClientStopped()); + assertNull(socketModeApp.getClient()); + } finally { + socketModeApp.close(); + } + } + // ------------------------------------------------- // JavaWebSocket implementation // -------------------------------------------------