Skip to content

Commit

Permalink
Add custom function support (#1241)
Browse files Browse the repository at this point in the history
  • Loading branch information
seratch authored Aug 20, 2024
1 parent efa3f24 commit 52d1c76
Show file tree
Hide file tree
Showing 15 changed files with 419 additions and 67 deletions.
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())
.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())
.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
35 changes: 35 additions & 0 deletions bolt/src/main/java/com/slack/api/bolt/context/FunctionUtility.java
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

0 comments on commit 52d1c76

Please sign in to comment.