У нас в «РТ МИС» уже был мессенджер для ЕЦП.МИС. Ну, как «мессенджер» – некий самописный сервис на Node.js и хранением сообщений в БД для общения врачей и групповых уведомлений типа «Терапия! Тортики в ординаторской, успевайте».

В один прекрасный день мы собрались и решили, что все, хватит: нам нужен новый продукт, чтобы было «модно и молодежно» и еще куча функций в придачу. Дорабатывали тогда как раз модуль Стационара – здесь и приемное отделение, куда постоянно кого-то привозят и надо отправлять уведомления врачам, и большой поток информации по результатам анализов, какие-то показатели пациентов, консилиумы и вот это все. Да еще где-то впереди маячили доработки Поликлиники с телемедицинскими консультациями и уведомлениями по статусам талонов на прием.

Основные задачи для мессенджера

  • текстовый чат пользователь – пользователь, обычно – это врач – врач или врач – медсестра;

  • различные уведомления пользователей, например, врача о появлении результатов анализов или прибытии пациента в приемное отделение;

  • текстовый чат в контексте пациента или случая лечения, когда врач может задать какие-то уточняющие вопросы другим врачам с привязкой к случаю лечения или пациенту;

  • передача файлов в сообщениях;

  • работа с историей сообщений (поиск, просмотр, отметки о прочтении).

    В перспективе

  • различные сценарии уведомления, например, уведомление врачу при смене статуса талона на запись в поликлинику, рассылки по должностям, структурным подразделениями;

  • «пейджер» – push-уведомления врачу на его личное мобильное устройство в случае, если врач не подключен к внутренней сети;

  • аудио-видео конференции (консилиум);

  • телемедицинские консультации, в том числе при участии пациента, авторизованного через Госуслуги;

Основные пользователи системы сообщений на данном этапе – врачи и средний медперсонал. Пациенты тут никак не задействованы, за исключением в перспективе их участия в телемедицинских консультациях, авторизовавшись через Госуслуги.

Вперед, на поиски!

Когда речь заходит о каких-то системах сообщений, «олдфаги» вспоминают IRC и ICQ. Если надо «модно и молодежно» – речь заходит о Discord и Slack. Поклонники приватности берут Matrix. Все остальные используют Telegram. Вроде бы бери и пользуйся. Но для наших целей это все абсолютно неприменимо: медицинская информация, с которой работают врачи, – это такой забористый коктейль из персональных данных и медицинской тайны, что требует особых подходов, в частности:

  • практически вся работа идет в защищенном контуре, куда нет доступа посторонним сервисам;

  • персональные данные и другая чувствительная информация должна храниться и обрабатываться в РФ;

  • нежелательно использовать каких-то иностранных поставщиков.

В общем, все это резко ограничивает набор возможных решений.

А чего, собственно, хочется от мессенджера?

  • открытость – отсутствие vendor lock in, открытый код;

  • контроль – возможность развернуть self-hosted инсталляцию;

  • наличие реализаций (клиентов) под основные языки (платформы), используемые у нас: Java, PHP, Node.js, Python;

  • стабильность - технология должна пройти фазу «хайпа»;

  • развитие – технология не должна быть «мертвой»;

  • шифрование – хорошо, но не в первую очередь, вся работа идет в защищенном контуре;

  • интеграция – возможность встраивания в существующие системы, в частности, авторизация и список пользователей;

  • расширяемость – некий подход для создания расширений в протоколе/ПО без «глобальных костылей».

Где-то тут мы стали понимать, что Телеграм, Дискорд и прочий Слак нас не спасут. Нужно переходить в область Open Source. Задачу осложняло то, что нужно было интегрироваться с нашими существующими системами и сервисами, а еще требовалась передача различной специфики по случаю лечения (врач, срочность, тип диагноза и т.д.). Можно было бы придумать свой формат сообщений – обмениваться json-ами и передавать всю необходимую специфику в полях, но хотелось не терять возможности работы с какими-то «стандартными» клиентами без наших доработок для облегчения интеграции сторонних модулей.

