1. Installation

repositories {
    mavenCentral()
}

dependencies {
    // for sending messages and other simple integration
    implemenation 'com.agorapulse:micronaut-slack-core:2.0.0'

    // for providing own webhooks, event handlers and OAuth workflow
    implemenation 'com.agorapulse:micronaut-slack-http:2.0.0'
    implemenation 'io.micronaut:micronaut-http-server-netty'
}
Install Micronaut Cache and configure slack-events cache for more sophisticated duplicate events protection.

2. Introduction

Micronaut Slack is more idiomatic alternative to Bolt Micronaut library for Slack API integration into the Micronaut applications.

The main difference compared to Bolt Micronaut

  • Using Micronaut configuration chain to configure Bolt’s AppConfig

  • Main bolt classes such as Slack, App and MethodsClient available as beans

  • Ability to define handlers as standalone beans out of the box

  • Beans to list all installed applications

  • Events for pre/post bot installation

  • Preventing Bolt event duplicate handling

3. Examples

This documentation contains example application manifest files which helps you to create the Slack Application. See more Create and configure apps with manifests. Visit Create an app > From an app manifest to create new application using the manifest.

You can manage your Slack applications at Your Apps page.

Some examples require that your application is accessible from the internet. Free and easy way is to use Localtunnel utility. Don’t forget to change the URLs when you deploy your applications to production. You can install Localtunnel using the following commands

# if you don't have NodeJS installed, you can use NVM to install the latest distribution
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
# install Localtunnel as a global lt command
npm install -g localtunnel
# use localtunnel, your app will be available as https://your-custom-subdomain.loca.lt
lt -p 8080 -s your-custom-subdomain

3.1. Sending Messages to Slack

The most simple example of the application is the one that only sends messages to the Slack workspace. You can use a following manifest to create such application:

display_information:
  name: Micronaut Messenger
features:
  bot_user:
    display_name: Micronaut Messenger
    always_online: false
oauth_config:
  scopes:
    bot:
      - channels:read
      - chat:write
      - chat:write.public
settings:
  org_deploy_enabled: false
  socket_mode_enabled: false
  token_rotation_enabled: false

The only requirement for the application is to have the bot token configured. You get the token once you install your application into your workspace under Settings > Install App. Then provide the token either as SLACK_BOT_TOKEN environment variable or in the application.yml.

slack:
  bot-token: xoxb-bot-token

You can inject MessagesClient or AsyncMessagesClient into your beans to perform operations against Slack API.

package com.agorapulse.slack.example.sender;

import com.slack.api.methods.MethodsClient;
import com.slack.api.methods.SlackApiException;
import com.slack.api.methods.response.conversations.ConversationsListResponse;
import com.slack.api.model.Conversation;

import jakarta.inject.Singleton;
import java.io.IOException;
import java.util.Optional;

@Singleton
public class MessageSender {

    private final MethodsClient methods;

    public MessageSender(MethodsClient methods) {                                       (1)
        this.methods = methods;
    }

    public void sendMessage(String message) throws SlackApiException, IOException {
        ConversationsListResponse conversations = methods.conversationsList(b -> b);
        Optional<Conversation> generalChannel = conversations                           (2)
            .getChannels()
            .stream()
            .filter(Conversation::isGeneral)
            .findAny();
        if (generalChannel.isPresent()) {
            methods.chatPostMessage(m -> m.
                text(message).channel(generalChannel.get().getId())                     (3)
            );
        }
    }

}
1 Inject MethodsClient
2 Find the proper destination channel
3 Post a simple message into the channel

3.2. Listening to Commands

Another type of application is the one that handles execution of the slash commands. You will need to make your application accessible from the internet. Free and easy way is to use Localtunnel utility. Don’t forget to change the URLs when you deploy your applications to production.

Create new application from the following manifest:

display_information:
  name: Micronaut Commander
features:
  bot_user:
    display_name: Micronaut Commander
    always_online: false
  slash_commands:
    - command: /commander
      # npm install -g localtunnel
      # lt -p 8080 -s micronaut-commander
      url: https://micronaut-commander.loca.lt/slack/events
      description: Micronaut Commander
      usage_hint: ahoy
      should_escape: false
oauth_config:
  scopes:
    bot:
      - commands
