Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 108 additions & 20 deletions bolt-socket-mode/src/test/java/samples/SimpleApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

import static com.slack.api.model.block.Blocks.*;
import static com.slack.api.model.block.composition.BlockCompositions.dispatchActionConfig;
Expand Down Expand Up @@ -150,15 +151,116 @@ public static void main(String[] args) throws Exception {
return ctx.ack();
});

// Note that this is still in beta as of Nov 2023
app.event(FunctionExecutedEvent.class, (req, ctx) -> {
// TODO: future updates enable passing callback_id as below
/* Example App Manifest
{
"display_information": {
"name": "manifest-test-app-2"
},
"features": {
"bot_user": {
"display_name": "test-bot",
"always_online": true
}
},
"oauth_config": {
"scopes": {
"bot": [
"commands",
"chat:write",
"app_mentions:read"
]
}
},
"settings": {
"event_subscriptions": {
"bot_events": [
"app_mention",
"function_executed"
]
},
"interactivity": {
"is_enabled": true
},
"org_deploy_enabled": true,
"socket_mode_enabled": true,
"token_rotation_enabled": false,
"hermes_app_type": "remote",
"function_runtime": "remote"
},
"functions": {
"hello": {
"title": "Hello",
"description": "Hello world!",
"input_parameters": {
"amount": {
"type": "number",
"title": "Amount",
"description": "How many do you need?",
"is_required": false,
"hint": "How many do you need?",
"name": "amount",
"maximum": 10,
"minimum": 1
},
"user_id": {
"type": "slack#/types/user_id",
"title": "User",
"description": "Who to send it",
"is_required": true,
"hint": "Select a user in the workspace",
"name": "user_id"
},
"message": {
"type": "string",
"title": "Message",
"description": "Whatever you want to tell",
"is_required": false,
"hint": "up to 100 characters",
"name": "message",
"maxLength": 100,
"minLength": 1
}
},
"output_parameters": {
"amount": {
"type": "number",
"title": "Amount",
"description": "How many do you need?",
"is_required": false,
"hint": "How many do you need?",
"name": "amount",
"maximum": 10,
"minimum": 1
},
"user_id": {
"type": "slack#/types/user_id",
"title": "User",
"description": "Who to send it",
"is_required": true,
"hint": "Select a user in the workspace",
"name": "user_id"
},
"message": {
"type": "string",
"title": "Message",
"description": "Whatever you want to tell",
"is_required": false,
"hint": "up to 100 characters",
"name": "message",
"maxLength": 100,
"minLength": 1
}
}
}
}
}
*/

// app.event(FunctionExecutedEvent.class, (req, ctx) -> {
// app.function("hello", (req, ctx) -> {
// app.function(Pattern.compile("^he.+$"), (req, ctx) -> {
app.function(Pattern.compile("^he.+$"), (req, ctx) -> {
ctx.logger.info("req: {}", req);
ctx.client().chatPostMessage(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getEvent().getBotAccessToken())
.channel(req.getEvent().getInputs().get("user_id").asString())
.text("hey!")
.blocks(asBlocks(actions(a -> a.blockId("b").elements(asElements(
Expand All @@ -174,14 +276,10 @@ public static void main(String[] args) throws Exception {
Map<String, Object> outputs = new HashMap<>();
outputs.put("user_id", req.getPayload().getFunctionData().getInputs().get("user_id").asString());
ctx.client().functionsCompleteSuccess(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getPayload().getBotAccessToken())
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
.outputs(outputs)
);
ctx.client().chatUpdate(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getPayload().getBotAccessToken())
.channel(req.getPayload().getContainer().getChannelId())
.ts(req.getPayload().getContainer().getMessageTs())
.text("Thank you!")
Expand All @@ -190,14 +288,10 @@ public static void main(String[] args) throws Exception {
});
app.blockAction("remote-function-button-error", (req, ctx) -> {
ctx.client().functionsCompleteError(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getPayload().getBotAccessToken())
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
.error("test error!")
);
ctx.client().chatUpdate(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getPayload().getBotAccessToken())
.channel(req.getPayload().getContainer().getChannelId())
.ts(req.getPayload().getContainer().getMessageTs())
.text("Thank you!")
Expand All @@ -206,8 +300,6 @@ public static void main(String[] args) throws Exception {
});
app.blockAction("remote-function-modal", (req, ctx) -> {
ctx.client().viewsOpen(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getPayload().getBotAccessToken())
.triggerId(req.getPayload().getInteractivity().getInteractivityPointer())
.view(view(v -> v
.type("modal")
Expand All @@ -223,8 +315,6 @@ public static void main(String[] args) throws Exception {
)))
)));
ctx.client().chatUpdate(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getPayload().getBotAccessToken())
.channel(req.getPayload().getContainer().getChannelId())
.ts(req.getPayload().getContainer().getMessageTs())
.text("Thank you!")
Expand All @@ -236,7 +326,6 @@ public static void main(String[] args) throws Exception {
Map<String, Object> outputs = new HashMap<>();
outputs.put("user_id", ctx.getRequestUserId());
ctx.client().functionsCompleteSuccess(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getPayload().getBotAccessToken())
Comment thread
seratch marked this conversation as resolved.
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
.outputs(outputs)
Expand All @@ -247,7 +336,6 @@ public static void main(String[] args) throws Exception {
Map<String, Object> outputs = new HashMap<>();
outputs.put("user_id", ctx.getRequestUserId());
ctx.client().functionsCompleteSuccess(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getPayload().getBotAccessToken())
Comment thread
seratch marked this conversation as resolved.
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
.outputs(outputs)
Expand Down
33 changes: 29 additions & 4 deletions bolt/src/main/java/com/slack/api/bolt/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@
import com.slack.api.methods.MethodsClient;
import com.slack.api.methods.SlackApiException;
import com.slack.api.methods.response.auth.AuthTestResponse;
import com.slack.api.model.event.AppUninstalledEvent;
import com.slack.api.model.event.Event;
import com.slack.api.model.event.MessageEvent;
import com.slack.api.model.event.TokensRevokedEvent;
import com.slack.api.model.event.*;
import com.slack.api.util.json.GsonFactory;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand Down Expand Up @@ -582,6 +579,7 @@ public Response run(Request request) throws Exception {
if (request == null || request.getContext() == null) {
return Response.builder().statusCode(400).body("Invalid Request").build();
}
request.getContext().setAttachingFunctionTokenEnabled(this.config().isAttachingFunctionTokenEnabled());
request.getContext().setSlack(slack()); // use the properly configured API client

if (neverStarted.get()) {
Expand Down Expand Up @@ -648,6 +646,33 @@ public App event(EventHandler<?> handler) {
return this;
}

public App function(String callbackId, BoltEventHandler<FunctionExecutedEvent> handler) {
return event(FunctionExecutedEvent.class, true, (req, ctx) -> {
if (log.isDebugEnabled()) {
log.debug("Run a function_executed event handler (callback_id: {})", callbackId);
}
if (callbackId.equals(req.getEvent().getFunction().getCallbackId())) {
return handler.apply(req, ctx);
} else {
return null;
}
});
}

public App function(Pattern callbackId, BoltEventHandler<FunctionExecutedEvent> handler) {
return event(FunctionExecutedEvent.class, true, (req, ctx) -> {
if (log.isDebugEnabled()) {
log.debug("Run a function_executed event handler (callback_id: {})", callbackId);
}
String sentCallbackId = req.getEvent().getFunction().getCallbackId();
if (callbackId.matcher(sentCallbackId).matches()) {
return handler.apply(req, ctx);
} else {
return null;
}
});
}

public App message(String pattern, BoltEventHandler<MessageEvent> messageHandler) {
return message(Pattern.compile("^.*" + Pattern.quote(pattern) + ".*$"), messageHandler);
}
Expand Down
9 changes: 9 additions & 0 deletions bolt/src/main/java/com/slack/api/bolt/AppConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,15 @@ public void setOauthRedirectUriPath(String oauthRedirectUriPath) {
@Builder.Default
private boolean allEventsApiAutoAckEnabled = false;

/**
* When true, the framework automatically attaches context#functionBotAccessToken
* to context#client instead of context#botToken.
* Enabling this behavior only affects function_executed event handlers
* and app.action/app.view handlers associated with the function token.
*/
@Builder.Default
private boolean attachingFunctionTokenEnabled = true;

// ---------------------------------
// Default middleware configuration
// ---------------------------------
Expand Down
28 changes: 26 additions & 2 deletions bolt/src/main/java/com/slack/api/bolt/context/Context.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,26 @@ public abstract class Context {
* A bot token associated with this request. The format must be starting with `xoxb-`.
*/
protected String botToken;

/**
* When true, the framework automatically attaches context#functionBotAccessToken
* to context#client instead of context#botToken.
* Enabling this behavior only affects function_executed event handlers
* and app.action/app.view handlers associated with the function token.
*/
private boolean attachingFunctionTokenEnabled;

/**
* The bot token associated with this "function_executed"-type event and its interactions.
* The format must be starting with `xoxb-`.
*/
protected String functionBotAccessToken;

/**
* The ID of function_executed event delivery.
*/
protected String functionExecutionId;

/**
* The scopes associated to the botToken
*/
Expand Down Expand Up @@ -88,17 +108,21 @@ public abstract class Context {
protected final Map<String, String> additionalValues = new HashMap<>();

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

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

public ChatPostMessageResponse say(BuilderConfigurator<ChatPostMessageRequest.ChatPostMessageRequestBuilder> request) throws IOException, SlackApiException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.slack.api.bolt.context;

import com.slack.api.methods.MethodsClient;
import com.slack.api.methods.SlackApiException;
import com.slack.api.methods.request.chat.ChatPostMessageRequest;
import com.slack.api.methods.response.chat.ChatPostMessageResponse;
import com.slack.api.methods.response.functions.FunctionsCompleteErrorResponse;
import com.slack.api.methods.response.functions.FunctionsCompleteSuccessResponse;
import com.slack.api.model.block.LayoutBlock;

import java.io.IOException;
import java.util.List;
import java.util.Map;

public interface FunctionUtility {

String getFunctionExecutionId();

MethodsClient client();

default FunctionsCompleteSuccessResponse complete(Map<String, ?> outputs) throws IOException, SlackApiException {
return this.client().functionsCompleteSuccess(r -> r
.functionExecutionId(this.getFunctionExecutionId())
.outputs(outputs)
);
}

default FunctionsCompleteErrorResponse fail(String error) throws IOException, SlackApiException {
return this.client().functionsCompleteError(r -> r
.functionExecutionId(this.getFunctionExecutionId())
.error(error)
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.slack.api.bolt.context.ActionRespondUtility;
import com.slack.api.bolt.context.Context;
import com.slack.api.bolt.context.FunctionUtility;
import com.slack.api.bolt.util.Responder;
import lombok.*;

Expand All @@ -15,7 +16,7 @@
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = false)
public class ActionContext extends Context implements ActionRespondUtility {
public class ActionContext extends Context implements ActionRespondUtility, FunctionUtility {

private String triggerId;
private String responseUrl;
Expand Down
Loading