Делаем своего Slack бота на Go

18 minute read

Перевод статьи "Build your own slack bot in Go"

Боты для Slack это довольно веселая и простая в написании штука. В этой статье мы посмотрим, как написать одного из таких ботов.

Вот что сможет делать наш свеженаписанный бот:

Для создания и использования ботов вам нужен Slack аккаунт с высокими привилегиями(для включения ботов) и немного знаний по Go.

Чтобы добавить нового бота, вам нужно пройти по ссылке https://YOURORG.slack.com/services/new/bot(вместо YOURORG нужно подставить ваш реальный домен), затем выбрать имя. Не забудьте сохранить токен для доступа к API, позже он нам очень пригодится.

После всех этих, манипуляций вы увидите имя бота в списке пользователей(в состоянии залогинен). По умолчанию бот будет добавлен в канал #general.

Теперь нужно скачать код с github. Мы будем использовать его для понимания основных принципов.

go get github.com/rapidloop/mybot

После выполнения команды, у вас появится бинарный файл в папке $GOPATH/bin. Запускаем и смотрим, что получится:

$ mybot
usage: mybot slack-bot-token
$ mybot xxxx-99999999999-USE.THE.TOKEN.FROM.ABOVE
mybot ready, ^C exits

теперь mybot готов показывать вам котировки акций, как показано на скриншоте выше. Когда наиграетесь с ботом, достаточно нажать комбинацию клавиш CTR+C для выхода из программы.

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

Код

Предупреждение: Код приложения mybot используется только для иллюстрации возможностей, в нем нет всех проверок ошибок и многого другого. Не используйте его в реальных проектах!

Для начала, нам нужно использовать токен для запуска сессии Real Time Message API. Для этого используем rtm.start API. Как сказано в документации, нам нужно передать токен как параметр при вызове rtm.start и в ответ мы получим целую кучу всякой всячины. Из этого всего нам нужна только пара вещей.

Пример того, как мы делаем вызов (файл slack.go, метод slackStart)

url := fmt.Sprintf("https://slack.com/api/rtm.start?token=%s", token)
resp, err := http.Get(url)
//...
body, err := ioutil.ReadAll(resp.Body)
//...
var respObj responseRtmStart
err = json.Unmarshal(body, &respObj)

В этом куске кода мы делаем HTTP GET запрос, читаем тело ответа и декодируем его как JSON в отдельный объект responseRtmStart. Как было сказано выше, нам нужно всего несколько вещей. Метод Unmarshal из пакета encoding/json позволяет игнорировать ненужные поля, что позволяет определить структуру, состоящую только из необходимых полей:

type responseRtmStart struct {
    Ok    bool         `json:"ok"`
    Error string       `json:"error"`
    Url   string       `json:"url"`
    Self  responseSelf `json:"self"`
}

type responseSelf struct {
    Id string `json:"id"`
}

Поле Url это специальный Websocket URL к которому мы можем подключится для запуска сессии RTM. Также, нам нужен параметр Id, который является уникальным идентификатором нашего бота. Он должен выглядеть примерно так: U1A2B3C4D. В будущем, мы будем использовать этот идентификатор для получения всех упоминаний.

Используем API для работы с сообщениями в реальном времени

Специальные Go библиотеки golang.org/x включают в себя пакет для работы с веб-сокетами: golang.org/x/net/websocket. В этом пакете есть JSON кодек, который предоставляет обертку для отправки и получения JSON объектов через веб-сокеты.

Запускаем websocket соединение с использованием этого пакета (файл slack.go, метод slackConnect):

ws, err := websocket.Dial(wsurl, "", "https://api.slack.com/")

Через это соединение, Slack сообщает нам [почти обовсем]https://api.slack.com/rtm), что происходит в чатах. Эти нотификации включают в себя и сообщения, которые пользователи отправляют в каналы, и сообщения, которые пользователи отправляют лично нашему боту. Личные сообщения нам особенно интересны.

Каждая нотификация - это JSON объект, котрый содержит поле type. Нам нужны нотификации, у которых в этом поле указан тип message. Как и раньше, мы можем получить значения из нужных полей JSON объекта с помощью правильной структуры (файл slack.go):

type Message struct {
  Id      uint64 `json:"id"`
  Type    string `json:"type"`
  Channel string `json:"channel"`
  Text    string `json:"text"`
}

Для работы с веб-сокет соединениями, нам нужен вот такой код(файл slack.go):

func getMessage(ws *websocket.Conn) (m Message, err error) {
    err = websocket.JSON.Receive(ws, &m)
    return
}

Для отправки сообщений в канал нам нужно записать JSON объект обратно в веб-сокет. Обратите внимание, нет никакой возможности отвечать на сообщения. Все что мы можем - это отправлять сообщения в канал. Для каждого сообщения нам нужен уникальный идентификатор. Для этого будем использовать атомарный инкремент счетчика.

var counter uint64

func postMessage(ws *websocket.Conn, m Message) error {
  m.Id = atomic.AddUint64(&counter, 1)
  return websocket.JSON.Send(ws, m)
}

Главный цикл

Давайте теперь соберем все вместе. Напишем главный цикл, который будет выглядеть как-то так (файл main.go):

ws, id := slackConnect(token)

for {
    msg, err := getMessage(ws)
    // ...
    // обрабатываем сообщение и отвечаем при необходимости
    // ...
    postMessage(ws, reply)
}

Мы читаем все сообщения, но нам нужны только те, в которых упомянули нашего бота. Slack хранить все упоминания пользователя как строки вида <@U1A2B3C4D>(U1A2B3C4D это идентификатор пользователя, которого упомянули) в тексте сообщения. Таким образом, сообщение вида "@mybot: stock goog" превратиться в "<@U1A2B3C4D>: stock goog". Мы можем использовать эту информацию для распознавания упоминаний нашего бота.

if m.Type == "message" && strings.HasPrefix(m.Text, "<@"+id+">") {

И пример обработки самого сообщения:

parts := strings.Fields(m.Text)
if len(parts) == 3 && parts[1] == "stock" {
    // получаем содержимое второй части сообщения через parts[2]
} else {
    m.Text = fmt.Sprintf("sorry, i didn't get that\n")
    postMessage(ws, m)
}

Для обработки сообщения и формирования ответа используем go-рутину:

go func(m Message) {
    m.Text = getQuote(parts[2])
    postMessage(ws, m)
}(m)

Метод getQuote получает данные котировок по указанному тиккеру используя Yahoo API. Конечно же, вы можете заменить этот функционал.

Вот несколько вещей, которые может делать бот и неописанных в этой статье.

Удачи вам в создании новых Slack ботов. Вот еще чутка ссылок, которыебудут вам интересны: