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
andMethodsClient
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.
dependencies {
implementation 'com.agorapulse:micronaut-aws-sdk-s3:2.0.4-micronaut-3.0'
}
aws:
access-key: AMAZON_KEY_ID
secret-key: AWS_SECRET_KEY_ABCDEF
slack:
bucket: mydata.example.com
# the rest of slack configuration (see above)