Что делать? Продолжать пилить что-то свое или все же есть выход?

Безусловно, одним из возможных путей было оставить все как есть и продолжать развивать собственное решение. Тем более что на тот момент уже кроме чатов была в каком-то виде реализована поддержка аудиоконференций. Альтернативным направлением поиска стал переход от готовых решений в область протоколов. 

Беглый поиск показал активное развитие различных децентрализованных протоколов, например, Matrix, Signal. Но по ряду параметров, в частности, возможность работы с историей, это нам не подходило. Что-то стало откровенной экзотикой (OSCAR). Или более относилось в категорию «мессенджер» чем протокол (Mattermost). И тут кто-то вспомнил про XMPP. На самом деле у нас уже был опыт использования XMPP, но в качестве… корпоративного мессенджера. Как раз в тот период, когда ICQ уже перестала быть популярной, а что-то более продвинутое еще не набрало нужной популярности. В последствии, уже в «наше время» мы вторично пытались его использовать, но несмотря на интересные фишки, которые там появились, наши технические специалисты не смогли (или не захотели) толком все настроить и XMPP проиграл гонку какому-то платному решению.

Краткое введение в XMPP

XMPP

eXtensible Messaging and Presence Protocol – «расширяемый протокол обмена сообщениями и информацией о присутствии», ранее известный как джа́ббер. Открытый, основанный на XML, свободный для использования протокол для мгновенного обмена сообщениями и информацией о присутствии в режиме, близком к режиму реального времени. Изначально спроектированный легко расширяемым протокол, помимо передачи текстовых сообщений, поддерживает передачу голоса, видео и файлов по сети.

JID

Jabber Identifier строится по тому же принципу, что адрес электропочты: имя@домен. Может быть записан в краткой форме имя@домен (bare JID) или в полной (full JID) имя@домен/ресурс. Ресурс служит для того, чтобы можно было различить нескольких клиентов, подключенных к одной учетной записи. У каждого клиента ресурс должен быть уникальным. Тогда мы можем выбирать послать сообщение только одному клиенту или всем сразу. JID может быть не только у пользователя, но и у чат-комнаты, подписки и т.д. Для ресурса вводится понятие «приоритета» – если сообщение будет отправлено на краткий JID то оно будет доставлено тому клиенту, приоритет которого выше (или всем, если приоритет у всех одинаковый).

Станза (строфа)

Законченный элемент XML-потока, который содержит определённую управляющую информацию:

  • информация о присутствии (Presence) — информационные пакеты специального вида, которые содержат в себе информацию о том, подключен ли в данный момент определенный JID к сети Jabber, а также передаёт его статус, статусное сообщение и приоритет;

  • IQ (Info/Query) – особый вид стансов, реализующий механизм типа «запрос-ответ». Интерпретация IQ-станс позволяет «сущности» сделать запрос и получить ответ от другой «сущности». Тип данных, передающихся в запросе или ответе определяет пространство имён (namespace) дочернего элемента по отношению к IQ;

  • сообщение (Message) – используется для обмена сообщениями между пользователями. Выглядит примерно так:
    <message from="doctor_maria@example.ru/Desktop" to="doctor_anna@example.ru" type="chat"><body>Привет, как дела?</body></message>

Ростер (список контактов)

Разбитый на группы список Jabber-адресов ваших собеседников (контактов). Хранится на сервере и передаётся клиенту по запросу. Сервер также обрабатывает запросы на добавление, удаление контакта из списка, а также смены группы для конкретного контакта.

XEP (расширения)

XMPP Extension Protocol — расширение протокола XMPP. Например, XEP-0045 – многопользовательский чат, XEP-0084 – поддержка аватарок пользователей, XEP-0107 – статус пользователя (user mood). XEP описывают как какие-то базовые вещи (XMPP Core), так и множество продвинутого и очень интересного функционала.

Вообще, расширения – одна из самых интересных особенностей XMPP, когда, используя кирпичики описанные выше (различные стансы), мы описываем необходимый нам функционал. При желании можно сделать и собственное расширение. В настоящий момент насчитывается порядка двух сотен действующих XEP.

