Пишем систему омниканальной рассылки оповещений в Telegram

Логотип компании Ренессанс Банк

Рассказали, как устроена система омниканальной рассылки без сложной персонализации и как реализовать что-то похожее с отправкой в Telegram

Все мы знаем о рассылках, где нас приглашают что-то купить или дарят бонусные рубли. Они не пишутся и не отправляются в ручном режиме — это долго, дорого и непрактично (особенно, если вы вдруг решили порадовать скидкой сразу миллионы клиентов). Такими сообщениями занимаются системы, которые на основе правил собирают «уникальные» предложения и отправляют их по SMS, на почту или в пуш-уведомлениях.

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

Немного о системе

Для своего, узкого, пользования мы разработали систему омниканальной отправки сообщений. И научили её собирать сообщения в raw-, markdown- и html-форматах для отправки по разным каналам: hpsm, Ivanti, email, Mattermost и Telegram.

Сервис помогает удобно и быстро встраивать уведомления для разных каналов связи в другие системы. Что позволяет в режиме реального времени менять шаблоны сообщений, а также учетные записи и, например, адреса smtp-серверов. Единственная проблема — ещё одна точка отказа для других систем.

Углубляться в систему не будем — лишь выделим интересующий нас API-метод /interact, и назовём его точкой входа. В этот метод мы будем передавать инструкции по сборке и отправке сообщений. Ключевыми параметрами для выполнения запроса будут: список каналов и получателей, набор параметров и «фич», а также нагрузка.  Например, такие:

			{
	"networks": {
    	"email": {
        	"to": [
                "username_or_email"
        	]
        },
    	"telegram": {
        	"chats": [88005553535]
    	}
	},
	"features": {
    	"x-reply-auto": true
	},
	"payload": {
    	"SUBJECT": "2+2",
    	"DESCRIPTION": "=",
    	"DONE": "5",
    	"KEBAB": "SIUUUUUUUUUU"
    }
}
		

В системе мы объявляем несколько уровней:

  1. Система. В нём мы определяем токены для интеграции API, указываем пути доставки с адресами, пароли, триггеры и все необходимое.
  2. Триггеры. В них указываются базовые характеристики: имя, ключ доступа, поля, которые необходимо указать в инструкции, их характеристики и модификаторы; способы доставки, которые описывают, по какому каналу, объявленному на уровне системы, необходимо отправить сообщение, и как будет выглядеть само сообщение. Например, поле name имеет тип string, модификатор toUpperCase и так далее.

Финальный API-метод исполнения инструкции отправки выглядит так: /api/v1/trigger_name_or_uid/interact.

Тут может возникнуть вопрос: куда делся первый уровень? Всё просто. Определение системы вшито в токен интеграции API.

Маршрут до оповещения выглядит так: system→ trigger → transport → template. Где transport — канал, по которым мы будем отправлять сообщения, а template — шаблон сообщения по транспорту. На этом моменте мы оставим дальнейшую работу системы под капотом.

Шаблоны и параметры заполнения сообщения

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

Шаблон вручную заполняет оператор системы — для каждого выбранного транспортного узла в триггере. То есть описывает инструмент и параметры отправки сообщения по каналу: Telegram, email, Mattermost и прочим.

Далее мы совмещаем шаблон с параметрами. Для этого используется движок Razor. Упрощенно, код инициализации экземпляра выглядит так:

			_engine = new RazorLightEngineBuilder()
  .UseEmbeddedResourcesProject(typeof(RazorMessageCompilationFeature))
  .UseCachingProvider(new RazorMemoryCachingProvider() { Lifetime = TimeSpan.FromMinutes(30) })
  .AddDefaultNamespaces(typeof(DateTime).Namespace)
  .Build();
_engine.Options.DisableEncoding = true;
		

