Привет! Я — Саша Казанцев, разработчик в 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.
Есть более “камерные” боты, например троттлер уведомлений или планинг покер. Всего десятка полтора.
Программисты же любят создавать свои ванильные велосипеды.
И напоследок мой личный рейтинг ботов: