Skip to content

Commit 52d1c76

Browse files
authored
Add custom function support (#1241)
1 parent efa3f24 commit 52d1c76

File tree

15 files changed

+419
-67
lines changed

15 files changed

+419
-67
lines changed

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

Lines changed: 108 additions & 20 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;
@@ -150,15 +151,116 @@ public static void main(String[] args) throws Exception {
150151
return ctx.ack();
151152
});
152153

153-
// 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
154+
/* Example App Manifest
155+
{
156+
"display_information": {
157+
"name": "manifest-test-app-2"
158+
},
159+
"features": {
160+
"bot_user": {
161+
"display_name": "test-bot",
162+
"always_online": true
163+
}
164+
},
165+
"oauth_config": {
166+
"scopes": {
167+
"bot": [
168+
"commands",
169+
"chat:write",
170+
"app_mentions:read"
171+
]
172+
}
173+
},
174+
"settings": {
175+
"event_subscriptions": {
176+
"bot_events": [
177+
"app_mention",
178+
"function_executed"
179+
]
180+
},
181+
"interactivity": {
182+
"is_enabled": true
183+
},
184+
"org_deploy_enabled": true,
185+
"socket_mode_enabled": true,
186+
"token_rotation_enabled": false,
187+
"hermes_app_type": "remote",
188+
"function_runtime": "remote"
189+
},
190+
"functions": {
191+
"hello": {
192+
"title": "Hello",
193+
"description": "Hello world!",
194+
"input_parameters": {
195+
"amount": {
196+
"type": "number",
197+
"title": "Amount",
198+
"description": "How many do you need?",
199+
"is_required": false,
200+
"hint": "How many do you need?",
201+
"name": "amount",
202+
"maximum": 10,
203+
"minimum": 1
204+
},
205+
"user_id": {
206+
"type": "slack#/types/user_id",
207+
"title": "User",
208+
"description": "Who to send it",
209+
"is_required": true,
210+
"hint": "Select a user in the workspace",
211+
"name": "user_id"
212+
},
213+
"message": {
214+
"type": "string",
215+
"title": "Message",
216+
"description": "Whatever you want to tell",
217+
"is_required": false,
218+
"hint": "up to 100 characters",
219+
"name": "message",
220+
"maxLength": 100,
221+
"minLength": 1
222+
}
223+
},
224+
"output_parameters": {
225+
"amount": {
226+
"type": "number",
227+
"title": "Amount",
228+
"description": "How many do you need?",
229+
"is_required": false,
230+
"hint": "How many do you need?",
231+
"name": "amount",
232+
"maximum": 10,
233+
"minimum": 1
234+
},
235+
"user_id": {
236+
"type": "slack#/types/user_id",
237+
"title": "User",
238+
"description": "Who to send it",
239+
"is_required": true,
240+
"hint": "Select a user in the workspace",
241+
"name": "user_id"
242+
},
243+
"message": {
244+
"type": "string",
245+
"title": "Message",
246+
"description": "Whatever you want to tell",
247+
"is_required": false,
248+
"hint": "up to 100 characters",
249+
"name": "message",
250+
"maxLength": 100,
251+
"minLength": 1
252+
}
253+
}
254+
}
255+
}
256+
}
257+
*/
258+
259+
// app.event(FunctionExecutedEvent.class, (req, ctx) -> {
156260
// app.function("hello", (req, ctx) -> {
157-
// app.function(Pattern.compile("^he.+$"), (req, ctx) -> {
261+
app.function(Pattern.compile("^he.+$"), (req, ctx) -> {
158262
ctx.logger.info("req: {}", req);
159263
ctx.client().chatPostMessage(r -> r
160-
// TODO: remove this token passing by enhancing bolt internals
161-
.token(req.getEvent().getBotAccessToken())
162264
.channel(req.getEvent().getInputs().get("user_id").asString())
163265
.text("hey!")
164266
.blocks(asBlocks(actions(a -> a.blockId("b").elements(asElements(
@@ -174,14 +276,10 @@ public static void main(String[] args) throws Exception {
174276
Map<String, Object> outputs = new HashMap<>();
175277
outputs.put("user_id", req.getPayload().getFunctionData().getInputs().get("user_id").asString());
176278
ctx.client().functionsCompleteSuccess(r -> r
177-
// TODO: remove this token passing by enhancing bolt internals
178-
.token(req.getPayload().getBotAccessToken())
179279
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
180280
.outputs(outputs)
181281
);
182282
ctx.client().chatUpdate(r -> r
183-
// TODO: remove this token passing by enhancing bolt internals
184-
.token(req.getPayload().getBotAccessToken())
185283
.channel(req.getPayload().getContainer().getChannelId())
186284
.ts(req.getPayload().getContainer().getMessageTs())
187285
.text("Thank you!")
@@ -190,14 +288,10 @@ public static void main(String[] args) throws Exception {
190288
});
191289
app.blockAction("remote-function-button-error", (req, ctx) -> {
192290
ctx.client().functionsCompleteError(r -> r
193-
// TODO: remove this token passing by enhancing bolt internals
194-
.token(req.getPayload().getBotAccessToken())
195291
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
196292
.error("test error!")
197293
);
198294
ctx.client().chatUpdate(r -> r
199-
// TODO: remove this token passing by enhancing bolt internals
200-
.token(req.getPayload().getBotAccessToken())
201295
.channel(req.getPayload().getContainer().getChannelId())
202296
.ts(req.getPayload().getContainer().getMessageTs())
203297
.text("Thank you!")
@@ -206,8 +300,6 @@ public static void main(String[] args) throws Exception {
206300
});
207301
app.blockAction("remote-function-modal", (req, ctx) -> {
208302
ctx.client().viewsOpen(r -> r
209-
// TODO: remove this token passing by enhancing bolt internals
210-
.token(req.getPayload().getBotAccessToken())
211303
.triggerId(req.getPayload().getInteractivity().getInteractivityPointer())
212304
.view(view(v -> v
213305
.type("modal")
@@ -223,8 +315,6 @@ public static void main(String[] args) throws Exception {
223315
)))
224316
)));
225317
ctx.client().chatUpdate(r -> r
226-
// TODO: remove this token passing by enhancing bolt internals
227-
.token(req.getPayload().getBotAccessToken())
228318
.channel(req.getPayload().getContainer().getChannelId())
229319
.ts(req.getPayload().getContainer().getMessageTs())
230320
.text("Thank you!")
@@ -236,7 +326,6 @@ public static void main(String[] args) throws Exception {
236326
Map<String, Object> outputs = new HashMap<>();
237327
outputs.put("user_id", ctx.getRequestUserId());
238328
ctx.client().functionsCompleteSuccess(r -> r
239-
// TODO: remove this token passing by enhancing bolt internals
240329
.token(req.getPayload().getBotAccessToken())
241330
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
242331
.outputs(outputs)
@@ -247,7 +336,6 @@ public static void main(String[] args) throws Exception {
247336
Map<String, Object> outputs = new HashMap<>();
248337
outputs.put("user_id", ctx.getRequestUserId());
249338
ctx.client().functionsCompleteSuccess(r -> r
250-
// TODO: remove this token passing by enhancing bolt internals
251339
.token(req.getPayload().getBotAccessToken())
252340
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
253341
.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: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,26 @@ 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+
72+
/**
73+
* The ID of function_executed event delivery.
74+
*/
75+
protected String functionExecutionId;
76+
5777
/**
5878
* The scopes associated to the botToken
5979
*/
@@ -88,17 +108,21 @@ public abstract class Context {
88108
protected final Map<String, String> additionalValues = new HashMap<>();
89109

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

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

104128
public ChatPostMessageResponse say(BuilderConfigurator<ChatPostMessageRequest.ChatPostMessageRequestBuilder> request) throws IOException, SlackApiException {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.slack.api.bolt.context;
2+
3+
import com.slack.api.methods.MethodsClient;
4+
import com.slack.api.methods.SlackApiException;
5+
import com.slack.api.methods.request.chat.ChatPostMessageRequest;
6+
import com.slack.api.methods.response.chat.ChatPostMessageResponse;
7+
import com.slack.api.methods.response.functions.FunctionsCompleteErrorResponse;
8+
import com.slack.api.methods.response.functions.FunctionsCompleteSuccessResponse;
9+
import com.slack.api.model.block.LayoutBlock;
10+
11+
import java.io.IOException;
12+
import java.util.List;
13+
import java.util.Map;
14+
15+
public interface FunctionUtility {
16+
17+
String getFunctionExecutionId();
18+
19+
MethodsClient client();
20+
21+
default FunctionsCompleteSuccessResponse complete(Map<String, ?> outputs) throws IOException, SlackApiException {
22+
return this.client().functionsCompleteSuccess(r -> r
23+
.functionExecutionId(this.getFunctionExecutionId())
24+
.outputs(outputs)
25+
);
26+
}
27+
28+
default FunctionsCompleteErrorResponse fail(String error) throws IOException, SlackApiException {
29+
return this.client().functionsCompleteError(r -> r
30+
.functionExecutionId(this.getFunctionExecutionId())
31+
.error(error)
32+
);
33+
}
34+
35+
}

bolt/src/main/java/com/slack/api/bolt/context/builtin/ActionContext.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.slack.api.bolt.context.ActionRespondUtility;
44
import com.slack.api.bolt.context.Context;
5+
import com.slack.api.bolt.context.FunctionUtility;
56
import com.slack.api.bolt.util.Responder;
67
import lombok.*;
78

@@ -15,7 +16,7 @@
1516
@AllArgsConstructor
1617
@ToString(callSuper = true)
1718
@EqualsAndHashCode(callSuper = false)
18-
public class ActionContext extends Context implements ActionRespondUtility {
19+
public class ActionContext extends Context implements ActionRespondUtility, FunctionUtility {
1920

2021
private String triggerId;
2122
private String responseUrl;

0 commit comments

Comments
 (0)