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

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

https://t.me/CyberLifes

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

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

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

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

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

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

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

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

Почему Clojure

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

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

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

Пример приложения, использующего библиотеку Flower

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 на «Гитхабе» будет очень ценным для меня. А заведенный автоматизированно, с использованием самой библиотеки — вдвойне!