1. About

The Spring Slack Bot project aims to provide a turn-key framework for implementing a CLI-style Slack bot at H-E-B.

Being a Spring library, the framework is designed so that users only need to provide the business logic for their specific commands and use cases. Much like Spring controller conventions, the library’s underlying AOT-driven implementation handles receiving events from the Slack API and invoking the processors of associated commands.

A powerful framework for command invocation exists which supports running commands both in a threaded manner and reactively through Project Reactor.

The framework is opinionated and dictates a Unix CLI-style command interface with support for a command hierarchy, much like the familiar git and docker CLI utilities. A security layer is included which provides RBAC functionality to individual commands.

The library is based upon the Spring Slack library, which requires configuration in addition to any configuration required by this library’s implementation. See the Spring Slack documentation for details.

The Picocli and Spring Boot frameworks are heavily relied on to provide the functionality of this library. This documentation does not aim to provide full usage of these libraries, and their respective documentation should be referenced if needed.

2. Concepts/Usage

To build a Slack bot with this library, it is useful to understand the conventions it assumes and how a client uses the final product.

2.1. Conventions

The Spring Slack Bot library enforces a Unix CLI-style structure allowing for a flat list of commands or a command hierarchy of arbitrary depth. Each command supports a syntax as defined by the command, with help available via the -h flag.

As a very contrived example, a Slack bot might have a hierarchy of groups and commands.

  • reports

    • releases

    • sales-data

    • incidents

  • flags

    • enable

    • disable

    • disable-all

The reports releases example from above might support some command line options to tailor the timeframe the report uses for its data. For example:

  • -m: month to generate the report for.

  • -y: year to generate the report for.

2.2. Usage

The bot can be interacted with by mentioning the Slack bot’s account in a channel where the bot account has been invited. Command messages sent to the bot must begin with the mention, followed by the complete path to the command, and finally any command arguments.

For example:

@bot reports incidents

2.3. Command Usage

Using the example releases command from above, a request might resemble the following.

@bot reports releases -m 1 -y 2022

2.4. Command Help

Help can be requested for any command that is configured with a picocli command line specification by passing the -h flag to the command. A help request might resemble the following.

@bot reports releases -h

Usage: reports releases [-hV] [-m[=<month>]] [-y[=<year>]]
  -h, --help              Show this help message and exit.
  -m, --month[=<month>]   The month number to generate a report for. Defaults
                            to current month.
  -V, --version           Print version information and exit.
  -y, --year[=<year>]     The year to generate a report for. Defaults to
                            current year.

Additionally, requesting help for a command that contains sub-commands will display the list of supported sub-commands.

@bot -h

Usage: @bot [-hV] [PARAM...] [COMMAND]
      [PARAM...]
  -h, --help       Show this help message and exit.
  -V, --version    Print version information and exit.
Commands:
  reports
  flags

3. Getting Started

3.1. Slack App Account

Before any code is written, a Slack application must be created for use with the bot. The application appears as a user in the Slack namespace it is registered to, and part of the setup generates keys or tokens needed to run the Slack bot.

3.1.1. Creating The App Account

To start the app creation process, head to https://api.slack.com/ and click Create an app. If prompted, choose to create an app from scratch.

The name of the application will also be the name of the bot as seen in Slack. Be sure to choose the appropriate Slack namespace to add the bot to.

3.1.2. Configuring The App Account

A few things need to be configured on the account for the bot framework to function correctly.

Socket Mode

The bot library may receive events from the Slack service either through direct HTTP calls or through a persistent socket connection. Socket connections are useful in situations where the application is unable to accept ingress connections from the internet.

If the application requires a socket connection, the Slack app must be configured in the Slack web console as a socket mode application. Under the app account’s settings, navigate to the Socket Mode screen. Be sure that socket mode is enabled. When enabled, and if asked, no additional scopes are required. Enabling socket mode will generate an App-Level token, which is used to configure the socket-mode API connection in the Slack bot.

The app token can be retrieved later and as needed from the Slack app’s configuration page.
Event Subscriptions

In order to respond to the app mention events the bot framework relies on, an event subscription must be created. Under the Event Subscriptions section, open the section titled Subscribe to bot events. Click the Add Bot User Event button, and select the app_mention event type.

