Полезная функциональщина. Грабим почту, трекеры задач и репозитории с Clojure

Я — тимлид в одной из команд разработки Positive Technologies Application Firewall. В один прекрасный момент размер команды превысил критическое значение, когда можно делать руками всю свою рутинную работу, и я начал писать автоматизированную систему, которая умела бы взаимодействовать с почтой, трекерами задач и системами контроля версий. Я назвал ее «автоматический тимлид».

Распределение задач — одна из рутинных процедур, которую легко автоматизировать, если в команде существует матрица компетенций: назначить исполнителя в зависимости от заголовка задачи или ее компонента можно, если уметь программно читать задачу с таск-трекера и обновлять некоторые поля в ней. Аналогичная история с автоматическим назначением по набору критериев ответственного проверяющего в пулл-реквест в системе контроля версий для рецензирования исходного кода.

В итоге, конечно, автоматический тимлид был разделен на бизнес-логику и общую библиотеку интеграций (я назвал ее Flower). О последней давай и поговорим.

Зачем оно вообще всем

Зачем мне понадобилось такое программное обеспечение, в целом понятно, но зачем же оно тебе?

Допустим, по счастливому стечению обстоятельств, у тебя есть учетные данные некоего пользователя Jira. Очень хочется на память сохранить пару тысяч задач этого пользователя (а заодно и всю его почту), чтобы развлекаться серыми осенними вечерами, почитывая занимательные комментарии. Почему бы не сделать это, написав три строчки кода?

Или ты менеджер проекта по разработке программных продуктов и решил устроить своим программистам полный KPI, замеряя производительность людей в условных пулл-реквестах на задачу. Собирать статистику с двух и более разных систем одновременно тоже можно без технических проблем.

Как вариант, ты желаешь перенести все задачи из GitLab и Jira на GitHub (потому что твой проект резко стал опенсорсным), а заодно настроить автоматическую пересылку сообщений из почты в Slack. И даже это делается не очень сложно!

Почему Clojure

Первый вопрос, который можно было бы мне задать, звучит примерно так: почему ты не выбрал Python, ведь ты с ним знаком десяток лет и он имеет все необходимые библиотеки интеграций? Не имеет. Имеет, конечно, но не все работает так гладко, как SDK на Java, созданный разработчиками систем, с которыми мы интегрируемся. А в некоторых случаях еще и не хватает многопоточности. Тут нужно заметить, что для первоначального прототипа я, конечно же, и выбрал Python, а точнее HyLang (Lisp на его стеке), но в итоге решил внимательнее присмотреться к JVM.

И второй очевидный вопрос — почему вообще Lisp? Потому что, насколько мне известно, лучший способ создать свой DSL, не изобретая новый синтаксис, — взять Lisp, проверенный временем язык с префиксной нотацией и веселыми скобочками (на самом деле это называется S-выражения), и, используя макросы, обогатить его до домена использования.

Совокупность этих двух факторов и побудила меня выбрать Clojure как язык программирования и платформу для интеграций с различными системами.

Show me the code

Чтобы начать писать на Clojure свой скрипт или даже целую систему, использующую Flower, необходимо для начала установить Leiningen (для Windows в некоторых случаях это может оказаться немного нетривиальной задачей, поэтому рекомендую взять любую *nix-систему). Если ты вдруг не знаешь Clojure, то рекомендую книгу Clojure for the Brave and True — идеальное пособие для освоения языка за пару вечеров.

Чтобы начать новый проект на базе Flower, можно воспользоваться шаблоном, набрав в терминале


lein new flower my-new-flower-app


При этом в файл project.clj в созданной директории проекта будет добавлена зависимость [flower "0.4.3"] — это метапакет, содержащий почти все необходимое. Для тестового приложения он нам вполне подойдет.

Давай теперь напишем наше приложение. В качестве подопытной системы контроля версий и трекера задач воспользуемся GitHub.

Для аутентификации нам понадобится добавить токен пользователя GitHub, чтобы иметь возможность делать изменения и не быть ограниченными рейтом запросов. Добавь сгенерированный для своей учетной записи токен вместо звездочек в файл .credentials.edn в домашней директории пользователя (~/.credentials.edn) в следующем формате:

<span class="pun">{:</span><span class="pln">token </span><span class="pun">{:</span><span class="pln">github </span><span class="str">"****************************************"</span><span class="pun">}}</span>


Теперь можно начать ставить эксперименты. Для этого в директории проекта запустим REPL из командной строки:


lein repl


Трекеры задач

Подключим необходимые модули и определим наш трекер задач:


 (require '[flower.macros]
        '[flower.tracker.core])
 
(flower.macros/with-default-credentials ;; Можно без этого макроса, если трекер задач приемлет запросы без авторизации
  (def pt-github-tracker (flower.tracker.core/get-tracker "https://github.com/PositiveTechnologies/flower")))

Давай посмотрим, что теперь содержится в определении pt-github-tracker. Для наглядности я сделал pretty-вывод и убрал не очень важные сейчас поля.


 my-new-flower-app.core=> pt-github-tracker
#flower.tracker.github.tracker.GithubTracker{
  :tracker-component #flower.tracker.core.TrackerComponent{
                      :auth {:github-token "****************************************"}
                      :context {}}
  :tracker-name :github-github.com-flower
  :tracker-url "https://github.com/PositiveTechnologies"
  :tracker-project "flower"}


Функция get-tracker подставила авторизационные данные и развернула строковый адрес трекера задач в запись с общим протоколом flower.tracker.proto.TrackerProto. Если тип трекера не определился верно, то тип записи будет flower.tracker.default.tracker.DefaultTracker. Это поведение можно изменить, эксплицитно указав тип трекера с помощью макроса flower.tracker.core/with-tracker-type.

Получить все задачи из трекера можно следующей командой:


 (def tasks (.get-tasks pt-github-tracker))

Самостоятельно можешь взглянуть, из каких полей состоит, например, первая задача в трекере после выборки, с помощью команды (first tasks), а затем давай сделаем полную выборку в формате JSON. Зависимость clojure.data.json приехала к нам вместе с метапакетом flower, поэтому весь оставшийся код будет выглядеть так:


 (require '[clojure.data.json])
 
;; Функция сериализации, которую мы используем для удаления
;; полей :tracker и разыменования дополнительных невыполненных
;; полей (например, комментарии)
(defn serialize-task [task]
  (reduce-kv (fn [self k v]
              (if (= k :tracker)
                self
                (assoc self
                        k (if (delay? v) @v v))))
            {}
            task))

 

;; Печатаем результирующий JSON

(clojure.data.json/pprint {:tasks (map serialize-task
                                      tasks)})
  

Из интересных вещей, которые содержит в себе библиотека Flower при работе с трекерами задач, можно выделить еще изменение полей задачи. Делает��я это двумя строчками кода (и требует прав на запись):

;; Получаем первую задачу из выборки

(def first-task (first tasks))
 
;; Обновляем поля заголовка и тегов
(.upsert! (assoc first-task
                :task-title "New title"
                :task-tags ["sometag"]))


На этом месте, конечно, вывалится ошибка о невозможности что-либо записать в репозиторий, если не хватает на это прав: RequestException Must have admin rights to Repository. (403). Но если вдруг хватает, то для первой задачи из выборки будут фактически обновлены заголовок и теги, а результатом вернется обновленная запись.

Системы контроля версий

С системами контроля версий абсолютно аналогичная история:


 (require '[flower.macros]
        '[flower.repository.core])
 
(flower.macros/with-default-credentials
  (def pt-github-repo (flower.repository.core/get-repository "https://github.com/PositiveTechnologies/flower")))

Знакомо, не правда ли? Во время разработки библиотеки я старался сделать так, чтобы определения для разных типов систем были похожи между собой и чтобы из названий функций было понятно их назначение.

Посмотреть, какие функции поддерживаются протоколами различных систем, можно здесь (однако идея записей в том, что обращаться к их полям можно и напрямую, минуя вызовы этих функций в протоколах; так что, если для какого-то поля не определена функция выборки, его можно выбрать из записи через функцию get).

Полистаем теперь пулл-реквесты из репозитория:


(def prs (.get-pull-requests pt-github-repo))

 

;; Получаем первый пулл-реквест из выборки

(def first-pr (first prs))
  

Для выбранного пулл-реквеста выведем на печать все комментарии и счетчики (например, количество комментариев LGTM):


 (println (.get-comments first-pr))
(println (.get-counters first-pr))
  

Если вдруг у тебя есть права на запись в репозиторий, то можно смерджить пулл-реквест. Мерджить будем, только если один из комментариев содержит слово LGTM:


 (when (> (get (.get-counters first-pr)
              :count-lgtms)
        0)
  (.merge-pull-request! first-pr))
  

Системы обмена сообщениями

Пора программно почитать рабочую почту! Для этого нужно добавить в созданный раньше ~/.credentials.edn запись аккаунта Exchange. После добавления учетных данных файл может выглядеть, например, так (в примере поле :login содержит пользователя Exchange, :password — его пароль, :domain — домен Active Directory и :email — почтовый адрес пользователя):


 {:token {:github "****************************************"}
 :account {:login "jdoe"
          :password "************************"
          :domain "EXAMPLE"
          :email "jdoe@example.com"}}
  

Так как метапакет не включает в себя зависимости конкретных реализаций мессенджинговых систем, то каждую нужно указывать в project.clj эксплицитно. Для Exchange необходимо подключить пакет [flower/flower-integration-exchange "0.4.3"]. Итоговый файл проекта после этого будет выглядеть, например, так:


 (defproject my-new-flower-app "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :dependencies [[org.clojure/clojure "1.9.0"]
                [flower "0.4.3"]
                [flower/flower-integration-exchange "0.4.3"]]
  :main ^:skip-aot my-new-flower.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})
  

