Skip to content

Commit 32dd49b

Browse files
committed
Add remote function support
1 parent 7942a3f commit 32dd49b

36 files changed

Lines changed: 970 additions & 11 deletions

File tree

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

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
package samples;
22

3+
import com.google.gson.Gson;
34
import com.slack.api.bolt.App;
45
import com.slack.api.bolt.AppConfig;
56
import com.slack.api.bolt.socket_mode.SocketModeApp;
67
import com.slack.api.model.Message;
78
import com.slack.api.model.block.element.RichTextSectionElement;
8-
import com.slack.api.model.event.AppMentionEvent;
9-
import com.slack.api.model.event.MessageChangedEvent;
10-
import com.slack.api.model.event.MessageDeletedEvent;
11-
import com.slack.api.model.event.MessageEvent;
9+
import com.slack.api.model.event.*;
1210
import com.slack.api.model.view.ViewState;
11+
import com.slack.api.util.json.GsonFactory;
1312
import config.Constants;
1413

1514
import java.util.Arrays;
@@ -153,6 +152,91 @@ public static void main(String[] args) throws Exception {
153152
return ctx.ack();
154153
});
155154

155+
app.event(FunctionExecutedEvent.class, (req, ctx) -> {
156+
ctx.logger.info("req: {}", req);
157+
ctx.client().chatPostMessage(r -> r
158+
.channel(req.getEvent().getInputs().get("user_id").asString())
159+
.text("hey!")
160+
.blocks(asBlocks(actions(a -> a.blockId("b").elements(asElements(
161+
button(b -> b.actionId("remote-function-button-success").value("clicked").text(plainText("block_actions success"))),
162+
button(b -> b.actionId("remote-function-button-error").value("clicked").text(plainText("block_actions error"))),
163+
button(b -> b.actionId("remote-function-modal").value("clicked").text(plainText("modal view")))
164+
)))))
165+
);
166+
return ctx.ack();
167+
});
168+
169+
app.blockAction("remote-function-button-success", (req, ctx) -> {
170+
Map<String, Object> outputs = new HashMap<>();
171+
outputs.put("user_id", req.getPayload().getFunctionData().getInputs().get("user_id").asString());
172+
ctx.client().functionsCompleteSuccess(r -> r
173+
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
174+
.outputs(outputs)
175+
);
176+
ctx.client().chatUpdate(r -> r
177+
.channel(req.getPayload().getContainer().getChannelId())
178+
.ts(req.getPayload().getContainer().getMessageTs())
179+
.text("Thank you!")
180+
);
181+
return ctx.ack();
182+
});
183+
app.blockAction("remote-function-button-error", (req, ctx) -> {
184+
ctx.client().functionsCompleteError(r -> r
185+
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
186+
.error("test error!")
187+
);
188+
ctx.client().chatUpdate(r -> r
189+
.channel(req.getPayload().getContainer().getChannelId())
190+
.ts(req.getPayload().getContainer().getMessageTs())
191+
.text("Thank you!")
192+
);
193+
return ctx.ack();
194+
});
195+
app.blockAction("remote-function-modal", (req, ctx) -> {
196+
ctx.client().viewsOpen(r -> r
197+
.triggerId(req.getPayload().getInteractivity().getInteractivityPointer())
198+
.view(view(v -> v
199+
.type("modal")
200+
.callbackId("remote-function-view")
201+
.title(viewTitle(vt -> vt.type("plain_text").text("Remote Function test")))
202+
.close(viewClose(vc -> vc.type("plain_text").text("Close")))
203+
.submit(viewSubmit(vs -> vs.type("plain_text").text("Submit")))
204+
.notifyOnClose(true)
205+
.blocks(asBlocks(input(input -> input
206+
.blockId("text-block")
207+
.element(plainTextInput(pti -> pti.actionId("text-action").multiline(true)))
208+
.label(plainText(pt -> pt.text("Text").emoji(true)))
209+
)))
210+
)));
211+
ctx.client().chatUpdate(r -> r
212+
.channel(req.getPayload().getContainer().getChannelId())
213+
.ts(req.getPayload().getContainer().getMessageTs())
214+
.text("Thank you!")
215+
);
216+
return ctx.ack();
217+
});
218+
219+
app.viewSubmission("remote-function-view", (req, ctx) -> {
220+
Map<String, Object> outputs = new HashMap<>();
221+
outputs.put("user_id", ctx.getRequestUserId());
222+
ctx.client().functionsCompleteSuccess(r -> r
223+
.token(req.getPayload().getBotAccessToken())
224+
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
225+
.outputs(outputs)
226+
);
227+
return ctx.ack();
228+
});
229+
app.viewClosed("remote-function-view", (req, ctx) -> {
230+
Map<String, Object> outputs = new HashMap<>();
231+
outputs.put("user_id", ctx.getRequestUserId());
232+
ctx.client().functionsCompleteSuccess(r -> r
233+
.token(req.getPayload().getBotAccessToken())
234+
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
235+
.outputs(outputs)
236+
);
237+
return ctx.ack();
238+
});
239+
156240
String appToken = System.getenv(Constants.SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN);
157241
SocketModeApp socketModeApp = new SocketModeApp(appToken, app);
158242
socketModeApp.start();

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,7 @@ public Response run(Request request) throws Exception {
582582
if (request == null || request.getContext() == null) {
583583
return Response.builder().statusCode(400).body("Invalid Request").build();
584584
}
585+
request.getContext().setAttachingFunctionTokenEnabled(this.config().isAttachingFunctionTokenEnabled());
585586
request.getContext().setSlack(slack()); // use the properly configured API client
586587

587588
if (neverStarted.get()) {

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();
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"ok": false,
3+
"error": "",
4+
"needed": "",
5+
"provided": ""
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"ok": false,
3+
"error": "",
4+
"needed": "",
5+
"provided": ""
6+
}

0 commit comments

Comments
 (0)