Skip to content

Commit 9cb6946

Browse files
Add Jakarta EE compatible Socket Mode client ref: #919 (#1352)
Co-authored-by: William Bergamin <[email protected]>
1 parent e542f29 commit 9cb6946

File tree

51 files changed

+4944
-24
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+4944
-24
lines changed

bolt-jakarta-socket-mode/pom.xml

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0"
2+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>com.slack.api</groupId>
8+
<artifactId>slack-sdk-parent</artifactId>
9+
<version>1.41.1-SNAPSHOT</version>
10+
</parent>
11+
12+
<properties>
13+
<tyrus-standalone-client.version>2.2.0</tyrus-standalone-client.version>
14+
<jakarta.websocket-api.version>2.2.0</jakarta.websocket-api.version>
15+
</properties>
16+
17+
<artifactId>bolt-jakarta-socket-mode</artifactId>
18+
<version>1.41.1-SNAPSHOT</version>
19+
<packaging>jar</packaging>
20+
21+
<dependencies>
22+
<dependency>
23+
<groupId>com.slack.api</groupId>
24+
<artifactId>slack-api-model</artifactId>
25+
<version>${project.version}</version>
26+
</dependency>
27+
<dependency>
28+
<groupId>com.slack.api</groupId>
29+
<artifactId>slack-api-client</artifactId>
30+
<version>${project.version}</version>
31+
<exclusions>
32+
<exclusion>
33+
<groupId>javax.websocket</groupId>
34+
<artifactId>javax.websocket-api</artifactId>
35+
</exclusion>
36+
<exclusion>
37+
<groupId>org.glassfish.tyrus.bundles</groupId>
38+
<artifactId>tyrus-standalone-client</artifactId>
39+
</exclusion>
40+
</exclusions>
41+
</dependency>
42+
<dependency>
43+
<groupId>com.slack.api</groupId>
44+
<artifactId>slack-jakarta-socket-mode-client</artifactId>
45+
<version>${project.version}</version>
46+
</dependency>
47+
<dependency>
48+
<groupId>com.slack.api</groupId>
49+
<artifactId>slack-app-backend</artifactId>
50+
<version>${project.version}</version>
51+
</dependency>
52+
<dependency>
53+
<groupId>com.slack.api</groupId>
54+
<artifactId>bolt</artifactId>
55+
<version>${project.version}</version>
56+
</dependency>
57+
58+
<dependency>
59+
<groupId>jakarta.websocket</groupId>
60+
<artifactId>jakarta.websocket-client-api</artifactId>
61+
<version>${jakarta.websocket-api.version}</version>
62+
<scope>provided</scope>
63+
</dependency>
64+
<dependency>
65+
<groupId>org.glassfish.tyrus.bundles</groupId>
66+
<artifactId>tyrus-standalone-client</artifactId>
67+
<version>${tyrus-standalone-client.version}</version>
68+
<scope>provided</scope>
69+
</dependency>
70+
71+
<dependency>
72+
<groupId>org.eclipse.jetty</groupId>
73+
<artifactId>jetty-servlet</artifactId>
74+
<version>${jetty-for-tests.version}</version>
75+
<scope>test</scope>
76+
</dependency>
77+
<dependency>
78+
<groupId>org.eclipse.jetty</groupId>
79+
<artifactId>jetty-server</artifactId>
80+
<version>${jetty-for-tests.version}</version>
81+
<scope>test</scope>
82+
</dependency>
83+
<dependency>
84+
<groupId>org.eclipse.jetty</groupId>
85+
<artifactId>jetty-webapp</artifactId>
86+
<version>${jetty-for-tests.version}</version>
87+
<scope>test</scope>
88+
</dependency>
89+
<dependency>
90+
<groupId>org.eclipse.jetty.websocket</groupId>
91+
<artifactId>websocket-server</artifactId>
92+
<version>${jetty-for-tests.version}</version>
93+
<scope>test</scope>
94+
</dependency>
95+
</dependencies>
96+
97+
</project>
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
package com.slack.api.bolt.jakarta_socket_mode;
2+
3+
import com.google.gson.Gson;
4+
import com.google.gson.JsonElement;
5+
import com.slack.api.bolt.App;
6+
import com.slack.api.bolt.request.Request;
7+
import com.slack.api.bolt.response.Response;
8+
import com.slack.api.bolt.jakarta_socket_mode.request.SocketModeRequest;
9+
import com.slack.api.bolt.jakarta_socket_mode.request.SocketModeRequestParser;
10+
import com.slack.api.jakarta_socket_mode.JakartaSocketModeClientFactory;
11+
import com.slack.api.socket_mode.SocketModeClient;
12+
import com.slack.api.socket_mode.response.AckResponse;
13+
import com.slack.api.util.json.GsonFactory;
14+
import lombok.Builder;
15+
import lombok.Data;
16+
import lombok.extern.slf4j.Slf4j;
17+
18+
import java.io.IOException;
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
import java.util.function.Function;
22+
import java.util.function.Supplier;
23+
24+
@Slf4j
25+
public class SocketModeApp {
26+
private boolean clientStopped = true;
27+
private final App app;
28+
private final Supplier<SocketModeClient> clientFactory;
29+
private SocketModeClient client;
30+
31+
private static final Function<ErrorContext, Response> DEFAULT_ERROR_HANDLER = (context) -> {
32+
Exception e = context.getException();
33+
log.error("Failed to handle a request: {}", e.getMessage(), e);
34+
return null;
35+
};
36+
37+
@Data
38+
@Builder
39+
public static class ErrorContext {
40+
private Request<?> request;
41+
private Exception exception;
42+
}
43+
44+
// -------------------------------------------
45+
46+
private static void sendSocketModeResponse(
47+
SocketModeClient client,
48+
Gson gson,
49+
SocketModeRequest req,
50+
Response boltResponse
51+
) {
52+
if (boltResponse.getBody() != null) {
53+
Map<String, Object> response = new HashMap<>();
54+
if (boltResponse.getContentType().startsWith("application/json")) {
55+
response.put("envelope_id", req.getEnvelope().getEnvelopeId());
56+
response.put("payload", gson.fromJson(boltResponse.getBody(), JsonElement.class));
57+
} else {
58+
response.put("envelope_id", req.getEnvelope().getEnvelopeId());
59+
Map<String, Object> payload = new HashMap<>();
60+
payload.put("text", boltResponse.getBody());
61+
response.put("payload", payload);
62+
}
63+
client.sendSocketModeResponse(gson.toJson(response));
64+
} else {
65+
client.sendSocketModeResponse(new AckResponse(req.getEnvelope().getEnvelopeId()));
66+
}
67+
}
68+
69+
private static Supplier<SocketModeClient> buildSocketModeClientFactory(
70+
App app,
71+
String appToken,
72+
Function<ErrorContext, Response> errorHandler
73+
) {
74+
return () -> {
75+
try {
76+
final SocketModeClient client = JakartaSocketModeClientFactory.create(app.slack(), appToken);
77+
final SocketModeRequestParser requestParser = new SocketModeRequestParser(app.config());
78+
final Gson gson = GsonFactory.createSnakeCase(app.slack().getConfig());
79+
client.addWebSocketMessageListener(message -> {
80+
long startMillis = System.currentTimeMillis();
81+
SocketModeRequest req = requestParser.parse(message);
82+
if (req != null) {
83+
try {
84+
Response boltResponse = app.run(req.getBoltRequest());
85+
if (boltResponse.getStatusCode() != 200) {
86+
log.warn("Unsuccessful Bolt app execution (status: {}, body: {})",
87+
boltResponse.getStatusCode(), boltResponse.getBody());
88+
return;
89+
}
90+
sendSocketModeResponse(client, gson, req, boltResponse);
91+
} catch (Exception e) {
92+
ErrorContext context = ErrorContext.builder().request(req.getBoltRequest()).exception(e).build();
93+
Response errorResponse = errorHandler.apply(context);
94+
if (errorResponse != null) {
95+
sendSocketModeResponse(client, gson, req, errorResponse);
96+
}
97+
} finally {
98+
long spentMillis = System.currentTimeMillis() - startMillis;
99+
log.debug("Response time: {} milliseconds", spentMillis);
100+
}
101+
}
102+
});
103+
return client;
104+
} catch (IOException e) {
105+
log.error("Failed to start a new Socket Mode client (error: {})", e.getMessage(), e);
106+
return null;
107+
}
108+
};
109+
}
110+
111+
public SocketModeApp(App app) throws IOException {
112+
this(System.getenv("SLACK_APP_TOKEN"), app);
113+
}
114+
115+
116+
public SocketModeApp(String appToken, App app) throws IOException {
117+
this(appToken, DEFAULT_ERROR_HANDLER, app);
118+
}
119+
120+
public SocketModeApp(
121+
String appToken,
122+
Function<ErrorContext, Response> errorHandler,
123+
App app
124+
) throws IOException {
125+
this(buildSocketModeClientFactory(app, appToken, errorHandler), app);
126+
}
127+
128+
public SocketModeApp(
129+
String appToken,
130+
App app,
131+
Function<ErrorContext, Response> errorHandler
132+
) throws IOException {
133+
this(buildSocketModeClientFactory(app, appToken, errorHandler), app);
134+
}
135+
136+
public SocketModeApp(Supplier<SocketModeClient> clientFactory, App app) {
137+
this.clientFactory = clientFactory;
138+
this.app = app;
139+
}
140+
141+
/**
142+
* If you would like to synchronously detect the connection error as an exception when bootstrapping,
143+
* use this constructor. The first line can throw an exception
144+
* in the case where either the token or network settings are valid.
145+
*
146+
* <code>
147+
* SocketModeClient client = JakartaSocketModeClientFactory.create(appToken);
148+
* SocketModeApp socketModeApp = new SocketModeApp(client, app);
149+
* </code>
150+
*/
151+
public SocketModeApp(SocketModeClient socketModeClient, App app) {
152+
this.client = socketModeClient;
153+
this.clientFactory = () -> socketModeClient;
154+
this.app = app;
155+
}
156+
157+
// -------------------------------------------
158+
159+
public void start() throws Exception {
160+
run(true);
161+
}
162+
163+
public void startAsync() throws Exception {
164+
run(false);
165+
}
166+
167+
public void run(boolean blockCurrentThread) throws Exception {
168+
this.app.start();
169+
if (this.client == null) {
170+
this.client = clientFactory.get();
171+
}
172+
if (this.isClientStopped()) {
173+
this.client.connectToNewEndpoint();
174+
} else {
175+
this.client.connect();
176+
}
177+
this.client.setAutoReconnectEnabled(true);
178+
this.clientStopped = false;
179+
if (blockCurrentThread) {
180+
Thread.sleep(Long.MAX_VALUE);
181+
}
182+
}
183+
184+
public void stop() throws Exception {
185+
if (this.client != null && this.client.verifyConnection()) {
186+
this.client.disconnect();
187+
}
188+
this.clientStopped = true;
189+
this.app.stop();
190+
}
191+
192+
public void close() throws Exception {
193+
this.stop();
194+
this.client = null;
195+
}
196+
197+
// -------------------------------------------
198+
// Accessors
199+
// -------------------------------------------
200+
201+
public boolean isClientStopped() {
202+
return clientStopped;
203+
}
204+
205+
public SocketModeClient getClient() {
206+
return client;
207+
}
208+
209+
public App getApp() {
210+
return app;
211+
}
212+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* Built-in Socket Mode adapter supports.
3+
*/
4+
package com.slack.api.bolt.jakarta_socket_mode;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.slack.api.bolt.jakarta_socket_mode.request;
2+
3+
import com.slack.api.bolt.request.Request;
4+
import com.slack.api.socket_mode.request.SocketModeEnvelope;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Data;
8+
import lombok.NoArgsConstructor;
9+
10+
@Data
11+
@AllArgsConstructor
12+
@NoArgsConstructor
13+
@Builder
14+
public class SocketModeRequest {
15+
private SocketModeEnvelope envelope;
16+
private Request<?> boltRequest;
17+
}

0 commit comments

Comments
 (0)