OAuth Scopes

For the library to access the Slack APIs it requires, a number of OAuth scopes must be configured on the Slack app.

From the app account’s settings, navigate to the OAuth & Permissions screen. Under the Scopes section, add the following scopes.

  • app_mentions:read

  • chat:write

  • files:write

  • users:read

While these are the minimum set of scopes needed for the library to function, more scopes may be added depending on the needs of any additional functionality implemented by Slack bot commands.

3.1.3. Installing The App Account

Once the Slack app has been configured it must be added to the Slack workspace. Under the Basic Information screen, click the Request to Install button under the Install your app drop-down.

If the workspace requires approval, be sure to enter a description when prompted for what your bot intends to do to assist workspace administrators with the approval process. App accounts requiring approval need to be approved before making further progress.

Once the app has been added to the workspace, simply invite the app’s account to any channels that the bot should respond to commands in.

3.2. Project Setup

Spring Slack Bot is implemented as a Spring Boot library, and the flow of bootstrapping an application follows the typical process for creating a Spring Boot application by setting up the project build (using Maven or Gradle), adding the required dependencies, and creating custom Slack commands.

The process to create or develop a Spring Boot application is outside the scope of this document, however, Spring has excellent documentation that can be referenced to quickly set up an application.
The Spring Initializr web application is a great tool to quickly create the skeleton of a Spring Boot application.

3.2.1. Installing

To use the Spring Slack Bot library, the dependencies must be added to the application. The manner by which this is done depends on the build tool the application uses.

Core Library

The core library contains the majority of the Slack Bot framework. It can be included using Gradle (build.gradle) or Maven (pom.xml) as follows.

build.gradle
implementation('com.heb:spring-slack-bot-cli:2.0.0')
pom.xml
<dependency>
    <groupId>com.heb</groupId>
    <artifactId>spring-slack-bot-cli</artifactId>
    <version>2.0.0</version>
</dependency>
Web Library

If the application will accept events from Slack via HTTP requests, the spring-slack-web library is required.

build.gradle
implementation('com.heb:spring-slack-web:2.0.0')
pom.xml
<dependency>
    <groupId>com.heb</groupId>
    <artifactId>spring-slack-web</artifactId>
    <version>2.0.0</version>
</dependency>
Socket Library

If the application requies a persistent socket connection, the spring-slack-socket library is required.

build.gradle
implementation('com.heb:spring-slack-socket:2.0.0')
pom.xml
<dependency>
    <groupId>com.heb</groupId>
    <artifactId>spring-slack-socket</artifactId>
    <version>2.0.0</version>
</dependency>
Project Reactor Extension

The library also supports Project Reactor through the use of another optional library in conjunction with the core library configured above.

build.gradle
implementation('com.heb:spring-slack-bot-reactor:2.0.0')
pom.xml
<dependency>
    <groupId>com.heb</groupId>
    <artifactId>spring-slack-bot-reactor</artifactId>
    <version>2.0.0</version>
</dependency>

3.3. Configuration

The Slack bot library is built on top of the spring-slack libraries, and most configuration of the bot is applied to this underlying library. See the Spring Slack documentation for a base set of configuration properties that must be configured.

In addition to the configuration required by the spring-slack libraries, the following configuration is available.

The following configuration properties are prefixed by slack.cli.

Property

Required

Default

Description

command-thread-pool-min-size

No

0

Minimum number of threads to allocate to the command thread pool.

command-thread-pool-max-size

No

100

Maximum number of threads that may be created in the command thread pool.

bot-user-id

Yes

Slack ID of the bot user (may be found by inspecting the bot account’s profile in Slack).

version

Yes

Version number of the Slack bot. The framework imposes no restrictions on the version other than it must exist.

convert-quotes

Yes

true

Convert fancy UTF-8 quotes to regular ASCII quotes.

strip-phone-link

No

true

Revert telephone number links back to user-provided arguments.

3.3.1. Security Configuration

In addition to the security configuration of the spring-slack library, the following configuration properties are available under the slack.cli.security prefix.

Property

Required

Default

Description

role-hierarchy

No

null

