Пишем простого slack-бота на Java

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

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

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

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

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

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

Выбираем From scratch. Вариант с манифестом пока выглядит недоработанным.

image-loader.svg

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

image-loader.svg

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

Теперь добавим права нашему боту. В том же меню ниже.

image-loader.svg

slack экспериментирует с интерфейсом, поэтому вид может вскоре измениться.

Общий алгоритм работы с правами

Зайдите в документацию требуемого метода. 

image-loader.svg

Вам могут понадобиться не все разрешения метода. Лучше прочитать про каждое отдельно. Для нашего примера будет достаточно chat: write. Все разрешения.

Запускаем приложение

pom.xml



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.6.0
    
    ru.hh.example
    slack-example
    0.0.1-SNAPSHOT
    slack-example

    
        11
        1.13.0
    

    
        
            org.springframework.boot
            spring-boot-starter
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        

        
        
            com.slack.api
            bolt
            ${slack.bolt.version}
        
        
            com.slack.api
            bolt-servlet
            ${slack.bolt.version}
        

        
        
            com.slack.api
            bolt-socket-mode
            ${slack.bolt.version}
        
        
            org.glassfish.tyrus.bundles
            tyrus-standalone-client
            1.9
        
        
            javax.websocket
            javax.websocket-api
            1.1
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    

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 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 Слака предполагается, что он отправляет запрос в ваше приложение, когда получает команду (действие) от пользователя.

image-loader.svg


Добавляем команду в »лк» слака

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

image-loader.svg

Доступ к боту можно получить через стандартного  Slackbot, либо добавив бота к себе в приложения. Если работать через Slackbot , то команда должна быть уникальной в пространстве вашей компании. Описание команд тут.

Последний штрих в личном кабинете — Install App и Reinstall to Workspace. 

Проверяем как работает бот:

image-loader.svg

Метод sendMessage

Можно вызвать локально. Он показывает, как можно работать без действий юзера. Например, для отправки сообщений из системы мониторинга.

curl -X POST http://localhost:8080/send-to-chat

image-loader.svg

Получили сообщение в канале

Что еще можно запилить

Slack предоставляет несколько api:

  • Webhook — самый простой вариант. Хорош тем, что ваше приложение может ничего не знать про slack и просто дергать его по http. Мы используем его для отправки алертов из okmeter и из самописных скриптов деплоймента сервисов;

  • Conversations api имеет более широкие возможности, но требует больше усилий для входа;  

  • Events API необходим для работы с командами и другими событиями из Cлака (например вход в канал, отправка определенного текста). Есть два способа взаимодействия — web, как был использован в примере и socket. Пожалуй, в следующий раз я бы использовал socket, поскольку он не требует выставления вашего сервиса наружу.

Не затронул большой раздел про оформление сообщений от бота. Почитать здесь

И еще эксперимент с socket mode

В моем боте он пока не нужен, но было интересно разобраться

В меню Socket Mode

Выставить enable

image-loader.svg

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

image-loader.svg

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

image-loader.svg

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

В меню Event Subscriptions

image-loader.svgSocketExample.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 — получили несколькими строками выше

Можно проверять

image-loader.svg

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

image-loader.svg

Из неочевидных особенностей — через сокет нельзя совершать любые действия. Только отвечать на событие. А все остальное нужно делать через web api

Больше примеров здесь и здесь

PS: Если не приходят определенные ивенты, возможно вы не подписались на них в разделе Event Subscriptions(я потратил какое то время, чтобы это понять))

В заключение 

Наши слак боты позволяют автоматизировать работу, упростить выход новых сотрудников, добавить ламповости в каналы.

Самый популярный бот — для работы с тестовой инфраструктурой. Он может отдавать текущий статус стендов и позволяет их «бронировать». А еще рассылает уведомления, если что-то пошло не так: с тестами, релизами etc.

image-loader.svgimage-loader.svg

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

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

image-loader.svg

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

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

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

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

© Habrahabr.ru