settings:
  org_deploy_enabled: false
  socket_mode_enabled: false
  token_rotation_enabled: false
The subdomain used with lt -s must be the same as the one in the application settings.

Package com.agorapulse.slack.handlers contains various interfaces that you can implement to get self-registered handlers of various Slack events. For handling Slash command events we need to implement MicronautSlashCommandHandler:

package com.agorapulse.slack.example.commander;

import com.agorapulse.slack.handlers.MicronautSlashCommandHandler;
import com.slack.api.bolt.context.builtin.SlashCommandContext;
import com.slack.api.bolt.request.builtin.SlashCommandRequest;
import com.slack.api.bolt.response.Response;
import io.micronaut.core.util.StringUtils;

import jakarta.inject.Singleton;

@Singleton
public class CommandHandler implements MicronautSlashCommandHandler {                   (1)

    @Override
    public String getCommandId() {
        return "/commander";                                                            (2)
    }

    @Override
    public Response apply(
        SlashCommandRequest slashCommandRequest,
        SlashCommandContext context
    ) {
        String salutation = StringUtils.capitalize(slashCommandRequest.getPayload().getText());
        return context.ack(salutation + " to you, sailor!");                            (3)
    }


}
1 Implement MicronautSlashCommandHandler bean
2 The slash command to trigger the handler
3 Use the context object for immediate response

3.3. Interactive Installed Application

Create a new application from the following manifest:

display_information:
  name: Micronaut Interactive Messenger
features:
  bot_user:
    display_name: Micronaut Interactive Messenger
    always_online: false
oauth_config:
  redirect_urls:
    - https://micronaut-interactive-messenger.loca.lt/slack/oauth_redirect
  scopes:
    user:
      - reactions:read
    bot:
      - channels:read
      - chat:write
      - chat:write.public
settings:
  event_subscriptions:
    request_url: https://micronaut-interactive-messenger.loca.lt/slack/events
    user_events:
      - reaction_added
  interactivity:
    is_enabled: true
    request_url: https://micronaut-interactive-messenger.loca.lt/slack/events
  org_deploy_enabled: false
  socket_mode_enabled: false
  token_rotation_enabled: false
The subdomain used with lt -s must be the same as the one in the application settings.

To distribute your application you need active public distribution under Settings > Manage Distributions. Then you can find all the secrets on the home page of your Slack application. Fill the secrets in your application.yml file.

slack:
  signing-secret: s3cret                                                                (1)
  scope: channels:read,chat:write,chat:write.public                                     (2)
  oauth-install-path: /slack/install                                                    (3)
  oauth-redirect-uri-path: /slack/oauth_redirect                                        (4)
  client-id: the-client-id                                                              (5)
  client-secret: the-client-secret                                                      (6)
1 Signing secret to verify incoming events
2 OAuth scopes to be requested
3 OAuth install path, use /slack/install unless you define your own controller
4 OAuth redirect path, use /slack/oauth_redirect unless you define your own controller
5 Slack application client ID
6 Slack application client secret

You can’t simply use the injected MethodsClient when working with distributed application. Use DistributedAppMethodsClientFactory to create the authenticated instance instead. Here is the example of a bean which posts messages to every installed workspace after the application is started.

package com.agorapulse.slack.example.sender.interactive;

import com.agorapulse.slack.install.enumerate.InstallationEnumerationService;
import com.agorapulse.slack.oauth.DistributedAppMethodsClientFactory;
import com.slack.api.bolt.model.Bot;
import com.slack.api.methods.MethodsClient;
import com.slack.api.methods.SlackApiException;
import com.slack.api.methods.response.conversations.ConversationsListResponse;
import com.slack.api.model.Conversation;
import io.micronaut.runtime.event.ApplicationStartupEvent;
import io.micronaut.runtime.event.annotation.EventListener;

import jakarta.inject.Singleton;
import java.io.IOException;
import java.util.List;
import java.util.Optional;

import static com.slack.api.model.block.Blocks.actions;
import static com.slack.api.model.block.Blocks.header;
import static com.slack.api.model.block.composition.BlockCompositions.plainText;
import static com.slack.api.model.block.element.BlockElements.button;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;

@Singleton
public class MessageSender {