List of role hierarchy strings to configure with Spring Security. See this Baeldung guide for hierarchy strings.

3.3.2. Example Configuration

A full configuration for the application might resemble the following.

application.yml
slack:
  single-team-token: xoxb-example
  bolt-thread-pool-size: 10
  socket:
    app-token: xapp-1-example
  cli:
    bot-user-id: U1234687
    version: 1.0.0
    security:
      role-hierarchy:
        - ROLE_ADMIN > ROLE_FOO
  security:
    users:
      U1234567890:
        authorities:
          - ROLE_EXAMPLE

4. Commands

Slack commands are objects or method that define an interface to a specific operation and are exposed to a user by the Slack bot.

4.1. Defining Commands

Slack commands may be created in a few different ways, and applications may choose the most convenient method according to the needs of the application.

4.1.1. SlackCommand Annotation

The @SlackCommand annotation marks its target as a Slack command implementation, and provides some metadata about the command. At a minimum, it requires a String value, which serves as the command’s name. Additional properties are available which further define the behavior and structure of the command .

Property

Description

name

Name of the command. Names do not need to be globally unique, however, a name must be unique among its sibling commands. The value property is an alias for this property. When providing more parameters than just the name of the command to the annotation, the name parameter should be used instead of value.

id

A unique ID of the command. If no ID is provided, the name of the command is used as the ID. All command ID’s must be globally unique, and providing an explicit ID allows the framework to differentiate between commands that may share the same name but exist in different places of the command hierarchy.

parent

The ID of the parent command. If provided, the command will be configured as a sub-command of the command referenced by the parent ID. When omitted, the command will be configured as a top-level command. See the Command Hierarchy section for more details.

argumentClass

A class type that informs the framework of an object that defines the command line parameters and options supported by the command. If present, an instance of the configured object will be passed as part of the context to the command’s implementation when requested by a user. This provides a valuable method to provide a CLI contract to the user and ease consuming arguments.

The @SlackCommand annotation may be used on both classes and methods, each having differing requirements regarding their structure.

Object Commands

Classes may be annotated with the @SlackCommand annotation, which informs the framework that the class itself implements the command.

To execute command logic, an object annotated with @SlackCommand must implement one of the following interfaces.

Interface

Description

SlackFunction

A specialized version of the Function interface specific to the Slack Bot CLI framework. This interface enforces that the apply method will be provided a SlackCommandContext object. The interface takes 2 template arguments: one for the type of the arguments class (which is provided by the context) and another for the return type of the apply method. The types defined on the class have no impact on the framework, and implementations may apply types as necessary.

SlackConsumer

Much like the SlackFunction interface, this interface is a specialized version of Consumer which enforces that the accept method will be provided a SlackCommandContext object. The same template concerns as SlackFunction apply here, except that there is no return type parameter.

Callable

The standard Callable interface may be used in cases where the command returns some value but does not require the context.

Runnable

The Runnable interface may be used in cases where the command does not return a value nor require the context.

See the Command Context section for more details about the SlackCommandContext.

Object commands that do not implement one of the above are considered non-executable. If requested directly, the command will reply to the user with the usage message.

Non-executable objects are useful when a grouping of sub-commands is desired, but the group name itself does not perform any action.
Example Object Command
@SlackCommand(name = "foo", argumentsClass = FooOptions.class) (1)
public class FooCommand implements SlackFunction<FooOptions, String> { (2)
    public String apply(SlackCommandContext<FooOptions> context) { (3)
        return "Hello, " + context.getAuth().getName(); (4)
    }
}
1 Specify that the FooCommand method is a Slack command with the name foo. Additionally, define the arguments class for the command using the FooOptions class.
2 The SlackFunction interface informs the framework that the command will accept the command context as an input and will return some.
3 The apply method is part of the SlackFunction interface and is invoked when the foo command is requested.
4 A welcoming string mentioning the name of the user is returned and will be automatically posted to the original mention message as a thread.
Method Commands

Methods may be annotated with the @SlackCommand annotation. All Spring beans registered with the application, and their methods, are inspected and checked for the presence of the annotation. This provides a lot of flexibility by allowing Slack command implementations to exist in any Spring bean where they make sense to exist.

