Привет! Я — Саша Казанцев, разработчик в hh.ru. В статье я расскажу, как сделать простого бота в Slack на java и немного о других вариантах использования slack api.
Слак обладает обширной и всеобъемлющей документацией и туториалами, и чтобы написать эталонного бота, лучше прочитать вообще все. Но поскольку у нас лапки, поэтому запилим по-простому.
Структура будет интересна тем, кому лень читать лишнее: в самом начале будет инструкция по созданию бота, а после — лирика про наших.

Кручу ручку, пишу бота
Без лишних слов и прелюдий перейдем сразу к делу. Сначала создадим приложение в “личном кабинете”. Больше деталей в официальном туториале.
Выбираем From scratch. Вариант с манифестом пока выглядит недоработанным.

Дальше нужно заполнить имя и workspace.

Из меню OAuth & Permissions забираем Bot User OAuth Token вида xoxb-YOUR-TOKEN. Он пригодится нам позже.
Теперь добавим права нашему боту. В том же меню ниже.

Slack экспериментирует с интерфейсом, поэтому вид может вскоре измениться.
Общий алгоритм работы с правами
Зайдите в документацию требуемого метода.

Вам могут понадобиться не все разрешения метода. Лучше прочитать про каждое отдельно. Для нашего примера будет достаточно chat:write. Все разрешения.
Запускаем приложение
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.0</version> </parent> <groupId>ru.hh.example</groupId> <artifactId>slack-example</artifactId> <version>0.0.1-SNAPSHOT</version> <name>slack-example</name> <properties> <java.version>11</java.version> <slack.bolt.version>1.13.0</slack.bolt.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- slack base and web--> <dependency> <groupId>com.slack.api</groupId> <artifactId>bolt</artifactId> <version>${slack.bolt.version}</version> </dependency> <dependency> <groupId>com.slack.api</groupId> <artifactId>bolt-servlet</artifactId> <version>${slack.bolt.version}</version> </dependency> <!-- socket mode--> <dependency> <groupId>com.slack.api</groupId> <artifactId>bolt-socket-mode</artifactId> <version>${slack.bolt.version}</version> </dependency> <dependency> <groupId>org.glassfish.tyrus.bundles</groupId> <artifactId>tyrus-standalone-client</artifactId> <version>1.9</version> </dependency> <dependency> <groupId>javax.websocket</groupId> <artifactId>javax.websocket-api</artifactId> <version>1.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
SlackController.java
package ru.hh.example.slack; import com.slack.api.Slack; import com.slack.api.methods.MethodsClient; import com.slack.api.methods.SlackApiException; import com.slack.api.methods.response.chat.ChatPostMessageResponse; import com.slack.api.methods.response.conversations.ConversationsListResponse; import com.slack.api.model.Conversation; import java.io.IOException; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class SlackController { private static final Logger LOGGER = LoggerFactory.getLogger(SlackController.class); @PostMapping(path = "/", consumes = {APPLICATION_FORM_URLENCODED_VALUE}) public String slackCommand(@RequestParam Map<String, String> slackRequest) { LOGGER.info("slackRequest: {}", slackRequest); return "hello " + slackRequest.get("user_name"); } @PostMapping(path = "/send-to-chat") public void sendMessage() throws SlackApiException, IOException { // https://api.slack.com/authentication/oauth-v2 String token = "xoxb-YOUR-TOKEN"; String channelName = "YOUR-CHANNEL-NAME"; String message = "hello"; var client = Slack.getInstance().methods(token); var channelId = findChannel(client, channelName); ChatPostMessageResponse chatPostMessageResponse = postMessage(client, channelId, message); LOGGER.info("chatPostMessageResponse.isOk() : {}", chatPostMessageResponse.isOk()); } // https://api.slack.com/messaging/retrieving#finding_conversation private String findChannel(MethodsClient client, String channelName) throws SlackApiException, IOException { String nextCursor = null; do { var result = getConversationsList(client, nextCursor); nextCursor = result.getResponseMetadata().getNextCursor(); for (Conversation channel : result.getChannels()) { if (channel.getName().equalsIgnoreCase(channelName)) { return channel.getId(); } } } while (nextCursor != null); throw new IllegalStateException(); } private ConversationsListResponse getConversationsList(MethodsClient client, String nextCursor) throws SlackApiException, IOException { return client.conversationsList(r -> r.cursor(nextCursor)); } // https://api.slack.com/messaging/sending#publishing private ChatPostMessageResponse postMessage(MethodsClient client, String conversationId, String message) throws SlackApiException, IOException { return client.chatPostMessage(r -> r.channel(conversationId).text(message)); } }
Нужно прописать token (Bot User OAuth Token) и имя тестового канала (channelName).
Для того, чтобы Слак смог достучаться до вашего сервиса, можно запустить его на хостинге или для простоты поднять локально ngrok.
./ngrok $PORT
Spring boot по умолчанию поднимается на 8080.
Зачем сервису “торчать наружу”? В web api Слака предполагается, что он отправляет запрос в ваше приложение, когда получает команду (действие) от пользователя.

Добавляем команду в “лк” слака
В меню Slash Commands. Нужно прописать имя команды и url сервиса. От вашего сервера или ngrok.

Доступ к боту можно получить через стандартного Slackbot, либо добавив бота к себе в приложения. Если работать через Slackbot, то команда должна быть уникальной в пространстве вашей компании. Описание команд тут.
Последний штрих в личном кабинете — Install App и Reinstall to Workspace.
Проверяем как работает бот:

Метод sendMessage
Можно вызвать локально. Он показывает, как можно работать без действи�� юзера. Например, для отправки сообщений из системы мониторинга.
curl -X POST http://localhost:8080/send-to-chat

Получили сообщение в канале.
Что еще можно запилить
Slack предоставляет несколько api:
Webhook — самый простой вариант. Хорош тем, что ваше приложение может ничего не знать про slack и просто дергать его по http. Мы используем его для отправки алертов из okmeter и из самописных скриптов деплоймента сервисов;
Conversations api имеет более широкие возможности, но требует больше усилий для входа;
Events API необходим для работы с командами и другими событиями из Cлака (например вход в канал, отправка определенного текста). Есть два способа взаимодействия — web, как был использован в примере и socket. Пожалуй, в следующий раз я бы использовал socket, поскольку он не требует выставления вашего сервиса наружу.
Не затронул большой раздел про оформление сообщений от бота. Почитать можно здесь.
И еще эксперимент с socket mode
В моем боте он пока не нужен, но было интересно разобраться
В меню Socket Mode
Выставить enable

Алиас для токена:

Забрать токен (нужно будет подложить приложению):

Теперь нужно подписаться на ивенты. Для моего примера нужно событие app_mention.
В меню Event Subscriptions

SocketExample.java
package ru.hh.example.slack; import com.slack.api.bolt.App; import com.slack.api.bolt.AppConfig; import com.slack.api.bolt.socket_mode.SocketModeApp; import com.slack.api.model.event.AppMentionEvent; public class SocketExample { public static void main(String[] args) throws Exception { String botToken = "xoxb-YOUR-BOT-TOKEN"; String appToken = "xapp-APPTOKEN"; AppConfig appConfig = AppConfig.builder().singleTeamBotToken(botToken).build(); App app = new App(appConfig); app.event(AppMentionEvent.class, (req, ctx) -> { System.out.println("HI! I received event!" + req.getEvent()); ctx.say("I was mentioned by user with id " + req.getEvent().getUser()); return ctx.ack(); }); SocketModeApp socketModeApp = new SocketModeApp(appToken, app); // #start() method establishes a new WebSocket connection and then blocks the current thread. // If you do not want to block this thread, use #startAsync() instead. socketModeApp.start(); } }
Нужно поменять две переменные
xoxb-YOUR-BOT-TOKEN — токен из первого раздела.
xapp-APPTOKEN — получили несколькими строками выше.
Можно проверять:

Схема взаимодействия через socket api:

Из неочевидных особенностей — через сокет нельзя совершать любые действия. Только отвечать на событие. А все остальное нужно делать через web api.
Больше примеров здесь и здесь.
P.S.: Если не приходят определенные ивенты, возможно вы не подписались на них в разделе Event Subscriptions (я потратил какое то время, чтобы это понять).
В заключение
Наши слак боты позволяют автоматизировать работу, упростить выход новых сотрудников, добавить ламповости в каналы.
Самый популярный бот - для работы с тестовой инфраструктурой. Он может отдавать текущий статус стендов и позволяет их “бронировать”. А еще рассылает уведомления, если что-то пошло не так: с тестами, релизами etc.


Есть более “камерные” боты, например троттлер уведомлений или планинг покер. Всего десятка полтора.
Программисты же любят создавать свои ванильные велосипеды.

И напоследок мой личный рейтинг ботов:
