Skip to content

Commit d51f7a8

Browse files
committed
Add app.function handlers
1 parent 32dd49b commit d51f7a8

4 files changed

Lines changed: 270 additions & 6 deletions

File tree

bolt-socket-mode/src/test/java/samples/SimpleApp.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.Arrays;
1515
import java.util.HashMap;
1616
import java.util.Map;
17+
import java.util.regex.Pattern;
1718

1819
import static com.slack.api.model.block.Blocks.*;
1920
import static com.slack.api.model.block.composition.BlockCompositions.dispatchActionConfig;
@@ -152,7 +153,9 @@ public static void main(String[] args) throws Exception {
152153
return ctx.ack();
153154
});
154155

155-
app.event(FunctionExecutedEvent.class, (req, ctx) -> {
156+
// app.event(FunctionExecutedEvent.class, (req, ctx) -> {
157+
// app.function("hello", (req, ctx) -> {
158+
app.function(Pattern.compile("^he.+$"), (req, ctx) -> {
156159
ctx.logger.info("req: {}", req);
157160
ctx.client().chatPostMessage(r -> r
158161
.channel(req.getEvent().getInputs().get("user_id").asString())

bolt/src/main/java/com/slack/api/bolt/App.java

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,7 @@
2828
import com.slack.api.methods.MethodsClient;
2929
import com.slack.api.methods.SlackApiException;
3030
import com.slack.api.methods.response.auth.AuthTestResponse;
31-
import com.slack.api.model.event.AppUninstalledEvent;
32-
import com.slack.api.model.event.Event;
33-
import com.slack.api.model.event.MessageEvent;
34-
import com.slack.api.model.event.TokensRevokedEvent;
31+
import com.slack.api.model.event.*;
3532
import com.slack.api.util.json.GsonFactory;
3633
import lombok.AllArgsConstructor;
3734
import lombok.Builder;
@@ -649,6 +646,33 @@ public App event(EventHandler<?> handler) {
649646
return this;
650647
}
651648

649+
public App function(String callbackId, BoltEventHandler<FunctionExecutedEvent> handler) {
650+
return event(FunctionExecutedEvent.class, (req, ctx) -> {
651+
if (log.isDebugEnabled()) {
652+
log.debug("Run a function_executed event handler (callback_id: {})", callbackId);
653+
}
654+
if (callbackId.equals(req.getEvent().getFunction().getCallbackId())) {
655+
return handler.apply(req, ctx);
656+
} else {
657+
return null;
658+
}
659+
});
660+
}
661+
662+
public App function(Pattern callbackId, BoltEventHandler<FunctionExecutedEvent> handler) {
663+
return event(FunctionExecutedEvent.class, (req, ctx) -> {
664+
if (log.isDebugEnabled()) {
665+
log.debug("Run a function_executed event handler (callback_id: {})", callbackId);
666+
}
667+
String sentCallbackId = req.getEvent().getFunction().getCallbackId();
668+
if (callbackId.matcher(sentCallbackId).matches()) {
669+
return handler.apply(req, ctx);
670+
} else {
671+
return null;
672+
}
673+
});
674+
}
675+
652676
public App message(String pattern, BoltEventHandler<MessageEvent> messageHandler) {
653677
return message(Pattern.compile("^.*" + Pattern.quote(pattern) + ".*$"), messageHandler);
654678
}
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
package test_locally.app;
2+
3+
import com.google.gson.Gson;
4+
import com.slack.api.Slack;
5+
import com.slack.api.SlackConfig;
6+
import com.slack.api.app_backend.SlackSignature;
7+
import com.slack.api.bolt.App;
8+
import com.slack.api.bolt.AppConfig;
9+
import com.slack.api.bolt.request.RequestHeaders;
10+
import com.slack.api.bolt.request.builtin.EventRequest;
11+
import com.slack.api.bolt.response.Response;
12+
import com.slack.api.model.event.FunctionExecutedEvent;
13+
import com.slack.api.util.json.GsonFactory;
14+
import lombok.extern.slf4j.Slf4j;
15+
import org.junit.After;
16+
import org.junit.Before;
17+
import org.junit.Test;
18+
import util.AuthTestMockServer;
19+
import util.MockSlackApiServer;
20+
21+
import java.util.Arrays;
22+
import java.util.HashMap;
23+
import java.util.List;
24+
import java.util.Map;
25+
import java.util.concurrent.atomic.AtomicBoolean;
26+
import java.util.regex.Pattern;
27+
28+
import static org.junit.Assert.assertEquals;
29+
import static org.junit.Assert.assertTrue;
30+
31+
@Slf4j
32+
public class RemoteFunctionTest {
33+
34+
MockSlackApiServer server = new MockSlackApiServer();
35+
SlackConfig config = new SlackConfig();
36+
Slack slack = Slack.getInstance(config);
37+
38+
@Before
39+
public void setup() throws Exception {
40+
server.start();
41+
config.setMethodsEndpointUrlPrefix(server.getMethodsEndpointPrefix());
42+
}
43+
44+
@After
45+
public void tearDown() throws Exception {
46+
server.stop();
47+
}
48+
49+
final Gson gson = GsonFactory.createSnakeCase();
50+
final String secret = "foo-bar-baz";
51+
final SlackSignature.Generator generator = new SlackSignature.Generator(secret);
52+
53+
String payload = "{\n" +
54+
" \"token\": \"xxx\",\n" +
55+
" \"team_id\": \"T03E94MJU\",\n" +
56+
" \"api_app_id\": \"A065ZJM410S\",\n" +
57+
" \"event\": {\n" +
58+
" \"type\": \"function_executed\",\n" +
59+
" \"function\": {\n" +
60+
" \"id\": \"Fn066C7U22JD\",\n" +
61+
" \"callback_id\": \"hello\",\n" +
62+
" \"title\": \"Hello\",\n" +
63+
" \"description\": \"Hello world!\",\n" +
64+
" \"type\": \"app\",\n" +
65+
" \"input_parameters\": [\n" +
66+
" {\n" +
67+
" \"type\": \"number\",\n" +
68+
" \"name\": \"amount\",\n" +
69+
" \"description\": \"How many do you need?\",\n" +
70+
" \"title\": \"Amount\",\n" +
71+
" \"is_required\": false,\n" +
72+
" \"hint\": \"How many do you need?\",\n" +
73+
" \"maximum\": 10,\n" +
74+
" \"minimum\": 1\n" +
75+
" },\n" +
76+
" {\n" +
77+
" \"type\": \"slack#/types/user_id\",\n" +
78+
" \"name\": \"user_id\",\n" +
79+
" \"description\": \"Who to send it\",\n" +
80+
" \"title\": \"User\",\n" +
81+
" \"is_required\": true,\n" +
82+
" \"hint\": \"Select a user in the workspace\"\n" +
83+
" },\n" +
84+
" {\n" +
85+
" \"type\": \"string\",\n" +
86+
" \"name\": \"message\",\n" +
87+
" \"description\": \"Whatever you want to tell\",\n" +
88+
" \"title\": \"Message\",\n" +
89+
" \"is_required\": false,\n" +
90+
" \"hint\": \"up to 100 characters\",\n" +
91+
" \"maxLength\": 100,\n" +
92+
" \"minLength\": 1\n" +
93+
" }\n" +
94+
" ],\n" +
95+
" \"output_parameters\": [\n" +
96+
" {\n" +
97+
" \"type\": \"number\",\n" +
98+
" \"name\": \"amount\",\n" +
99+
" \"description\": \"How many do you need?\",\n" +
100+
" \"title\": \"Amount\",\n" +
101+
" \"is_required\": false,\n" +
102+
" \"hint\": \"How many do you need?\",\n" +
103+
" \"maximum\": 10,\n" +
104+
" \"minimum\": 1\n" +
105+
" },\n" +
106+
" {\n" +
107+
" \"type\": \"slack#/types/user_id\",\n" +
108+
" \"name\": \"user_id\",\n" +
109+
" \"description\": \"Who to send it\",\n" +
110+
" \"title\": \"User\",\n" +
111+
" \"is_required\": true,\n" +
112+
" \"hint\": \"Select a user in the workspace\"\n" +
113+
" },\n" +
114+
" {\n" +
115+
" \"type\": \"string\",\n" +
116+
" \"name\": \"message\",\n" +
117+
" \"description\": \"Whatever you want to tell\",\n" +
118+
" \"title\": \"Message\",\n" +
119+
" \"is_required\": false,\n" +
120+
" \"hint\": \"up to 100 characters\",\n" +
121+
" \"maxLength\": 100,\n" +
122+
" \"minLength\": 1\n" +
123+
" }\n" +
124+
" ],\n" +
125+
" \"app_id\": \"A065ZJM410S\",\n" +
126+
" \"date_created\": 1700110468,\n" +
127+
" \"date_updated\": 1700110470,\n" +
128+
" \"date_deleted\": 0,\n" +
129+
" \"form_enabled\": false\n" +
130+
" },\n" +
131+
" \"inputs\": {\n" +
132+
" \"amount\": 1,\n" +
133+
" \"message\": \"hey\",\n" +
134+
" \"user_id\": \"U03E94MK0\"\n" +
135+
" },\n" +
136+
" \"function_execution_id\": \"Fx066G2XBP0E\",\n" +
137+
" \"workflow_execution_id\": \"Wx066862SLRM\",\n" +
138+
" \"event_ts\": \"1700554202.283041\",\n" +
139+
" \"bot_access_token\": \"xwfp-this-is-valid\"\n" +
140+
" },\n" +
141+
" \"type\": \"event_callback\",\n" +
142+
" \"event_id\": \"Ev067BMBHK16\",\n" +
143+
" \"event_time\": 1700554202\n" +
144+
"}\n";
145+
146+
@Test
147+
public void all_function_events() throws Exception {
148+
App app = buildApp();
149+
AtomicBoolean called = new AtomicBoolean(false);
150+
app.event(FunctionExecutedEvent.class, (req, ctx) -> {
151+
called.set(req.getEvent().getFunction().getCallbackId().equals("hello")
152+
&& req.getEvent().getInputs().get("user_id").asString().equals("U03E94MK0")
153+
&& req.getEvent().getInputs().get("amount").asInteger().equals(1)
154+
&& ctx.isAttachingFunctionTokenEnabled()
155+
&& ctx.getFunctionBotAccessToken().equals("xwfp-valid"));
156+
called.set(ctx.client().functionsCompleteSuccess(r -> r
157+
.functionExecutionId(req.getEvent().getFunctionExecutionId())
158+
.outputs(new HashMap<>())
159+
).getError().equals(""));
160+
called.set(ctx.client().functionsCompleteError(r -> r
161+
.functionExecutionId(req.getEvent().getFunctionExecutionId())
162+
.error("something wrong")
163+
).getError().equals(""));
164+
return ctx.ack();
165+
});
166+
167+
Response response = app.run(buildRequest());
168+
assertEquals(200L, response.getStatusCode().longValue());
169+
assertTrue(called.get());
170+
}
171+
172+
@Test
173+
public void static_callback_id() throws Exception {
174+
App app = buildApp();
175+
AtomicBoolean called = new AtomicBoolean(false);
176+
app.function("hello", (req, ctx) -> {
177+
called.set(req.getEvent().getFunction().getCallbackId().equals("hello")
178+
&& req.getEvent().getInputs().get("user_id").asString().equals("U03E94MK0")
179+
&& req.getEvent().getInputs().get("amount").asInteger().equals(1)
180+
&& ctx.isAttachingFunctionTokenEnabled()
181+
&& ctx.getFunctionBotAccessToken().equals("xwfp-valid"));
182+
called.set(ctx.client().functionsCompleteSuccess(r -> r
183+
.functionExecutionId(req.getEvent().getFunctionExecutionId())
184+
.outputs(new HashMap<>())
185+
).getError().equals(""));
186+
return ctx.ack();
187+
});
188+
189+
Response response = app.run(buildRequest());
190+
assertEquals(200L, response.getStatusCode().longValue());
191+
assertTrue(called.get());
192+
}
193+
194+
@Test
195+
public void regexp_callback_id() throws Exception {
196+
App app = buildApp();
197+
AtomicBoolean called = new AtomicBoolean(false);
198+
app.function(Pattern.compile("^he.+"), (req, ctx) -> {
199+
called.set(req.getEvent().getFunction().getCallbackId().equals("hello")
200+
&& req.getEvent().getInputs().get("user_id").asString().equals("U03E94MK0")
201+
&& req.getEvent().getInputs().get("amount").asInteger().equals(1)
202+
&& ctx.isAttachingFunctionTokenEnabled()
203+
&& ctx.getFunctionBotAccessToken().equals("xwfp-valid"));
204+
called.set(ctx.client().functionsCompleteSuccess(r -> r
205+
.functionExecutionId(req.getEvent().getFunctionExecutionId())
206+
.outputs(new HashMap<>())
207+
).getError().equals(""));
208+
return ctx.ack();
209+
});
210+
211+
Response response = app.run(buildRequest());
212+
assertEquals(200L, response.getStatusCode().longValue());
213+
assertTrue(called.get());
214+
}
215+
216+
App buildApp() {
217+
return new App(AppConfig.builder()
218+
.signingSecret(secret)
219+
.singleTeamBotToken(AuthTestMockServer.ValidToken)
220+
.slack(slack)
221+
.build());
222+
}
223+
224+
void setRequestHeaders(String requestBody, Map<String, List<String>> rawHeaders, String timestamp) {
225+
rawHeaders.put(SlackSignature.HeaderNames.X_SLACK_REQUEST_TIMESTAMP, Arrays.asList(timestamp));
226+
rawHeaders.put(SlackSignature.HeaderNames.X_SLACK_SIGNATURE, Arrays.asList(generator.generate(timestamp, requestBody)));
227+
}
228+
229+
EventRequest buildRequest() {
230+
Map<String, List<String>> rawHeaders = new HashMap<>();
231+
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
232+
setRequestHeaders(payload, rawHeaders, timestamp);
233+
return new EventRequest(payload, new RequestHeaders(rawHeaders));
234+
}
235+
}

bolt/src/test/java/util/MockSlackApi.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
public class MockSlackApi extends HttpServlet {
2121

2222
public static final String ValidToken = "xoxb-this-is-valid";
23+
public static final String ValidFunctionToken = "xwfp-this-is-valid";
2324
public static final String InvalidToken = "xoxb-this-is-INVALID";
2425

2526
private final FileReader reader = new FileReader("../json-logs/samples/api/");
@@ -41,7 +42,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
4142
resp.getWriter().write("{\"ok\":false,\"error\":\"not_authed\"}");
4243
resp.setContentType("application/json");
4344
return;
44-
} else if (!authorizationHeader.equals("Bearer " + ValidToken)) {
45+
} else if (!authorizationHeader.equals("Bearer " + ValidToken)
46+
&& !authorizationHeader.equals("Bearer " + ValidFunctionToken)) {
4547
resp.setStatus(200);
4648
resp.getWriter().write("{\"ok\":false,\"error\":\"invalid_auth\"}");
4749
resp.setContentType("application/json");

0 commit comments

Comments
 (0)