In contrast to object-based commands, where some form of execution must be provided by a supported interface, methods themselves are inherently executable. The framework provides a great deal of flexibility in how method signatures are defined, and the method signatures inform the framework of the behavior and requirements of the command itself.

Parameters of a method command may be dynamic, in that a method may choose to contain any number of the supported parameter types, or no parameters at all. The supported parameter types include the following.

Type

Description

SlackCommandContext

The complete command context for the invocation of the command. See the Command Context section for more details.

AppMentionEvent

The event object provided by the Slack SDK containing details of the request.

Authentication

A Spring Security token for the user making the request. Will either be the token for a fully authenticated user, or an anonymous user token. See the Spring Security section for more details.

List<String>

The raw list of arguments passed to the command before any processing.

String[]

An array form of the arguments passed to the command before any processing.

CommandLine

The Picocli object containing all the context for the parsed arguments. This is a fairly low-level object and is likely not often required.

Additionally, the class type of the object provided by the @SlackCommand.argumentsClass parameter is supported in the method signature.

Method commands may optionally return some value. By default, this value is transmitted back to the user who invoked the command. The framework imposes no constraints on the return type of the method, so a method may type the return as needed. Methods that do not need to return some value may return null or simply be marked void.

See the Response Handling section for more details on returning responses from commands.
Example Method Command
@Component
public class Foo {
    @SlackCommand(name = "foo", argumentsClass = FooOptions.class) (1)
    public String fooCommand(Authentication authentication, FooOptions fooOptions) { (2)
        return "Hello, " + authentication.getName(); (3)
    }
}
1 Specify that the fooCommand method is a Slack command with the name foo. Additionally, define the arguments class for the command using the FooOptions class.
2 The method signature requests the Authentication and FooOptions objects and will be populated with the authentication token of the requesting user and the parsed command line arguments.
3 A welcoming string mentioning the name of the user is returned and will be automatically posted to the original mention message as a thread.

4.1.2. Command Registration

At application runtime, the framework will inspect all Spring beans and their methods for the @SlackCommand annotation. All instances of objects or methods with the annotation are parsed into a SlackCommandRegistration object, which stores the required data necessary to invoke a command.

As an alternative to the @SlackCommand annotation, using the SlackCommandRegistration class directly allows applications to define commands programmatically instead of relying on AOP. Any Spring beans that are instances of the registration class are registered as-is with the framework with no further reflection required.

Example Registration
@Bean
public SlackCommandRegistration fooCommand() { (1)
    SlackFunction<FooOptions, String> function = ctx -> { (2)
        return "Hello, " + ctx.getAuth().getName(); (3)
    };
    return new SlackCommandRegistration("foo", function, null, FooOptions.class, null); (4)
}
1 Create the fooCommand bean as part of a Spring Configuration class.
2 Define the SlackFunction as a lambda.
3 A welcoming string mentioning the name of the user is returned and will be automatically posted to the original mention message as a thread.
4 Create the registration instance with the command name foo, the function, a null ID, the FooOptions class as the arguments class, and a null parent ID. The constructor for the registration object essentially mirrors the parameters of the @SlackCommand annotation.

4.1.3. Command Hierarchy

Commands may be organized into a tree-like structure of an arbitrary depth. This is accomplished by specifying a parent reference on a command (through the parent property on a command’s @SlackCommand annotation), which will result in the command becoming a child of the referenced parent.

Parents are referenced by the ID of their command. By default, if a command’s @SlackCommand annotation does not explicitly define an ID, the name of the command is used as the ID. Each command defined in an application must contain a globally unique ID, so if more than one command shares the same name, those commands must define unique identifiers to differentiate between them.

Annotations may be kept shorter and cleaner by omitting an explicitly defined ID as long as there are no name conflicts.

Typically, a command is configured as a top-level command when its annotation does not define a parent reference. However, commands implemented as methods may be configured as implicit child commands of their containing class. If a class that contains Slack command methods is itself annotated with @SlackCommand, then any methods defined in that class that are also annotated with @SlackCommand will automatically become children of the class' command.

Commands defined as methods inside a class also annotated as a command may still explicitly set a parent reference, which will take precedence over the implicit parent/child relationship of the class and method.

