По мере развития вашей карьеры разработчика есть одна вещь, с которой, как мне кажется, могут столкнуться все старшие разработчики. Чем больше опыта и "выше по карьерной лестнице", тем меньше времени остается на то, чтобы просто писать код. Работа штатным инженером означает, что вы работаете по совершенно другому графику над совершенно другими задачами. Просто загляните на потрясающий сайт staffeng.com и особенно в статью об архетипах сотрудников.
Сейчас я трачу гораздо меньше времени на кодинг, чем когда начинал работать, но в дни Hackdays вы даже не сможете оторвать меня от клавиатуры, вбивая код. В Schibsted мы серьезно относимся к Hackdays. Два раза в год мы проводим общекорпоративные Hackdays и даже объединяем их в большие фестивали Product & Tech Festivals с известными внешними спикерами, часами хакинга и огромной демонстрацией в завершение.
Я больше всего люблю создавать ботов Slack во время Hackdays! Они, как правило, довольно маленькие, поэтому их можно закончить в течение 24 часов, они решают конкретную проблему с помощью простого решения и отлично подходят для демонстрации вживую в Slack. Пару Hackdays назад я задался целью найти способ быстро узнать, кто дежурит в определенной команде, и получить номер их телефона. Мы используем PagerDuty для ротации дежурных, составления расписания и оповещения, поэтому я занялся созданием бота для Slack с помощью Serverless Framework, который получал данные из API PagerDuty и писал, кто дежурит в канале Slack.
Заставляем бота говорить
Прежде всего, давайте убедимся, что у нас налажено взаимодействие: бот должен реагировать на сообщение и отвечать простым "Hej Världen" ("Здравствуй мир" на шведском).
Начните с создания проекта и установки зависимостей
$ npm init
$ npm i -D serverless
$ npm i @slack/web-api node-pagerduty
Мне нравится иметь serverless в качестве dev-зависимости в проекте, а не глобального пакета, и затем запускать его с помощью npx. Это значительно облегчает поддержку и позволяет убедиться, что все используют одну и ту же версию, а также позволяет легко использовать другие версии в других проектах.
Serverless Framework использует файл serverless.yml для настройки облачного провайдера, всех функций Lambda и других ресурсов, которые будут созданы в развертывании. Давайте добавим один из них с некоторыми базовыми настройками и функцией-обработчиком, принимающей POST.
После этого мы можем создать простой файл-обработчик, который регистрирует событие и просто отвечает HTTP 200.
Код вызова в коротком файле выше необходим для подтверждения настройки со Slack при создании подписки на события для бота. Первый запрос от Slack будет содержать вызов, на который вы должны ответить, чтобы подтвердить подписку.
Давайте развернем эту первую начальную версию с помощью npx и serverless, чтобы создать необходимые ресурсы и получить URL API Gateway для нашей функции Lambda.
$ npx sls deploy
Создайте приложение Slack на https://api.slack.com/apps/new и перейдите в раздел Event Subscriptions и включите эту функцию. Вам нужно ввести url вашего бота, который вы можете найти в выводе serverless deployment, а также подписаться на события бота "app_mention" и "message.im".
Перейдите в раздел "OAuth & Permissions" и добавьте "chat:write" в Bot Token Scopes, а затем перейдите в "App Home" в меню слева и включите опции "Always show my bot as online", "Message tab" и "Allow users to send Slash commands and messages from the messages tab" для поддержки прямых IM с приложением, а не только в сообщениях канала. Теперь вы готовы добавить приложение в Slack, перейдя в раздел "Установить приложение" и установив его в свое рабочее пространство. В ответ вы получите OAuth-токен Bot User, сохраните его где-нибудь, так как он нам понадобится позже.
В настоящее время приложение мало что делает, поэтому давайте заставим его реагировать и отвечать. При его настройке мы поняли одну вещь: вы должны ответить на сообщение в Slack в течение 3 секунд, иначе пользователь получит сообщение об ошибке. Учитывая холодные запуски Lambda и время на вызов API PagerDuty и Slack, это сообщение об ошибке возникает довольно часто. Это легко исправить, установив "async: true" в файле serverless.yml в конфигурации вашей функции, что заставит AWS API Gateway отвечать HTTP 200 сразу после запроса. Асинхронный вызов не очень хорошо сочетается с вызовом Slack, поскольку вызов всегда синхронный, а это значит, что вы должны включать асинхронный вызов только после завершения вызова. Еще одним последствием перехода на асинхронный вызов является то, что интеграция AWS API Gateway меняется с LAMBDA_PROXY на LAMBDA, а Serverless Framework добавляет шаблоны запросов, которые автоматически разбирают тело в JSON и изменяют структуру события. Это будет иметь последствия позже, когда мы будем проверять подписи из Slack.
Вы могли заметить, что когда вы пишете боту в IM, он входит в бесконечный цикл, реагируя на собственное сообщение. Сниппет содержит if-выражение для игнорирования сообщений от самого себя. Попробуйте! Это выглядит следующим образом.
Обеспечение безопасности вашей интеграции
Последнее, чего вы хотите - это утечка данных или спам от хорошо составленных вредоносных запросов. Slack предоставляет два способа защиты интеграции: первый - Verification Token, который устарел, и второй - проверка подписи с помощью секрета подписи. Давайте выберем поддерживаемый и не устаревший метод.
Итак, как же проверить подпись? В Slack есть отличное руководство по проверке подписи, но в библиотеках немного не хватает фактического кода. Существует реализация глубоко внутри пакета @slack/events-api, но она требует настройки сервера для запуска. Мы можем просто создать свой собственный.
Для проверки подписи вам нужны заголовки "X-Slack-Signature", "X-Slack-Request-Timestamp" и тело как строка. Однако из-за использования асинхронного вызова и того, что тело анализируется API Gateway как JSON, вам нужно JSON.stringify() тело перед тем, как предоставить его этому методу. Поначалу это работает, пока кто-то™ не решит отправить сообщение, включающее символ юникода. Внезапно сигнатуры не совпадают. Единственный способ исправить это - использовать фактическое необработанное тело запроса, что означает, что вам нужно настроить шаблон запроса.
Чтобы настроить шаблоны запросов, вы можете скопировать шаблоны по умолчанию из конфигурации вашего API Gateway и добавить следующую строку.
Добавьте два шаблона запросов (application/json и application/x-www-form-urlencoded) в вашу базу кода и добавьте следующие строки в конфигурацию http-события в вашем файле serverless.yml, ниже "async: true".
В обработчике теперь доступно "event.rawBody", содержащее необработанное тело в виде строки, которая будет использоваться при проверке подписи. Фрагмент выше также явно устанавливает интеграцию в лямбду, которая ранее просто подразумевалась через директиву async.
Подключение к PagerDuty
Теперь, когда мы разобрались с основами, давайте подключим все необходимое.
Когда у вас появляется идея, как решить проблему, вы начинаете искать доступные ресурсы и API. Отличным источником является документация API, а если есть API explorer, то это еще лучше. У PagerDuty потрясающая документация и отличный проводник API. Просматривая документацию по API, я нашел два метода, которые, казалось, достигли того, что я хотел: метод On-calls и List users on-call for schedule. Ни один из этих методов не возвращал фактическую контактную информацию пользователя, только ссылку на пользователя или ссылку на контактную информацию. Для получения контактной информации требовался последующий вызов API, и лучше всего для этого подходили методы List a user's contact. Я решил использовать метод List users on-call для поиска расписания, но в ретроспективе метод On-calls был бы более подходящим, поскольку он позволяет использовать несколько расписаний в качестве критериев поиска, что изначально не входило в задачу. Век живи - век учись.
Следующий шаг - покопаться в PagerDuty SDK и найти эквивалентные методы.
Чтобы вызвать пользователей на вызов, нам нужен идентификатор расписания, а его не так легко запомнить, поэтому нам нужно сопоставить названия команд с расписаниями. Мы также можем называть команды разными именами, и у команды может быть более одной ротации, что означает более одного расписания.
Для поддержки вышеуказанных критериев мы можем поместить команды в json-файл со следующей структурой.
Нам нужно иметь возможность получить сообщение из slack, разобрать названия команд и альтернативные названия и отфильтровать список команд на основе этих ключевых слов.
Это может показаться сложнее, чем есть на самом деле. Во-первых, это создание плоского массива всех допустимых имен и альтернативных имен, или тегов. Затем разбиваем входную строку на слова и отфильтровываем те, которые соответствуют какой-либо команде. Последний шаг - фильтрация команд по списку допустимых ключевых слов.
Затем мы получаем пользователей на вызове для всех расписаний в отфильтрованных командах, берем методы контакта с пользователем, форматируем ответ и отправляем его обратно в slack.
Задача была разделена на два метода: один - для цикла команд и потенциальных нескольких расписаний для этой команды, а другой - для получения пользователя на вызове и контактных данных. Вы также можете заметить, что SDK PagerDuty инжектируется, а не инстанцируется. Оба внешних SDK, PagerDuty и Slack, инстанцируются в коде обработчика, а затем инжектируются в функции, чтобы можно было высмеивать их и проводить модульное тестирование всех частей функции Lambda.
Я являюсь поклонником функционального кодирования и всегда стараюсь использовать map/filter/reduce вместо циклов. Если вы не знакомы с этими функциями, рекомендую ознакомиться со статьей Этьена Талбота "Упростите свой JavaScript".
Каким будет ваш следующий проект Hackday? Какие API, по вашему мнению, было бы здорово сшить вместе, а может быть, даже сделать из них бота для Slack? Теперь у вас есть инструменты для начала работы!