Skip to content

Commit 49304c3

Browse files
committed
Add app.function listener support
1 parent cf0fbd7 commit 49304c3

9 files changed

Lines changed: 129 additions & 35 deletions

File tree

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

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.Arrays;
1313
import java.util.HashMap;
1414
import java.util.Map;
15+
import java.util.regex.Pattern;
1516

1617
import static com.slack.api.model.block.Blocks.*;
1718
import static com.slack.api.model.block.composition.BlockCompositions.dispatchActionConfig;
@@ -151,14 +152,11 @@ public static void main(String[] args) throws Exception {
151152
});
152153

153154
// Note that this is still in beta as of Nov 2023
154-
app.event(FunctionExecutedEvent.class, (req, ctx) -> {
155-
// TODO: future updates enable passing callback_id as below
155+
// app.event(FunctionExecutedEvent.class, (req, ctx) -> {
156156
// app.function("hello", (req, ctx) -> {
157-
// app.function(Pattern.compile("^he.+$"), (req, ctx) -> {
157+
app.function(Pattern.compile("^he.+$"), (req, ctx) -> {
158158
ctx.logger.info("req: {}", req);
159159
ctx.client().chatPostMessage(r -> r
160-
// TODO: remove this token passing by enhancing bolt internals
161-
.token(req.getEvent().getBotAccessToken())
162160
.channel(req.getEvent().getInputs().get("user_id").asString())
163161
.text("hey!")
164162
.blocks(asBlocks(actions(a -> a.blockId("b").elements(asElements(
@@ -174,14 +172,10 @@ public static void main(String[] args) throws Exception {
174172
Map<String, Object> outputs = new HashMap<>();
175173
outputs.put("user_id", req.getPayload().getFunctionData().getInputs().get("user_id").asString());
176174
ctx.client().functionsCompleteSuccess(r -> r
177-
// TODO: remove this token passing by enhancing bolt internals
178-
.token(req.getPayload().getBotAccessToken())
179175
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
180176
.outputs(outputs)
181177
);
182178
ctx.client().chatUpdate(r -> r
183-
// TODO: remove this token passing by enhancing bolt internals
184-
.token(req.getPayload().getBotAccessToken())
185179
.channel(req.getPayload().getContainer().getChannelId())
186180
.ts(req.getPayload().getContainer().getMessageTs())
187181
.text("Thank you!")
@@ -190,14 +184,10 @@ public static void main(String[] args) throws Exception {
190184
});
191185
app.blockAction("remote-function-button-error", (req, ctx) -> {
192186
ctx.client().functionsCompleteError(r -> r
193-
// TODO: remove this token passing by enhancing bolt internals
194-
.token(req.getPayload().getBotAccessToken())
195187
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
196188
.error("test error!")
197189
);
198190
ctx.client().chatUpdate(r -> r
199-
// TODO: remove this token passing by enhancing bolt internals
200-
.token(req.getPayload().getBotAccessToken())
201191
.channel(req.getPayload().getContainer().getChannelId())
202192
.ts(req.getPayload().getContainer().getMessageTs())
203193
.text("Thank you!")
@@ -206,8 +196,6 @@ public static void main(String[] args) throws Exception {
206196
});
207197
app.blockAction("remote-function-modal", (req, ctx) -> {
208198
ctx.client().viewsOpen(r -> r
209-
// TODO: remove this token passing by enhancing bolt internals
210-
.token(req.getPayload().getBotAccessToken())
211199
.triggerId(req.getPayload().getInteractivity().getInteractivityPointer())
212200
.view(view(v -> v
213201
.type("modal")
@@ -223,8 +211,6 @@ public static void main(String[] args) throws Exception {
223211
)))
224212
)));
225213
ctx.client().chatUpdate(r -> r
226-
// TODO: remove this token passing by enhancing bolt internals
227-
.token(req.getPayload().getBotAccessToken())
228214
.channel(req.getPayload().getContainer().getChannelId())
229215
.ts(req.getPayload().getContainer().getMessageTs())
230216
.text("Thank you!")
@@ -236,7 +222,6 @@ public static void main(String[] args) throws Exception {
236222
Map<String, Object> outputs = new HashMap<>();
237223
outputs.put("user_id", ctx.getRequestUserId());
238224
ctx.client().functionsCompleteSuccess(r -> r
239-
// TODO: remove this token passing by enhancing bolt internals
240225
.token(req.getPayload().getBotAccessToken())
241226
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
242227
.outputs(outputs)
@@ -247,7 +232,6 @@ public static void main(String[] args) throws Exception {
247232
Map<String, Object> outputs = new HashMap<>();
248233
outputs.put("user_id", ctx.getRequestUserId());
249234
ctx.client().functionsCompleteSuccess(r -> r
250-
// TODO: remove this token passing by enhancing bolt internals
251235
.token(req.getPayload().getBotAccessToken())
252236
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
253237
.outputs(outputs)

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

Lines changed: 29 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;
@@ -582,6 +579,7 @@ public Response run(Request request) throws Exception {
582579
if (request == null || request.getContext() == null) {
583580
return Response.builder().statusCode(400).body("Invalid Request").build();
584581
}
582+
request.getContext().setAttachingFunctionTokenEnabled(this.config().isAttachingFunctionTokenEnabled());
585583
request.getContext().setSlack(slack()); // use the properly configured API client
586584

587585
if (neverStarted.get()) {
@@ -648,6 +646,33 @@ public App event(EventHandler<?> handler) {
648646
return this;
649647
}
650648

649+
public App function(String callbackId, BoltEventHandler<FunctionExecutedEvent> handler) {
650+
return event(FunctionExecutedEvent.class, true, (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, true, (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+
651676
public App message(String pattern, BoltEventHandler<MessageEvent> messageHandler) {
652677
return message(Pattern.compile("^.*" + Pattern.quote(pattern) + ".*$"), messageHandler);
653678
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,15 @@ public void setOauthRedirectUriPath(String oauthRedirectUriPath) {
380380
@Builder.Default
381381
private boolean allEventsApiAutoAckEnabled = false;
382382

383+
/**
384+
* When true, the framework automatically attaches context#functionBotAccessToken
385+
* to context#client instead of context#botToken.
386+
* Enabling this behavior only affects function_executed event handlers
387+
* and app.action/app.view handlers associated with the function token.
388+
*/
389+
@Builder.Default
390+
private boolean attachingFunctionTokenEnabled = true;
391+
383392
// ---------------------------------
384393
// Default middleware configuration
385394
// ---------------------------------

bolt/src/main/java/com/slack/api/bolt/context/Context.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,21 @@ public abstract class Context {
5454
* A bot token associated with this request. The format must be starting with `xoxb-`.
5555
*/
5656
protected String botToken;
57+
58+
/**
59+
* When true, the framework automatically attaches context#functionBotAccessToken
60+
* to context#client instead of context#botToken.
61+
* Enabling this behavior only affects function_executed event handlers
62+
* and app.action/app.view handlers associated with the function token.
63+
*/
64+
private boolean attachingFunctionTokenEnabled;
65+
66+
/**
67+
* The bot token associated with this "function_executed"-type event and its interactions.
68+
* The format must be starting with `xoxb-`.
69+
*/
70+
protected String functionBotAccessToken;
71+
5772
/**
5873
* The scopes associated to the botToken
5974
*/
@@ -88,17 +103,21 @@ public abstract class Context {
88103
protected final Map<String, String> additionalValues = new HashMap<>();
89104

90105
public MethodsClient client() {
106+
String primaryToken = (isAttachingFunctionTokenEnabled() && functionBotAccessToken != null)
107+
? functionBotAccessToken : botToken;
91108
// We used to pass teamId only for org-wide installations, but we changed this behavior since version 1.10.
92109
// The reasons are 1) having teamId in the MethodsClient can reduce TeamIdCache's auth.test API calls
93110
// 2) OpenID Connect + token rotation allows only refresh token to perform auth.test API calls.
94-
return getSlack().methods(botToken, teamId);
111+
return getSlack().methods(primaryToken, teamId);
95112
}
96113

97114
public AsyncMethodsClient asyncClient() {
115+
String primaryToken = (isAttachingFunctionTokenEnabled() && functionBotAccessToken != null)
116+
? functionBotAccessToken : botToken;
98117
// We used to pass teamId only for org-wide installations, but we changed this behavior since version 1.10.
99118
// The reasons are 1) having teamId in the MethodsClient can reduce TeamIdCache's auth.test API calls
100119
// 2) OpenID Connect + token rotation allows only refresh token to perform auth.test API calls.
101-
return getSlack().methodsAsync(botToken, teamId);
120+
return getSlack().methodsAsync(primaryToken, teamId);
102121
}
103122

104123
public ChatPostMessageResponse say(BuilderConfigurator<ChatPostMessageRequest.ChatPostMessageRequestBuilder> request) throws IOException, SlackApiException {

bolt/src/main/java/com/slack/api/bolt/request/builtin/BlockActionRequest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,14 @@ public BlockActionRequest(
2323
this.headers = headers;
2424
this.payload = GsonFactory.createSnakeCase().fromJson(payloadBody, BlockActionPayload.class);
2525
if (this.payload != null) {
26+
getContext().setFunctionBotAccessToken(payload.getBotAccessToken());
2627
getContext().setResponseUrl(payload.getResponseUrl());
2728
getContext().setTriggerId(payload.getTriggerId());
29+
if (payload.getTriggerId() == null
30+
&& payload.getInteractivity() != null
31+
&& payload.getInteractivity().getInteractivityPointer() != null) {
32+
getContext().setTriggerId(payload.getInteractivity().getInteractivityPointer());
33+
}
2834
if (payload.getEnterprise() != null) {
2935
getContext().setEnterpriseId(payload.getEnterprise().getId());
3036
} else if (payload.getTeam() != null) {

bolt/src/main/java/com/slack/api/bolt/request/builtin/EventRequest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.slack.api.bolt.request.Request;
88
import com.slack.api.bolt.request.RequestHeaders;
99
import com.slack.api.bolt.request.RequestType;
10+
import com.slack.api.model.event.FunctionExecutedEvent;
1011
import com.slack.api.util.json.GsonFactory;
1112
import lombok.ToString;
1213

@@ -104,6 +105,13 @@ public EventRequest(
104105
} else if (event.get("channel_id") != null) {
105106
this.getContext().setChannelId(event.get("channel_id").getAsString());
106107
}
108+
109+
if (this.eventType != null
110+
&& this.eventType.equals(FunctionExecutedEvent.TYPE_NAME)
111+
&& event.get("bot_access_token") != null) {
112+
String functionBotAccessToken = event.get("bot_access_token").getAsString();
113+
this.getContext().setFunctionBotAccessToken(functionBotAccessToken);
114+
}
107115
}
108116

109117
private EventContext context = new EventContext();

bolt/src/main/java/com/slack/api/bolt/request/builtin/ViewClosedRequest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public ViewClosedRequest(
4242
getContext().setTeamId(payload.getUser().getTeamId());
4343
}
4444
getContext().setRequestUserId(payload.getUser().getId());
45+
getContext().setFunctionBotAccessToken(payload.getBotAccessToken());
4546
}
4647

4748
private DefaultContext context = new DefaultContext();

bolt/src/main/java/com/slack/api/bolt/request/builtin/ViewSubmissionRequest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public ViewSubmissionRequest(
4343
}
4444
getContext().setRequestUserId(payload.getUser().getId());
4545
getContext().setResponseUrls(payload.getResponseUrls());
46+
getContext().setFunctionBotAccessToken(payload.getBotAccessToken());
4647
}
4748

4849
private ViewSubmissionContext context = new ViewSubmissionContext();

bolt/src/test/java/test_locally/app/RemoteFunctionTest.java

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.slack.api.bolt.request.builtin.BlockActionRequest;
1111
import com.slack.api.bolt.request.builtin.EventRequest;
1212
import com.slack.api.bolt.request.builtin.ViewSubmissionRequest;
13+
import com.slack.api.bolt.request.builtin.EventRequest;
1314
import com.slack.api.bolt.response.Response;
1415
import com.slack.api.model.event.FunctionExecutedEvent;
1516
import com.slack.api.util.json.GsonFactory;
@@ -26,6 +27,7 @@
2627
import java.util.List;
2728
import java.util.Map;
2829
import java.util.concurrent.atomic.AtomicBoolean;
30+
import java.util.regex.Pattern;
2931

3032
import static org.junit.Assert.assertEquals;
3133
import static org.junit.Assert.assertTrue;
@@ -356,17 +358,13 @@ public void all_function_events() throws Exception {
356358
called.set(req.getEvent().getFunction().getCallbackId().equals("hello")
357359
&& req.getEvent().getInputs().get("user_id").asString().equals("U03E94MK0")
358360
&& req.getEvent().getInputs().get("amount").asInteger().equals(1)
359-
&& req.getEvent().getBotAccessToken().equals("xwfp-this-is-valid")
360-
);
361+
&& ctx.isAttachingFunctionTokenEnabled()
362+
&& ctx.getFunctionBotAccessToken().equals("xwfp-valid"));
361363
called.set(ctx.client().functionsCompleteSuccess(r -> r
362-
// TODO: remove this token passing by enhancing bolt internals
363-
.token(req.getEvent().getBotAccessToken())
364364
.functionExecutionId(req.getEvent().getFunctionExecutionId())
365365
.outputs(new HashMap<>())
366366
).getError().equals(""));
367367
called.set(ctx.client().functionsCompleteError(r -> r
368-
// TODO: remove this token passing by enhancing bolt internals
369-
.token(req.getEvent().getBotAccessToken())
370368
.functionExecutionId(req.getEvent().getFunctionExecutionId())
371369
.error("something wrong")
372370
).getError().equals(""));
@@ -378,6 +376,52 @@ public void all_function_events() throws Exception {
378376
assertTrue(called.get());
379377
}
380378

379+
@Test
380+
public void static_callback_id() throws Exception {
381+
App app = buildApp();
382+
AtomicBoolean called = new AtomicBoolean(false);
383+
app.function("hello", (req, ctx) -> {
384+
called.set(req.getEvent().getFunction().getCallbackId().equals("hello")
385+
&& req.getEvent().getInputs().get("user_id").asString().equals("U03E94MK0")
386+
&& req.getEvent().getInputs().get("amount").asInteger().equals(1)
387+
&& ctx.isAttachingFunctionTokenEnabled()
388+
&& ctx.getFunctionBotAccessToken().equals("xwfp-valid"));
389+
called.set(ctx.client().functionsCompleteSuccess(r -> r
390+
.functionExecutionId(req.getEvent().getFunctionExecutionId())
391+
.outputs(new HashMap<>())
392+
).getError().equals(""));
393+
return ctx.ack();
394+
});
395+
app.function("something-else", (req, ctx) -> ctx.ack());
396+
397+
Response response = app.run(buildEventRequest());
398+
assertEquals(200L, response.getStatusCode().longValue());
399+
assertTrue(called.get());
400+
}
401+
402+
@Test
403+
public void regexp_callback_id() throws Exception {
404+
App app = buildApp();
405+
AtomicBoolean called = new AtomicBoolean(false);
406+
app.function(Pattern.compile("^he.+"), (req, ctx) -> {
407+
called.set(req.getEvent().getFunction().getCallbackId().equals("hello")
408+
&& req.getEvent().getInputs().get("user_id").asString().equals("U03E94MK0")
409+
&& req.getEvent().getInputs().get("amount").asInteger().equals(1)
410+
&& ctx.isAttachingFunctionTokenEnabled()
411+
&& ctx.getFunctionBotAccessToken().equals("xwfp-valid"));
412+
called.set(ctx.client().functionsCompleteSuccess(r -> r
413+
.functionExecutionId(req.getEvent().getFunctionExecutionId())
414+
.outputs(new HashMap<>())
415+
).getError().equals(""));
416+
return ctx.ack();
417+
});
418+
app.function("something-else", (req, ctx) -> ctx.ack());
419+
420+
Response response = app.run(buildEventRequest());
421+
assertEquals(200L, response.getStatusCode().longValue());
422+
assertTrue(called.get());
423+
}
424+
381425
@Test
382426
public void button_clicks() throws Exception {
383427
App app = buildApp();
@@ -389,8 +433,6 @@ public void button_clicks() throws Exception {
389433
&& req.getPayload().getBotAccessToken().equals("xwfp-this-is-valid")
390434
);
391435
called.set(ctx.client().functionsCompleteSuccess(r -> r
392-
// TODO: remove this token passing by enhancing bolt internals
393-
.token(req.getPayload().getBotAccessToken())
394436
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
395437
.outputs(new HashMap<>())
396438
).getError().equals(""));
@@ -413,8 +455,6 @@ public void view_submissions() throws Exception {
413455
&& req.getPayload().getBotAccessToken().equals("xwfp-this-is-valid")
414456
);
415457
called.set(ctx.client().functionsCompleteSuccess(r -> r
416-
// TODO: remove this token passing by enhancing bolt internals
417-
.token(req.getPayload().getBotAccessToken())
418458
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
419459
.outputs(new HashMap<>())
420460
).getError().equals(""));
@@ -461,4 +501,5 @@ ViewSubmissionRequest buildViewSubmissionRequest() {
461501
setRequestHeaders(body, rawHeaders, timestamp);
462502
return new ViewSubmissionRequest(body, viewSubmissionPayload, new RequestHeaders(rawHeaders));
463503
}
504+
464505
}

0 commit comments

Comments
 (0)