As an example, the following code creates a nested command structure with the parent command foo which contains the child commands bar and baz .

  • foo

    • bar

    • baz

@SlackCommand("foo")
class Foo {
    @SlackCommand("bar")
    String foo() {
        return "foo";
    }

    @SlackCommand("baz")
    String baz() {
        return "baz";
    }
}

4.2. Arguments Object

Command line utilities typically support command line options. Those options may also be validated to ensure they are within bounds. Help output makes it easier to interact with tooling without having to look up documentation.

The bot library supports defining command line options and parameters via the picocli library. This allows applications to define a command contract, where command line flags are defined, along with their types. Defining the supported options for a command also builds up to a helpful usage output.

While picocli helps define the supported command line options for a command, arguments are validated against constraints through the use of JSR-380 annotations. Combining these two technologies creates a powerful way to both define the contract of a command and ensure that options are within the bounds that the command expects, keeping the business logic of a command free from input validation.

Command options for a command are simply classes with properties that are annotated with picocli and JSR_380 annotation.

Example Command Options
public class EchoOptions {
    @NotEmpty (1)
    @CommandLine.Parameters( (2)
        paramLabel = "Text",
        description = "Text to echo back to the user",
        arity = "1..*" (3)
    )
    private List<String> text; (4)

    public List<String> getText() {
        return text;
    }

    public void setText(List<String> text) {
        this.text = text;
    }
}
1 Declare that the text list should have more than one element using a JSR-380 annotation.
2 Define the picocli parameters for the text option.
3 Define the arity (or number of arguments) required for the parameter.
4 Property that will contain the matching arguments to the command.

To use a command options definition, the type of the arguments class should be provided to the argumentsClass property of a @SlackCommand annotation.

This document does not aim to provide documentation on the picocli library. Instead, see their documentation for a comprehensive on how to configure command line parameters.

4.3. Command Context

To provide commands details of the invocation of a specific request, a context object is built as a result of parsing the request and is optionally provided to the commands when the command execution occurs.

The SlackCommandContext provides the following details.

Table 1. Supported Parameters

Type

Description

AppMentionEvent getEvent()

A context object received from the Slack API relating to the mention event.

List<String> getArguments()

List of strings representing the arguments provided to the requested command.

Authentication getAuth()

Spring security authentication token associated with the requesting user. Will never be null and may represent an anonymous user.

? getParsedArgs()

Instance of the arguments class associated with the command. The exact implementation of the command options must match that class type configured in the @SlackCommand annotation.

CommandLine getCommandLine()

The picocli command line instance that represents the aggregate state of the parsed Slack mention request.

Defining parameter types not contained in the above table is not supported and will cause errors at startup.

4.4. Return Values

A Slack command method may return any object. Objects returned from a command method will receive further processing which may ultimately result in a response back to the user. See the Response Handling section for more details.

If a command method returns void or null, no automatic response handling will occur.

4.5. Multithreading

Slack’s Bolt API socket mode app objects contain a thread pool which services incoming events. Each incoming request from the Slack service allocates a new thread from the pool for the duration of the request. To avoid exhausting the Bolt thread pool by holding on to threads for the entire duration of a command’s execution, all commands are instead executed in a separate thread pool called the command thread pool. In this manner, the underlying Bolt thread pool should not become exhausted, and any potential thread contention will instead occur at the command thread pool. If the command thread pool becomes exhausted because the bot is receiving more commands at once that it can concurrently handle, there will be a delay in processing requests from users.

The size of the command thread pool may be customized in the application configuration.

It is up to the authors of applications using the library to determine the size of the command thread pool that the bot will require to serve its audience and its processing time requirements.

4.6. Reactive Commands

The Spring Slack Bot library supports project Reactor for commands implemented with non-blocking I/O.

For a command to be processed as a reactive stream, command methods should return instances of Mono or Flux. The framework will recognize these types, set up the Spring Security authentication in the stream’s context, and subscribe to the reactive stream.

Just as with normal, threaded commands, each non-null result from a stream will be submitted to response handlers for automatic responses to the user.