Использования XMPP

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

Список компаний и решений внушал, задачи «зарабатывать с пользователя» перед нами не стояло, и мы уже были готовы бежать и делать все на XMPP. Но тут выяснилась одна особенность: для XMPP необходимо рассматривать не только протокол, но в большей степени сервер и клиента, что его реализуют. А все потому, что набор реализуемых расширений (тех самых XEP) от сервера к серверу могут различаться.

Выбираем сервер

Самыми часто упоминаемыми серверами XMPP являются (в скобках – язык реализации):

Когда вы читаете про десятки и сотни тысяч пользователей, которых держит один XMPP-сервер, скорее всего, речь идет о Ejabberd. Но мы сразу понимали, что возможны доработки, а специалистов по Erlang среди нас не было. Поэтому выбор пал на Openfire от компании Igniterealtime, кстати, автора одного из самых популярных XMPP-клиентов для Android – Smack.

Детали нашей реализации

XMPP сервер – Openfire https://www.igniterealtime.org/projects/openfire/.

Клиент для фронтэнда – Strophe.js https://github.com/strophe/strophejs.

Клиент для Java сервисов и Android – Smack https://www.igniterealtime.org/projects/smack/.

Интеграция с хранилищем пользователей и системой авторизации – реализована через плагины Openfire.

Для оптимизации работы с нашим веб-приложением на PHP реализовали отправку сообщений через плагин с REST API – иначе каждый раз авторизовываться получается накладно по времени и ресурсам. Дополнительная фишка плагина – поддерживается отправка сообщений в json, включая наши дополнительные поля:

{
	"from": "Отправитель",
	"to": "Получатель",
	"headers": {
		"urgency": {
			"@xmlns": "http://rtmis.ru/protocol/xmpp/common",
			"value": 3
		},
		"disease": {
			"@xmlns": "http://rtmis.ru/protocol/xmpp/disease",
			"diag": {
				"@code": "X57",
				"value": "Лишения неуточненные"
			},
			"phase": {
				"@id": 1,
				"value": "Ранняя"
			}
		}
	},
	"body": "Пациент находится в приемном отделении"
}

Кроме того, сообщения в этом плагине складываются в очередь – дополнительный плюс для масштабируемости.

Уведомления мы сделали через комнаты (групповой чат) – бот отправляет сообщения в нужную комнату и все, кто в нее входит, получают сообщения. Обычно комнаты создаются по принципу «одна комната – одно отделение». Это оказался самый быстрый и простой способ для реализации.

Общие впечатления по XMPP и Openfire

XML-природа протокола пусть избыточна, но строга и удобна.

XEP описывают много «вкусных» вещей, но надо внимательно смотреть, что реализовано для конкретных клиента и сервера. Список для Openfire: http://download.igniterealtime.org/openfire/docs/latest/documentation/protocol-support.html. Для нас, в целом, этот список оказался достаточным.

Понятие «ресурса» («устройства») – может быть применено очень широко, например, у нас в качестве «устройства» может выступать боковая панель уведомлений для веб-приложения, основное окно чата в том же веб-приложении, мобильное устройство, приложение – «пейджер» и т.д.

К достоинствам Openfire можно отнести:

  • активную разработку;

  • много готовых плагинов https://www.igniterealtime.org/projects/openfire/plugins.jsp;

  • хорошие возможности для кастомизации: с помощью плагинов и расширений можно настроить авторизацию, обработку пакетов, маршрутизацию и многое другое.

Из недостатков:

  • не очень удачно реализован механизм плагинов, реализующих собственное REST API. По всей видимости, авторы изначально не особо рассчитывали на такое применение, поэтому получилось то, что получилось;

  • отсутствует автоматическая чистка истории в комнатах – удаляем скриптом из БД;

  • при старте Openfire подгружается ВСЯ история по ВСЕМ комнатам – приводит  к резкому росту потребления памяти, решилось ограничением глубины истории;

  • по умолчанию неиспользуемые комнаты удаляются. Долго искали в чем причина, пока не нашли что это регулируется опцией «Disable MUC room unloading for this service» в свойствах службы группового чата. Здесь же можно настроить после скольких дней неиспользуемая комната будет удалена, а также загружать или нет все комнаты при старте.

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