А метод компиляции сообщения — так

			public async Task Handle(TransportContext context, IMessage message, CancellationToken cancellationToken)
{
    var properties = context.Network.Payload.ToExpando();
                
    await Task.WhenAll(CompileSubjectAsync(context, properties, cancellationToken).ContinueWith(m => message.Subject = HttpUtility.HtmlDecode(m.Result)),
                       CompileMessageAsync(context, properties, cancellationToken).ContinueWith(m => message.Body = HttpUtility.HtmlDecode(m.Result)));
}
		

Telegram

Телеграм предоставляет разработчикам несколько способов создать чат-бот и готовые библиотеки для работы с API. Нам понадобится способ Bot API. Это REST, поэтому много новой информации по взаимодействию искать не придётся.

Разработчики Bot API придумали интересный метод организации методов. Они разделили запрос на два части: токен доступа и название метода. Запрос на получение информации о боте в Telegram выглядит так: https://api.telegram.org/bot/getMe. Во всех адресах обращения к API будут меняться лишь названия методов:

  • /getMe;
  • /sendMessage;
  • /getFile и так далее.

Всю информацию по доступным методам можно найти здесь: https://core.telegram.org/bots/api#available-methods.

Регистрация бота в Telegram

Для начала необходимо создать бота:

  • заходим в приложение и запускаем чат с @BotFather;
  • отправляем команду /newbot и следуем инструкциям:пишем имя бота;вводим username, который обязательно должен заканчиваться на bot.

Выполнив все шаги, мы получим сообщение об успешном создании бота и токен для работы с ним.

Храните токен в защищённом месте и не публикуйте его вместе с кодом в репозиторий. Если вдруг всё пошло не так, и секретность токена под угрозой, вы можете перевыпустить токен или отключить бота до момента решения вопросов с безопасностью.

Учимся отправлять сообщения в чаты

Для теста подойдёт любой канал, в котором вы являетесь администратором. Для отправки сообщений нам понадобится метод sendMessage.

Объявляем структуру сообщения:

			public readonly struct Message
    {
      [Required, JsonInclude, JsonPropertyName("chat_id")]
      public readonly string ChatId;
       
      [Required, Range(1, 4096), JsonInclude, JsonPropertyName("text")]
      public readonly string Text;
 
  	  public Message(string chatId, string text)
    	{
        	if (string.IsNullOrEmpty(text) || text.Length < 1 || text.Length > 4096)
        	{
            	throw new ArgumentOutOfRangeException(nameof(text), "Размер сообщения должен удовлетворять диапазону 1 - 4096");
          }
            
          ChatId = chatId;
          Text = text;
    	}
	}
		

Передаём идентификатор канала и сообщение:

			var client = new HttpClient();