The Project Reactor documentation is a great resource to learn best practices when working with reacting streams.
Example Reactive Command
@Component
public class ExampleClass {
    @SlackCommand("foo") (1)
    public Mono<String> fooCommand() { (2)
        return ReactiveSecurityContextHolder.getContext() (3)
            .map(ctx -> ctx.getAuthentication()) (4)
            .map(auth -> "Hello, " + auth.getName()); (5)
    }
}
1 Specify that the fooCommand method is a Slack command with the name foo.
2 The Mono return type will result in the command being processed as a reactive stream.
3 Retrieve the Spring security context as a Mono<SecurityContext>.
4 Map the SecurityContext to an Authentication in a Mono<Authentication>.
5 Map the Authentication to a welcoming string mentioning the name of the user. The string will be returned from the steam and will be automatically posted to the original mention message as a thread.

4.6.1. Concurrency

Since the bot library cannot predict the behavior of streams returned from command methods, and to allow re-usability of response and error handlers, all streams are subscribed to on a bounded elastic scheduler. Streams may override this behavior through the use of publishOn.

Since streams are consumed via Mono or Flux objects returned from command methods, those methods will have already been invoked on the command thread pool. Commands that return streams will complete nearly instantaneously since the method will only construct a stream.

If the bot application only contains commands that return reactive streams, it is likely wasteful to configure the command thread pool size to be much larger than a 5 to 10 of threads. Additionally, it may be beneficial to set the minimum number of threads equal to the maximum so that threads do not need to be allocated from the underlying operating system on demand.

If the application contains a mix of threaded commands and reactive commands, authors should carefully consider the command thread pool size to both support the expected workloads and account for the efficiency of reactive streams.

4.6.2. Flux Handling

When the library subscribes to Flux streams, it aggregates all elements into Mono containing a list containing the results from each element in the stream. Those results are then individually fed back into the ResponseManagerHandler to facilitate the automatic response handling functionality.

The business logic of commands is opaque to the library, and it can make no assumption about the order of elements in a Flux. When the results are aggregated, it will process them in the order in which the stream presents them. Therefore, if the order of responses is important, it is important that streams are ordered or reduced as part of the command logic.

5. Response Handling

Content may be posted to Slack directly through the use of the Slack API client. The client may be used directly in command methods. Alternatively, as mentioned in the Commands section, command methods may return some value to be sent back to the request. The built-in handling for objects returned by commands is provided by the combination of response handlers and a response strategy.

5.1. Response Handlers

Response handlers are beans that are responsible for accepting values returned by command methods and performing some operation on them. They typically will respond to a user with the contents of objects it supports, but the specifics of how objects are handled is left to these handlers.

Response handlers may use Spring’s ordering system (either by implementing Ordered or annotation the bean with @Order).

Response handlers advertise what they may handle via a method named supports which receives the class type of the object. If this method returns true, the object will be passed to the handler for processing.

To add a custom response handler, implement the ResponseHandler<T> interface as a Spring bean, and it will automatically be registered and used.

Since the base functionality that provides responses to users operates on strings, custom ResponseHandler implementations are useful to build rich layout responses.

A default, fallback handler is used when no other handler indicates that it can handle the response type. This handler accepts any object type, calls toString() on the object, and hands the resulting string to the response strategy bean.

As an example, imagine an object type the contains a header and body that should be translated to a rich layout-based output. While this logic can also live directly in a command method, it may be moved into a response handler so that it may be used by all command methods.

public class RichContent { (1)
    private final String header;
    private final String content;

    public RichContent(String header, String content) {
        this.header = header;
        this.content = content;
    }

    public String getHeader() {
        return header;
    }

    public String getContent() {
        return content;
    }
}

@Component
public class RichContentHandler implements ResponseHandler<RichContent> { (2)
    private final SlackClientHelper slackClientHelper;

    public RichContentHandler(SlackClientHelper slackClientHelper) {
        this.slackClientHelper = slackClientHelper;
    }

    public boolean supports(Class<?> clazz) { (3)
        return RichContent.class.isAssignableFrom(clazz);
    }