    private final DistributedAppMethodsClientFactory factory;                           (1)
    private final InstallationEnumerationService enumerationService;                    (2)

    public MessageSender(
        DistributedAppMethodsClientFactory factory,
        InstallationEnumerationService enumerationService
    ) {
        this.factory = factory;
        this.enumerationService = enumerationService;
    }

    @EventListener
    public void sendMessage(ApplicationStartupEvent event) throws SlackApiException, IOException {
        List<Bot> allBots = enumerationService.findAllBots().collect(toList());         (3)
        for (Bot bot : allBots) {
            MethodsClient methods = factory
                .createClient(bot)                                                      (4)
                .orElseThrow(() -> new IllegalStateException("Should not happen"));

            ConversationsListResponse channels = methods.conversationsList(b -> b);     (5)
            Optional<Conversation> generalChannel = channels
                .getChannels()
                .stream()
                .filter(Conversation::isGeneral)
                .findAny();

            if (generalChannel.isPresent()) {
                methods.chatPostMessage(m -> m                                          (6)
                    .channel(generalChannel.get().getId())
                    .blocks(asList(
                        header(b -> b.text(plainText("Micronaut is awesome, WDYT?"))),  (7)
                        actions(asList(
                            button(b -> b.actionId("#mn-yes").text(plainText("Yes"))),  (8)
                            button(b -> b.actionId("#mn-no").text(plainText("No")))
                        ))
                    ))
                );
            }
        }
    }

}
1 DistributedAppMethodsClientFactory allows you to create MethodsClient authenticated against given Slack team
2 InstallationEnumerationService allows you to enumerate all current bot installations
3 Collect all existing Bot configurations
4 Create authenticated MethodsClient
5 List all channels in the team
6 Post a chat message using the builder
7 Create plain text header
8 Add some interactive buttons

You need to implement MicronautBlockActionHandler to handle the interactive actions.

package com.agorapulse.slack.example.sender.interactive;

import com.agorapulse.slack.handlers.MicronautBlockActionHandler;
import com.slack.api.app_backend.interactive_components.payload.BlockActionPayload;
import com.slack.api.bolt.context.builtin.ActionContext;
import com.slack.api.bolt.request.builtin.BlockActionRequest;
import com.slack.api.bolt.response.Response;

import jakarta.inject.Singleton;
import java.io.IOException;
import java.util.Optional;
import java.util.regex.Pattern;

@Singleton
public class MessageActionHandler implements MicronautBlockActionHandler {              (1)

    @Override
    public Pattern getActionIdPattern() {
        return Pattern.compile("#mn-.*");                                               (2)
    }

    @Override
    public Response apply(
        BlockActionRequest blockActionRequest,
        ActionContext context
    ) throws IOException {
        Optional<BlockActionPayload.Action> firstAction = blockActionRequest            (3)
            .getPayload()
            .getActions()
            .stream()
            .findFirst();

        if (firstAction.isPresent()) {
            BlockActionPayload.Action action = firstAction.get();
            if ("#mn-yes".equals(action.getActionId())) {                               (4)
                context.respond("Same do I!");                                          (5)
            } else if ("#mn-no".equals(action.getActionId())) {
                context.respond("I'm sorry to hear that!");
            } else {
                context.respond("I don't know what you mean!");
            }
        }
        return context.ack();
    }

}
1 Implement MicronautBlockActionHandler interface
2 Specify the pattern to match the action ids
3 Get the first matched action
4 Check the actual action ID
5 Respond with immediate message

3.3.1. Using S3 Storage

OAuth-enabled application can store information about the OAuth in Amazon S3 bucket. The application requires having slack.bucket property set and AmazonS3 bean must be available. The easiest solution is to add Micronaut AWS SDK Simple Storage Service for AWS SDK v1 on the classpath. You also need to have the AWS credentials set up correctly and the bucket must already exist.

build.gradle
dependencies {
    implementation 'com.agorapulse:micronaut-aws-sdk-s3:2.0.4-micronaut-3.0'
}
application.yml
aws:
    access-key: AMAZON_KEY_ID
    secret-key: AWS_SECRET_KEY_ABCDEF

slack:
    bucket: mydata.example.com
# the rest of slack configuration (see above)