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

Слак обладает обширной и всеобъемлющей документацией и туториалами, и чтобы написать эталонного бота, лучше прочитать вообще все. Но поскольку у нас лапки, поэтому запилим по-простому. 

Структура будет интересна тем, кому лень читать лишнее: в самом начале будет инструкция по созданию бота, а после — лирика про наших.

Slack тебе в руки!
Slack тебе в руки!

Кручу ручку, пишу бота

Без лишних слов и прелюдий перейдем сразу к делу. Сначала создадим приложение в “личном кабинете”. Больше деталей в официальном туториале.

Выбираем 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.

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

Программисты же любят создавать свои ванильные велосипеды.

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

  • Polly — позволяет создавать красивые опросы;

  • Zoom и Teams боты  делают жизнь чуть проще;

  • Disco - имитация корпоративного общения.

Комментарии (0)