    public void handle(RichContent response, CommandExecutionContext context)
            throws Exception { (4)
        List<LayoutBlock> blocks = List.of(
            HeaderBlock.builder()
                .text(
                    PlainTextObject.builder()
                        .text(response.getHeader())
                        .build()
                )
                .build(),
            SectionBlock.builder()
                .text(
                    MarkdownTextObject.builder()
                        .text(response.getContent())
                        .build()
                )
                .build()
        );

        slackClientHelper.postBlocksToChannel(response.getHeader(), blocks, context.getChannel());
    }
}
1 A simple class containing a header string and a content string.
2 Define a response handler that will process RichContent objects by implementing the ResponseHandler<RichContent> interface.
3 The supports method advertises whether the handler instance can handle the class type. In this case, return true if the class is equals to or is a subclass of RichContent.
4 Implement the handling of a RichClient object. Specifically, a rich block layout is generated and posted to the channel that Slack event originated from.

5.2. Response Strategy

The ResponseStrategy bean is responsible for sending a Slack message as a response to any message generated internally by the bot framework. This includes string-type messages generated by the default response handler.

Command methods and response handlers have the option to maintain full control over the manner by which content is posted to Slack via the Slack API. The response strategy bean offers a common and consistent way to post content when fine-grain control over user interaction is not needed or desired.

The default response strategy sends messages to the user in threaded replies to the message that originated the bot command/request. To change this behavior, applications only need to register their own implementation of the ResponseStrategy interface.

For example, a response strategy that sends a response directly to a channel instead of a thread might look like the following.

@Component
public class CustomResponseStrategy {
    private final SlackClientHelper slackClientHelper;

    public CustomResponseStrategy(SlackClientHelper slackClientHelper) {
        this.slackClientHelper = slackClientHelper;
    }

    @Override
    public void respond(String content, AppMentionEvent event) throws Exception {
        slackClientHelper.postTextToChannel(content, event.getChannel());
    }
}

6. Slack API

The library provides two methods to interact with the Slack API , depending on the degree of granularity required.

6.1. Methods Client

The MethodsClient is Slack’s Java API binding for its suite of APIs. An instance of this object is provided as a bean and may be injected as desired. The client is pre-configured with the API key required to interact with the API using the same token used to configure the underlying Slack App instance.

Slack also provides an AsyncMethodsClient, which contains the same API methods as MethodsClient. However, all API calls are handled via an internal thread pool and methods return futures. This version of the client is also exposed as a Spring bean and is suitable for reactive streams.

Learn more about Slack’s API clients in their {url-methods-client}[documentation,role=external,window=_blank].

6.2. Slack Client Helper

The SlackClientHelper bean contains several convenience methods that makes it much easier and more concise to perform common operations. It wraps the Slack MethodsClient, which is a Java API binding for the Slack API.

The bot framework itself uses this class extensively in favor using the MethodsClient.

Reactive commands should prioritize non-blocking I/O, and so an AsyncSlackClientHelper bean is also defined which may be used in a reactive stream. Where the SlackClientHelper uses Slack’s MethodsClient, the AsyncSlackClientHelper uses Slack’s AsyncMethodsClient.

7. Error Handling

As with most applications, errors are inevitable. An application that receives a request from a user but does not respond when an error occurs is a bad user experience. To avoid this, the library contains error handlers which implement the logic of how to respond to a user when an error occurs.

All error handlers utilize the ResponseStrategy bean that abstracts the logic of sending content to the user in Slack.

7.1. Command Error Handler

Ideally, command methods should handle their own error handling logic in order to provide appropriate and accurate feedback to users. In order for the library continue to function if errors leak from command methods, all errors stemming from command invocations are passed to a CommandErrorHandler.

A built-in, default error handler is defined that provides a generic error message back to the user when errors in commands occur. Additionally, the default error handler will unwrap and handle InvocationTargetException errors (which are thrown as part of reflectively executing command methods) to handle the actual error thrown by the command method. This also captures AccessDeniedException errors thrown by the Spring Framework security chain when users attempt to execute a command they do not have access to.

If a change in the behavior of this feedback is desired, applications may implement their own version of the CommandErrorHandler bean instead.

7.2. Mention Error Handler

A MentionHandlerErrorHandler bean is responsible for handling errors thrown by the internals of the bot library. It handles both errors that result from parsing and invoking commands and responding to the user command usage.