Заключение

Если нужно быстро поднять корпоративный централизованный мессенджер или интегрировать его в существующий продукт – XMPP и Openfire отличный вариант для старта. Чат, групповой чат – все работает «из коробки».

Нужно внимательно смотреть какие XEP реализует используемый сервер и клиент.

В целом, XMPP – это ближе к фреймворку, когда сервер (и клиент) реализуют много всяких интересных штук, но требуется приложить определенное усилие для того, чтобы это стало законченным решением.

Перспективы (планы на следующий этап):

  • переход на использование PubSub вместо группового чата для уведомлений;

  • «Пейджер» и push-уведомления для врачей – с отправкой обезличенных данных по незащищенным сетям;

  • авторизация через Госуслуги;

  • интеграция Openfire с Jitsi (https://jitsi.org/) для аудио-видео конференций;

  • интеграция Openfire с Minio/IPFS для хранения больших файлов, в том числе записей конференций.

Комментарии (7)


  1. MentalBlood
    26.01.2023 10:43

    отсутствует автоматическая чистка истории в комнатах – удаляем скриптом из БД

    по умолчанию неиспользуемые комнаты удаляются

    Неиспользуемые — это прям без единого сообщения? Иначе странная логика у авторов Openfire получается


    1. taratello Автор
      26.01.2023 11:37

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

      По поводу автоматического удаления истории - теоретически можно было бы сделать плагином, используя внутренний шедулер Openfire, красиво реализовав еще всякие опции в интерфейсе. Но скриптом оказалось быстрее и надежнее :)


  1. vanyas
    26.01.2023 18:45

    Так а чем собственно Mattermost не подошел?


    1. taratello Автор
      26.01.2023 22:04

      1. Потенциальный vendor lock in. Да, есть версия которую можно развернуть у себя. Но мы не застрахованы от того, что, допустим, завтра компания Mattermost Inc изменит условия для российских госорганов. Конечно Ignite Realtime (Openfire) тоже никто не мешает это сделать, но: "Ignite Realtime is an Open Source community composed of end-users, developers and service providers", что снижает такой риск + переход с условного Openfire на условный Ejabbed/Prosody в теории выглядит проще

      2. "Не наш" стек (Go) для возможных доработок

      3. Функционал в Mattermost хорош, но сложилось впечатление что как только мы делаем шаг в сторону от стандартных сценариев использования (или хотим их расширить) то кастомизируется это все сложнее чем тот же Openfire. Хотя, возможно, это субъективное мнение. Ну и много функционала для совместной работы из Mattermost у нас просто не востребовано (или реализуется другими сервисами)


      1. vanyas
        26.01.2023 22:06

        1. Там MIT лицензия же, никаких условий для госорганов нет и изменить их нельзя, разве что в новых версиях сменить лицензию, что маловероятно https://github.com/mattermost/mattermost-server/blob/master/LICENSE.txt


  1. kap1ik
    27.01.2023 00:45

    Не увидел части про авторизацию: есть ли LDAP или SSO? Сейчас этих мессенджеров и их вариацией применения - вагон и меленькая тележка, но вот действительно рабочих продуктов именно для корпоратов раз-два (а для нас уже и не раз-два)


    1. taratello Автор
      27.01.2023 00:58

      Поддержка LDAP в Openfire идет "из коробки", SSO - реализуется через плагины. Видел, к примеру, плагин с поддержкой авторизации через Keycloak. В этом плане в Openfire все можно настроить.

      но вот действительно рабочих продуктов именно для корпоратов раз-два (а для нас уже и не раз-два)

      Есть такое. Для корпоративного использования разные требования могут быть, но в основном это что-то из серии "мессенджер для командной работы" со всякими досками, задачами и подобным. Те же Mattermost, Rocket Chat, да много их :)