Перезапустим REPL, закрыв его и повторно из директории проекта выполнив команду lein repl. После этого можно приступить к подключению библиотек и определению почтового ящика:


 (require '[flower.macros]
        '[flower.messaging.core])
 
(flower.macros/with-default-credentials
  (flower.messaging.core/with-messaging-type :exchange
    (def msg-box (flower.messaging.core/get-messaging))))
  

Выберем первое сообщение из почтового ящика с загрузкой тела сообщения и отправим его же на другой почтовый ящик (фактически это не пересылка, а отправка нового сообщения с заменой получателя):


 (def first-msg (first (.search-messages msg-box
                                        {:count 1
                                        :load-body true})))
 
(.send-message! (assoc first-msg
                      :msg-recipients ["jstiles@example.com"]))
  

Но еще интереснее настроить автоматическую пересылку писем. Так как пакет clojure.core.async установлен как зависимость метапакета flower, то можно сразу же использовать его для асинхронной обработки входящих сообщений:

 (require '[clojure.core.async :as async])
 
;; Подпишемся на входящие сообщения
(def ch (.subscribe msg-box
                    {:load-body true}))
 
;; Создаем сопрограмму для вывода на печать
;; и автоматической пересылки сообщений
(async/go-loop []
  (when-let [msg (async/<! ch)]
    (println msg)
    (.send-message! (assoc msg
                          :msg-recipients ["jstiles@example.com"]))
    (recur)))
  

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

Заключительное слово

В статье я показал лишь небольшую часть примеров применения библиотеки интеграции. С этими знаниями уже можно писать небольшие сценарии ad hoc. А после погружения в Clojure можно писать и готовый программный продукт.

В моих планах — добавить в библиотеку поддержку большего числа сервисов. YouTrack, HipChat, Telegram и некоторые другие системы ждут своей очереди на добавление. Поскольку проект опенсорсный, я предлагаю тебе присоединиться любым доступным способом: даже простой фидбек в виде Issue на «Гитхабе» будет очень ценным для меня. А заведенный автоматизированно, с использованием самой библиотеки — вдвойне!