While applications may replace this bean by defining their own implementation of MentionHandlerErrorHandler, it is not recommended.

8. Spring Security

The library includes a security configuration that secures command methods with Spring Security.

8.1. Method Security

The security configuration contained in the library enables global method security. This allows authors to annotate secured methods with authorization criteria.

Individual command methods may be annotated with @PreAuthorize, and the criteria contained within the annotation must be met for the user to be granted access to the command.

As an example, the following command implementation requires the user to have the role ROLE_FOO in order to be able to invoke the command.

@Component
public class ExampleCommandGroup {
    @SlackCommand("foo")
    @PreAuthorize("hasRole('ROLE_FOO')") (1)
    public String fooCommand(Authentication authentication) {
        return "Hello, " + authentication.getName();
    }
}
1 The @PreAuthorize annotation specifies that the user must have been assigned the ROLE_FOO role to gain access to the command.

8.2. User Configuration

The SlackAuthProvider bean is a Spring Security authentication provider that gets registered in the security configuration. It specifically authenticates users by their Slack ID, which it accomplishes through the use of a SlackUserDetailsService.

The SlackUserDetailsService specifically returns instances of SlackUser as its user details. The default implementation provided by the bot library uses the application’s configuration file as a source for Slack user details.

Users are configured under the slack.security.users key. This is a mapped key, where the next key is the Slack ID of a user, and additional properties underneath the users pertain to that user.

The following examples should illustrate how to configure users who will be authenticated.

application.properties
slack.security.users.U12345.authorities=ROLE_FOO,ROLE_BAR (1) (2)
1 U12345 is an example Slack user ID.
2 The authorities list is formatted as comma separated values.
application.yml
slack:
  security:
    users:
      U12345: (1)
        authorities: (2)
          - ROLE_FOO
          - ROLE_BAR
1 U12345 is an example Slack user ID. The ID may be retrieved through Slack.
2 A list of Spring Security roles to assign to the user.

8.2.1. Custom User Lookup

If an application requires the security scheme provided by the library, but needs to look users up by some means other than a configuration file (e.g. database, etc), applications may provide their own implementations of SlackUserDetailsService. Custom beans implementing this interface will take precedence over the bundled, config-based implementation.

8.3. Security Context

Before invoking command methods, authentication tokens are provided to and stored in a security context to make it available to both the command’s execution and to the Spring Security framework. Commands do not need to implement any logic around authenticating users.

For standard, threaded commands, the authentication token is stored in the SecurityContextHolder. This makes the authentication available to the current thread for the duration of the request.

If a command manually spawns a new thread as part of its business logic, it is the responsibility of the custom logic to set up the SecurityContextHolder again.

For reactor-based commands, the authentication token is configured in the ReactiveSecurityContextHolder, and subsequently injected into the reactive stream’s context.

security

9. Changelog

2.0.0
  • Migrated project to be compatible with Spring Boot 3.x. This breaks compatibility with previous versions of Spring Boot.

  • Migrated project to JDK 17. Note that since javax packages have been migrated to jakarta, older versions of Java are no longer supported.

  • Updated several underlying libraries.

1.1.2
  • Fixed issue where telephone number links were not being parsed correctly.

  • Add new configuration (slack.cli.strip-phone-link) which reverts the telephone number link back to arguments.

1.1.1
  • Update Spring Boot to version 2.6.9 to remediate Spring4Shell vulnerability.

  • Update Spring Slack to version 1.0.3 to remediate Spring4Shell vulnerability.

1.1.0
  • Add a new configuration (slack.cli.convert-quotes) that will convert fancy quotes to their basic ASCII equivalents. As with other commandline interfaces, quotes may be used to group arguments that contain spaces into a single argument. Defaults to true.

  • Add better error messaging when argument quotes are unbalanced and a request is unable to be parsed.

  • Update documentation to be explicit about parent/child relationships when Slack command methods are defined in a class also annotated with @SlackCommand.

1.0.1
  • Updated documents with missing Slack account requirements.

  • Added slightly better error handling when the framework is unable to retrieve a bot account’s name.

1.0.0
  • First version of the library.