client.BaseAddress = new Uri(“https://api.telegram.org/bot<token>/sendMessage”); 

var message = new Message(88005553535, “мое первое сообщение”)

var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
request.Content = new StringContent(JsonSerializer.Serialize(message), Encoding.UTF8, "application/json");

_  = await client.SendAsync(request, cancellationToken);
		

Почти все запросы к Bot API будут с типом POST, так как это самый удачный метод для передачи большого набора данных (но можно и GET). Тело запроса на отправку сообщения в канал будет выглядеть так:

			{chat_id = 123, text = my text, parse_mode = html}
		

Поле parse_mode нужно, чтобы регулировать формат текста, выбрать html или markdown-разметку.

В модели Message поле parse_mode не указано для упрощения, но вы можете его добавить и поиграться с html или markdown-разметкой.

Отправлять сообщения пользователям тоже можно, но вот начать переписку с ботом в Telegram должен человек. Дальше надо научиться слушать события.

Слушаем чаты

Чтобы бот умел реагировать на команды, понадобится «сканирование» на обновления. Есть два метода прослушивания обновлений: регулярно ходить в Telegram или предоставлять ему адрес прослушивания на вашем сервере, то есть webhook.

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

В теле запроса мы будем отправлять следующие данные: offset, limit, allowed_updates.

offset

Поле offset самое интересное. Оно обеспечивает последовательное чтение обновлений с сервера. Его магия в том, что если мы не будем указывать его в каждом последующем запросе, то будем всегда получать одни и те же обновления раз за разом. Назовём это поле «ластиком», который стирает все полученные ранее обновления, и предоставляет свежие.

Важно! Для корректного функционирования системы, в запросах getUpdates всегда необходимо указывать — идентификатор последнего полученного обновления. При первом вызове метода у нас его не будет — мы получим его после первого запроса getUpdates. При каждом последующем запуске приложения, что логично, идентификатора обновления снова не будет, вы получите его после первого запроса, конечно же, если в потоке будет хоть одно обновление.

limit

Это лимит обновлений, которые мы можем получить в одном запросе getUpdates. Получать можно от 1 до 100 обновлений за раз. По умолчанию стоит значение 100, поэтому поле можно не указывать, если вы хотите получать максимальное количество обновлений.

allowed_updates

Это поле — массив строк, то есть список типов обновлений, которые мы хотим получить в ответ. Для полного охвата нам будет достаточно: chat_member, message и my_chat_member. Полный список можно посмотреть тут: https://core.telegram.org/bots/api#update.

Вы получите ровно те объекты обновлений, типы которых были указаны в поле allowed_updates. В нашем случае объект может выглядеть так:

			{ result = [ { update_id, chat_member }, { update_id, message } ] }
		

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

Верифицируем пользователей

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

  • таблицу сопоставления логина телеграмм с логином в AD;
  • статус верификации;
  • кодовое слово — случайно сгенерированную строку в 1024 символа.

Процесс выглядит так:

  1. Заводим заявку на подключение пользователя к Telegram.
  2. В заявке генерируются необходимые данные: запись сопоставления логинов, ключ верификации и так далее.
  3. На корпоративную почту отправляется сообщение с инструкцией, как пройти верификацию.
  4. Пользователь отправляет секретный ключ через команду /verify в чат с ботом.
  5. Мы обрабатываем все обновления с объектом message в ответе. То есть ищем поля message.chat.type, которым эквивалентно значение private.
  6. Определяем команду /verify.
  7. Подтверждаем верификацию в системе, сохраняем ID пользователя в таблицу сопоставления и отправляем ему письмо об успешной верификации.
Мы решили обойтись без усложнений и не стали регистрировать команды в боте. Но вы можете зарегистрировать их для удобства ваших клиентов.

Приглашаем пользователей в чаты

Сперва нам нужно запросить разблокировку пользователя, даже если он не был заблокирован ранее. Для этого воспользуемся командой unbanChatMember. Как тело запроса передаём поля: chat_id и user_id — и проверяем в поле в коде OK 200.

Далее генерируем ссылку на приглашение методом createChatInviteLink. В теле передаём поля chat_id, expire_date, member_limit = 1. Лимит нужен, чтобы ограничить количество вошедших по ней пользователей. (все равно неавторизованного пользователя мы забаним????).

Дата «протухания» приглашения передаётся в формате unix (seconds).

Далее отправляем сообщение пользователю со ссылкой на приглашение. Например, «Вам отправлено приглашение на вступление в группу ‘{chat.FriendlyName}’.\n{inviteLink.Link}\n Приглашение активно до {invite.ExpireDate.ToUniversalTime():dd.MM.yyyy H:mm:ss zzz».

Блокировка неверифицированных пользователей

Для определения нового вошедшего в канал  пользователя получаем значение из поля chat_member.member.status. Оно должно быть эквивалентно значению member. Если пользователь есть в системе, и он помечен как верифицированный, то мы игнорируем сообщение. Иначе смотрим на него с презрением и выписываем перманентный бан.

Для блокировки в чате используем метод banChatMember. В тело запроса передаём 2 поля: chat_id и user_id. И пишем пользователю в личку информацию о блокировке c инструкцией, как выйти и зайти правильно.

Что в итоге

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

Telegram
С#
2868