Telegram-бот для Redmine. Как упростить жизнь себе и людям
В любой компании, использующей систему управления проектами и задачами, рано или поздно возникает желание объединить её с каким-нибудь популярным мессенжером для упрощения коммуникаций. Особенно если через эту систему идёт взаимодействие с клиентами.
В статье речь пойдёт о том, как подружить Redmine с Telegram и при этом не поломать имеющиеся бизнес-процессы.
Небольшая предыстория, как родилась идея сабжа.
Наша компания занимается администрированием серверов и сайтов 24/7, развитием инфраструктуры для крупных и развивающихся интернет-проектов, проектированием отказоустойчивых систем и автоматизацией DevOps.
Для взаимодействия с клиентами мы используем Redmine, который мы затюнили под наши процессы и навешали на него ряд приятных мелочей, которые делают нашу жизнь легче.
Ещё когда мы только начали, было это в 2011, то решили, что будем вести коммуникации с клиентами только через наш таск-трекер. Это связано с тем, что помимо поддержки серверов и реакции на инциденты, бо́льшую часть нашей работы составляют задачи по развитию инфраструктуры. Нам важно в любой момент хорошо понимать текущее состояние инфраструктуры, вектор развития проекта, правильно оценивать потребности и узкие места, предлагать решения, согласовывать их и внедрять. Дело осложняется тем, что проектов много, над каждым работает не один человек, а команда администраторов. При этом администраторы работают посменно, часть из них могут не всегда пересекаться друг с другом в офисе, а задачи нужно делать. Поэтому для нас крайне важен вопрос синхронизации знаний и действий внутри команды и между сменами.
Учитывая все эти факторы, мы решили, что единственный возможный способ держать ситуацию под контролем — сделать так, чтобы все коммуникации и вся работа проходила в одном месте — в системе задач. Естественно, мы ведём проектные wiki и делаем ещё много чего, чтобы решить проблему синхронизации действий и контекста, но ключевым моментом всегда была единая точка взаимодействия с клиентами. Если добавить к коммуникациям ещё какой-то канал, например, мессенджеры, скажем, для быстрого обсуждения каких-то вопросов, клиенты 100% начнут этим злоупотреблять, мы быстро потонем в пучине бесконечных комментариев, и важная информация начнёт просто теряться. Да и администраторов перспектива хаотичного общения с клиентами в 5 разных окнах против разложенной на чёткие бизнес-процессы работы по задачам в Redmine, мягко говоря, не привлекала.
В итоге так мы прожили лет 5. За это время многие клиенты полюбили наш Redmine, его простоту и функциональность. Кто-то, поработав с ним у нас, решил внедрить его у себя в компании. Лишь изредка единичные клиенты интересовались, можно ли ставить нам задачи не только через систему задач, но и через какой-нибудь мессенджер, потому что так было бы удобнее.
Но за последние 2 года число таких запросов значительно возросло. Настолько, что уже нельзя было не принимать их во внимание. И мы в очередной раз задумались, что нам с этим делать. Менеджеры по продажам, как обычно, пытались протащить идею каким-нибудь чудесным образом встроить общение с клиентами через Skype, WhatsApp или Telegram в наши рабочие процессы. Администраторы наотрез этого не хотели.
После бурных споров и дискуссий у нас родилась идея разработать Telegram-бота, который позволял бы все сообщения клиентов раскладывать по уже существующим задачам в Redmine или создавать новые, где с ними будут работать администраторы в привычной для себя среде.
Накидали небольшой план — сначала минимальный функционал, потом приятные мелочи. Через некоторое время мы получили прообраз будущего бота, который успешно доставил написанное ему сообщение из Telegram в Redmine, и наоборот! Это дало уверенность, что идея жизнеспособная. И мы приложили все силы к тому, чтобы довести её до конца и запустить.
Для того, чтобы лучше объяснить как всё это устроено — статья разбита на два блока. В первом блоке содержится общее описание бота и примеры его работы. Во втором — устройство и технические аспекты бота.
В конце статьи приведены ссылки где скачать и как настроить нашего бота.
Итак, что у нас получилось?
Авторизация пользователя
Во-первых, чтобы пользователь мог работать с ботом — его нужно авторизовать.
Для этого у пользователя должен быть активный Redmine-аккаунт.
Ему необходимо придумать себе username и прописать его в настройках своего Telegram-аккаунта:
Этот же username нужно прописать в настройках Redmine-аккаунта в специальное дополнительное поле «Telegram»:
Далее пользователь находит бота в Telegram и нажимает Start. Бот авторизует его и приветствует:
Постановка задач
Для того чтобы поставить новую задачу или добавить комментарий в уже существующую задачу, достаточно просто написать боту текст, и он предложит варианты. При этом к сообщению можно аттачить файлы:
Написанный комментарий можно добавить в последнюю активную задачу, выбрать задачу из списка, создать на основе него новую задачу или ничего не делать и завершить диалог.
При выборе пункта «Добавить в последнюю задачу» комментарий пользователя добавится к задаче, по которой велась последняя переписка. Бот показывает её выше.
Если выбрать пункт «Создать новую задачу», бот перейдёт в режим создания новой задачи:
И предложит пользователю:
- Выбрать проект (опционально, если не устроил дефолтный):
- Указать приоритет задачи (опционально, если не устроит дефолтный):
Пользователь может пропустить эти шаги, если его устроят дефолтные значения, и сразу нажать «Создать задачу», потребуется только указать для неё заголовок:
После этого бот уведомит клиента о создании задачи (ссылка на задачу кликабельна и по ней при необходимости сразу можно будет перейти в тикет):
Которая в Redmine будет выглядеть следующим образом:
Если же пользователь нажал на «Выбрать задачу» — бот предложит список открытых на текущий момент задач, в которые можно отправить написанный комментарий:
При этом пользователь видит только те проекты и задачи, к которым он подключен в Redmine. Это относится ко всем действиям в боте.
Переписка по задачам
Бот поддерживает доставку нотификаций об изменениях в задачах. Например, наш администратор выполнил поставленную задачу и просит клиента проверить результат:
Вот что получит клиент:
Отвечать на задачи можно через Reply на соответствующее сообщение в Telegram. Это особенно удобно когда работа ведётся сразу по нескольким задачам (ответ будет попадать сразу в нужный тикет без дополнительных вопросов):
Итог. Клиент создал задачу, общался по ней, и делал всё это через Telegram. Ему ни разу не пришлось заходить для этого в Redmine. Сотрудник получил и обработал задачу в привычном для себя виде в Redmine, в рамках стандартных бизнес-процессов, ему не пришлось переключаться на мессенджер, а потом заботиться о переносе переписки в систему задач.
Поддержка аттачей
В боте мы реализовали поддержку аттачей. Причём работает это в обе стороны. Аттачить можно всё что угодно: фотки, документы, видео и аудио. Можно прикреплять даже стикеры, правда выглядят они в браузере так себе, а жаль, порой отлично подобранный стикер продлевает жизнь всему нашему офису.
Допустим, система мониторинга сообщила нам о проблеме на одном из серверов. Администратор через Redmine создал задачу и прикрепил к ней график системы мониторинга. Клиенту придёт следующее сообщение:
На которое он может ответить голосовым сообщением:
Голосовое сообщение будет доставлено в задачу и прослушано администратором.
Ограничения
Конечно же, они есть. Мы столкнулись со следующими:
- Длинные сообщения. Telegram имеет ограничение на длину одного сообщения — 4096 символов. Изначально длинные комментарии из Redmine просто обрезались. Пришлось писать обработчик, который разбивал длинные комменты на части. Причём так, чтобы это было приятно глазу, то есть чтобы коммент не обрывался строго на 4096 символе на середине слова, а разбиение происходило аккуратно, на стыке фраз. Для клиентов неудобство заключается в том, что в этом случае будет приходить несколько сообщений.
- Форматирование. Форматирование в Redmine и в Telegram — это две разные вселенные. Но оно и понятно, у них и предназначение несколько разное. Форматирование Redmine отображается в Telegram в виде служебных символов, и порой это сильно не удобно читать. Пока мы так и не придумали как выводить их в нормальном виде, но не оставляем надежд устранить эту неприятность. Из Telegram в Redmine тоже не всё переносится. Например, если текст содержит определённые смайлики, то получив такое сообщение Redmine просто обрежет его. Это особенности структуры базы данных Redmine. У нас есть некоторые намётки решения этой проблемы и они корректно работают на тестовом стенде, но пока боимся применить их на боевой базе.
- Имена изображений. Если в Telegram сделать аттач изображения (именно изображения), то боту придёт файл с именем в формате file_xyz.jpg. Соответственно, и в Redmine он попадёт с таким именем. Тут мы пока сделать ничего не можем, поскольку бот сам получает файлы с такими именами.
- Интерфейс и все сообщения бота пока только на русском языке, но в одной из ближайших версий мы планируем добавить поддержку других языков.
Статистика использования
С момента запуска прошло чуть больше двух месяцев. Первые наблюдения:
- На текущий момент около 3,5% задач создаются и обновляются клиентами через бота.
- Как уже писал выше, бот нашёл применение не только среди клиентов, но и активно используется нами самими. Очень удобно получать нотификации по задачам (прилетают быстрее, чем в почту) и тут же на них отвечать. По ощущениям, с запуском бота у нас подросла скорость коммуникаций по внутренним задачам.
Кратко о том как устроен наш бот
- Он написан на C. Точнее на нашем фрэймворке, который написан на С
- Использует два типа БД: Redis и MySQL
- Нам потребовалось написать плагин для Redmine, который бы активировал механизм хуков при создании задач и комментариев
- Легко реплицируется и масштабируется
Итак, приступим…
Принципиальная схема работы бота следующая:
Вроде ничего сложного — все изменения в Redmine с помощью хуков доставляются боту, он смотрит тип события, в каком проекте это произошло, определяет Telegram-аккаунты получателей этого сообщения и доставляет их. Со стороны Telegram всё происходит похожим образом — определяется тип сообщения, к какой задаче оно относится и доставляется в Redmine.
Теперь давайте добавим немного экшена и познакомимся с некоторыми особенностями доставки сообщений в Telegram!
Telegram разбивает большие сообщения на несколько мелких. Эти сообщения приходят с некоторой задержкой. Все вложения доставляются в виде отдельных сообщений и иногда с большой задержкой (по мере загрузки их пользователем). Т.е. просто так брать и пересылать все получаемые сообщения нельзя, иначе мы заспамим пользователей и испортим себе карму.
И что же делать? Мы решили создать для этого очереди сообщений и использовать для их хранения Redis. Каждое поступающее сообщение наш бот кладёт в очередь и обновляет время задержки на его обработку. Когда эта задержка истекла мы считаем, что сообщение сформировано полностью и теперь можно выбирать задачу и отправлять его в Redmine.
Далее встаёт вопрос: а как же выбрать нужную задачу в Redmine? Ведь для того, чтобы боту понять что делать с полученными данными — ему надо спросить об этом пользователя, а это, как ни крути, ещё одно сообщение (даже если это нажатие кнопки) и его надо как-то связать с предыдущими действиями пользователя. В этом нам помогут сессии.
Первое пришедшее сообщение от Telegram-аккаунта пользователя — порождает сессию, которая содержит:
- ID пользователя с которым связана сессия
- Тип сессии (т.е. её состояние)
- Данные, с которыми надо что-то сделат
Каждое последующее пришедшее от Telegram сообщение может либо дополнить сессию данными, либо изменить её состояние (например, запустить процесс дальнейшей обработки и доставки данных), либо уничтожить сессию (например, если пользователь передумал писать свой комментарий), либо привести к ошибке (например, если тип поступивших данных не соответствует текущему состоянию сессии).
Ещё одним важным составляющим элементом бота является кэширование. Наш Redmine состоит из достаточно большого числа проектов, пользователей и некоторых других данных. Боту достаточно часто требуется обращаться к этим данным и если он каждый раз будет ходить за ними в Redmine, то это приведёт к заметным задержкам при обработке запросов.
У бота есть отдельный процесс, который занимается только тем, что периодически получает из Redmine данные, требуемые для остальных процессов, и сохраняет их в Redis.
А что насчёт MySQL?
Выше отмечалось, что кроме Redis бот использует ещё и MySQL. Конечно, все данные можно было бы хранить в Redis, но несмотря на наличие периодических дампов — данные всё же размещаются в памяти и неожиданный сбой в системе может привести к их потере, а среди них есть очень важная информация, к которой нужно относиться особенно трепетно. Чтобы лучше понимать о чём идёт речь — вспомним первую часть статьи. Там говорилось о том, что пользователь может не только выбирать из диалога с ботом как поступить с его сообщением (создать новую задачу или выбрать существующую), но и сделать ответ на сообщение. И бот должен понять, в какую задачу нужно его добавить.
В Telegram сообщения имеют идентификаторы, которые никак не связаны с идентификаторами задач в Redmine. Для того, чтобы их подружить — нужно установить какое-то соответствие и решить как хранить эти данные. Но перед этим давайте попробуем понять как эти данные формируются, что будет если эти данные потеряются и исходя из этого — понять их важность.
Допустим пользователь пишет своё первое сообщение в Telegram-боту. Бот открывает сессию и выясняет у пользователя в какую задачу нужно добавить комментарий. После того как ответ получен — бот отправляет данные в Redmine и одновременно с этим сохраняет это соответствие у себя.
Далее, когда пользователь сделает reply на это сообщение в Telegram, бот получит номер «родительского» сообщения и по нему определит номер задачи Redmine, в которую нужно послать комментарий. Описанное соответствие также будет установлено и для этого нового сообщения, т.е. reply потом можно сделать и на него. Это же справедливо и для событий, поступающих из Redmine. Наши клиенты считают, что это очень удобный механизм и, судя по статистике, очень активно пользуются им.
Теперь давайте представим, что все эти соответствия мы потеряли. Т.е. клиент по привычке щёлкает reply на пришедшее ему от администратора сообщение, пишет свой комментарий и отправляет боту. Но т.к. бот по описанному алгоритму уже не в силах понять в какую задачу отправить комментарий, то выдаст сообщение об ошибке и задаст дополнительные вопросы для того, чтобы выяснить что делать дальше.
Другими важными типами данных, которые нельзя потерять (причём их ещё больше нельзя потерять, чем первые) — это соответствие ID пользователей Redmine и Telegram. Если всё же это произойдёт, то пользователи просто перестанут получать информацию о событиях, происходящих в Redmine до тех пор, пока что-то не напишут боту. А т.к. по опросам бота чуть ли не чаще используют как уведомлятор (на замену почты), то тут мы можем получить ещё большие неприятности.
Чтобы не расстраивать пользователей — положили эти данные в менее быстрый, но более надёжный MySQL.
Справедливости ради стоит отметить, что в нашей команде до сих пор идут споры о том, стоит ли отказаться от MySQL и перенести эти данные в Redis. Поэтому не исключено, что в будущем мы можем полностью перейти на работу с Redis, а может и вообще к какой-то другой системе хранения данных. Благо, что архитектура нашего приложения позволяет делать такие замены относительно просто.
Масштабирование и отказоустойчивость
Теперь хотелось бы поговорить о том, как обеспечить масштабирование и отказоустойчивость бота…
С появлением бота наш Redmine в некоторых случаях превратился в чат и интенсивность обмена сообщениями постоянно растёт. Некоторые наши клиенты так же хотят интегрировать его в своей инфраструктуре. А учитывая то, что наша разработка полностью open source — не известно какая ещё компания или команда администраторов/девелоперов пожелает им воспользоваться и не известно с какими нагрузками бот столкнётся. По этому мы ещё на стадии проработки проекта заложили в него возможность масштабирования.
В нашем проекте масштабирование можно разделить на два класса:
- Масштабирование баз данных
Тут всё достаточно известно:
- Для MySQL можно использовать Percona Cluster или аналоги
- Для Redis — Redis Cluster
- Масштабирование самого бота
Чтобы понять, как можно масштабировать бота — давайте более подробно рассмотрим процесс приёма и обработки сообщений.
Принципиальная схема следующая:
Единственная задача процесса «Rest API» — это полученное от Redmine или Telegram сообщение положить в очередь (ra-queue) и ожидать следующих. Эту очередь постоянно мониторят процессы «Queue-workers» и при появлении в ней данных — считывают их.
Сообщения от Redmine сразу обрабатываются и отправляются нужным пользователям в Telegram. С событиями от Telegram всё сложнее. Т.к. их сразу отправлять в Redmine нельзя (причины описаны выше), то они перекладываются в другую очередь, уже разбитую по пользователям. Тип сообщения определяет время, в течении которого мы ждём другие сообщения. Рано или поздно все эти сообщения считываются, проходят предварительную подготовку, тем или иным образом изменяют состояние сессии и могут быть отправлены в Redmine со всеми прикреплёнными файлами, которые скачиваются из Telegram на этом же шаге.
Тут важно отметить, что благодаря такому подходу и имеющемуся механизму блокировок — обработка всегда происходит на одном конкретном узле. Именно это позволяет создать столько инстансов бота и с таким количеством процессов «Queue-workers», сколько это необходимо в рамках имеющейся нагрузки.
При получении Redmine'ом сообщения срабатывает хук и это же сообщение отправляется обратно боту, после чего доставляется остальным Telegram-аккаунтам, подписанным на задачу.
Таким образом, инфраструктуру бота в распределённом варианте можно представить так:
Но всё описанное никак не ограничивает возможность использовать бота в обычном standalone варианте с одним сервером MySQL и обычным некластерным вариантом Redis.
Как попробовать бота?
Для демонстрации работы бота мы сделали demo-версию, доступную по адресу demo.nxs-chat.nixys.ru.
Чтобы попробовать его в действии — необходимо выполнить несколько предварительных шагов:
- В demo.nxs-chat.nixys.ru/account/register зарегистрируйте минимум 2 аккаунта (чтобы была возможность переписываться между ними) и заполните поле Telegram хотя бы для одного из них
- Активируйте созданные аккаунты
- Найдите бота @nixys_demo_chat_bot для каждого Telegram-аккаунта, указанного при регистрации, и нажмите кнопку Start.
Убедитесь, что в настройках Telegram-аккаунтов заданы те же usernames, что и в настройках аккаунтов в demo.nxs-chat.nixys.ru - Создайте проект в Redmine и добавьте в него созданные аккаунты в роли «Project member»
- В созданном проекте добавьте задачу и начните общение!
Важное замечание! Бот будет отправлять сообщения в Telegram в следующих случаях:
- Аккаунту, являющемуся автором задачи (при условии, что это не автор конкретного комментария)
- Аккаунту, являющемуся исполнителем задачи (при условии, что это не автор конкретного комментария)
- Аккаунтам, находящимся в списке наблюдателей в задаче (при условии, что это не автор конкретного комментария)
Как получить бота?
Бот является полностью открытым и его можно получить как в виде исходных кодов, так и в виде пакета (пакеты пока доступны только для Debian 8, но в ближайшее время появится для Debian 9 и CentOS 7).
Ссылка на Github репозиторий с исходными кодами бота. Там же находится инструкция по установке бота из пакетов и его настройке.
Заключение
В ближайшем будущем планируем добавить следующий функционал:
- Упрощение создания задач
- Настройка уведомлений
- Мультиязычный интерфейс
- Подписка на задачи и отписка от них через бота (сейчас это возможно сделать только через Redmine)
- В перспективе планируем добавить распознавание голосовых файлов в текст, что будет весьма полезно для задач, которые клиенты создают голосом
- И многое другое