Текст Telegram в CRM: ссылки, эмодзи и смещения UTF-16

10 мин чтения

Правила преобразования Telegram message entities для собственной CRM-интеграции: исходный текст, подписи, ссылки и проверка смещений UTF-16.

Если обращение из Telegram попало в CRM с неверной ссылкой или обрезанным словом после эмодзи, проверьте преобразование текста до изменения вебхука. Telegram описывает участки текста через смещения в кодовых единицах UTF-16. Диапазоны относятся к исходному тексту или подписи, а не к очищенной копии. Сохраняйте строку вместе с её разметкой, затем отдельно формируйте обычную заметку и проверенные записи о ссылках.

Разбор предназначен для разработчика собственной интеграции Telegram Bot API с CRM. Это проектное решение и локальная диагностика, не описание проверенной встроенной функции 24go.site. Предполагается, что бот уже получает обращения на законном основании. Для локального примера нужен Python, но не токен бота и не доступ к CRM. Проверка не испытывает действующий коннектор, не отображает недоверенный HTML и не даёт права собирать сведения о посторонних людях.

Сначала выберите исходное поле

В объекте Message предусмотрены необязательные text и entities для текстовых сообщений. Подпись к медиа использует caption и caption_entities. Каждый список описывает соответствующую строку. Фотография с подписью не является текстовым сообщением с неожиданно пустым телом: обработчик, который читает только text, может потерять обращение ещё до разбора форматирования.

Передавайте эти две пары в один преобразователь как разные варианты входа и записывайте, какая пара выбрана. Если у поддерживаемого сообщения нет ни одного поля, фиксируйте явный результат «нет текста» или «неподдерживаемый формат». Пустая заметка о продаже не должна выглядеть успешным переносом. Остальным видам сообщений нужен собственный разбор схемы; эта инструкция не является полным декодером всех нынешних типов Bot API.

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

Почему эмодзи сдвигает ссылку

Entity здесь означает типизированный участок строки: например, ссылку, упоминание или выделение. В контракте MessageEntity и offset, и length измеряются в кодовых единицах UTF-16. Срез строки Python использует позиции кодовых точек Unicode. Для многих обычных букв эти системы совпадают, но различаются для символов, представленных суррогатной парой UTF-16, включая улыбающееся лицо в примере.

Возьмём искусственную строку 😀 offer. Участок offer начинается со смещения UTF-16 3 и имеет длину 5. Эмодзи занимает две кодовые единицы, следующий пробел ещё одну. Обычный срез Python с позиции 3 до 8 вернёт ffer, а не нужное слово. Исключения может не быть: обработчик выдаст правдоподобный, но неправильный фрагмент.

Из этого не следует правило «любой эмодзи считается за два». Некоторые видимые символы занимают одну кодовую единицу, а отображаемая последовательность эмодзи может состоять из нескольких кодовых точек. То, что читатель видит как один знак, не является надёжной единицей смещения. Используйте указанную кодировку или проверенную библиотечную функцию вместо собственного списка исключений для эмодзи.

Разделите исходник и представление в CRM

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

Нормализация является отдельным преобразованием. Удаление начальных пробелов, замена переводов строк или нормализация Unicode могут изменить координаты либо выбранный ими текст. Сначала проверяйте и разрешайте диапазоны на неизменённой строке. Если следующему слою нужна другая строка, вычислите её координаты заново или не передавайте позиции в это представление. Старые смещения нельзя прикреплять к изменённому тексту в расчёте, что они сохранят смысл.

При обновлении сообщения рассматривайте новое тело и новые метаданные как одну версию. Не переносите старые entities на новый текст. Клиент может изменить и подпись ссылки, и её адрес. Если заменить только видимую заметку, отдельная запись о ссылке останется устаревшей даже при верном расчёте UTF-16. Отдельно согласуйте, должна ли CRM заменять заметку, добавлять редакцию или отправлять изменение на проверку.

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

Entity типа url выделяет URL непосредственно в строке. У text_link в тексте находится видимая подпись, а отдельное необязательное поле url содержит адрес перехода. Если сохранить только выделенный фрагмент, адрес потеряется. Если показать адрес так, будто это исходная подпись, исчезнет различие между словами человека и целью перехода. Для процесса, которому нужны ссылки, сохраняйте оба значения и тип entity.

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

Для CRM с обычным текстом часто достаточно показать подпись и отдельно обозначенный адрес. Для форматированной заметки экранируйте обычный текст и стройте только поддерживаемую разметку через безопасный механизм отображения. Не склеивайте HTML с клиентским текстом и не воспринимайте оформление как разрешение что-либо выполнять. Корректный диапазон подтверждает расположение фрагмента, но не безопасность его содержимого.

Входные данныеЧто сохранитьЧего не следует заключать
urlФрагмент исходной строки и типЧто адрес безопасно открывать
text_linkПодпись и отдельный адресЧто подпись описывает настоящий домен
text_mentionПодпись и допустимую ссылку на пользователя, если она нужнаЧто подпись является постоянным username
bold или italicИсходный текст и необязательное поддерживаемое оформлениеЧто выделение меняет приоритет заявки
code или preБуквальный текст и поддерживаемые метаданныеЧто команду нужно выполнить
Неизвестный типТекст и явное решение о неподдерживаемом типеЧто всё сообщение обязательно удалять
Согласно API, text_mention может содержать объект пользователя. Это не доказывает, что отправитель владеет упомянутой учётной записью или что её владелец согласен на создание контакта. Проверка личности и сбор контактных данных остаются отдельными решениями. Не превращайте любое оформленное упоминание в нового человека в CRM.

Проверяйте диапазон перед извлечением

Проверяйте целочисленные координаты в пределах представления выбранной строки в UTF-16. Для этого преобразователя отклоняйте отрицательное смещение, неположительную длину, выход за конец и границу внутри суррогатной пары. Различайте некорректные метаданные и ошибку кодирования текста. Автоматический символ замены может скрыть, какой вход сломан; явный результат проверки проще расследовать.

Следующий код является небольшой диагностикой извлечения, а не полным валидатором Telegram entities. Он намеренно принимает одну строку и один диапазон. Точная проверка типа не позволяет Python принять логические значения за координаты. Строгое декодирование отклоняет границу внутри пары эмодзи. Тесты искусственные: они не отправляют сообщения и не переходят по ссылкам.

def entity_text(text, offset, length):
    if not isinstance(text, str):
        raise ValueError("text must be a string")
    if type(offset) is not int or type(length) is not int:
        raise ValueError("coordinates must be integers")
    if offset < 0 or length <= 0:
        raise ValueError("invalid range")
    raw = text.encode("utf-16-le", errors="strict")
    start = offset * 2
    end = (offset + length) * 2
    if end > len(raw):
        raise ValueError("range outside text")
    try:
        return raw[start:end].decode("utf-16-le", errors="strict")
    except UnicodeDecodeError as exc:
        raise ValueError("range splits a surrogate pair") from exc

text = "😀 offer"
assert entity_text(text, 3, 5) == "offer"
assert text[3:8] == "ffer"
for offset, length in [(1, 1), (-1, 1), (3, 6), (True, 1)]:
    try:
        entity_text(text, offset, length)
    except ValueError:
        pass
    else:
        raise AssertionError("invalid range accepted")
print("UTF-16 extraction checks passed")

Сохраните блок в локальный файл Python и выполните в одноразовой среде. Ожидаемая строка терминала: UTF-16 extraction checks passed. Ошибка утверждения или исключение означают, что эта диагностика не пройдена; не меняйте ожидаемые значения молча. В исследовательских артефактах статьи сохранён фактический локальный запуск. Действующий бот и CRM в этой проверке не участвовали.

Функция не проверяет типы entities, обязательные поля отдельных типов, правила вложенности, схемы URL и разрешения CRM. Строка с непарным суррогатом может вызвать ошибку уже при кодировании, до декодирования диапазона. В настоящем адаптере направляйте такую ошибку в тот же явный процесс обработки некорректного входа. Короткий вспомогательный пример не является полной границей безопасности.

Порядок преобразования в интеграции

  1. Выберите поддерживаемое тело сообщения и соответствующий список entities. Свяжите задание с источником, областью доступа и версией сообщения. Не применяйте диапазоны подписи к текстовому сообщению и не объединяйте списки разных версий.
  2. Проверьте строку и метаданные до формирования производных полей. Извлекайте диапазоны в документированной системе координат. Некорректные входы должны быть доступны только через согласованный процесс разбора; не копируйте полные обращения в общие журналы приложения.
  3. Сформируйте обычную заметку без удаления значимого клиентского текста. Нужные ссылки вынесите в отдельные записи с подписями и адресами. Задайте явный список поддерживаемых типов или правила проверки вместо предположения, что любой новый тип имеет аналог в CRM.
  4. Подготовьте данные по документированному API и правилам экранирования конкретной CRM. Если цель поддерживает только обычный текст, отправляйте обычный текст. Не включайте форматированный режим ради одной кликабельной ссылки.
  5. Перечитайте тестовую запись через разрешённый интерфейс CRM. Сравните полную заметку, подписи и адреса с задуманным представлением. Успешный HTTP-ответ не показывает, удалила ли CRM разметку или изменила переводы строк.
  6. Запишите версию преобразователя и наблюдаемый результат. Подготовьте откат изменения адаптера без дублирующих заметок и перезаписи последующих правок оператора. Повторы относятся к устройству доставки операции, а не к функции извлечения UTF-16.
Для повторных доставок и неоднозначных записей в CRM есть отдельная инструкция по идемпотентности вебхука. Правильное извлечение диапазона не предотвращает вторую заметку после таймаута с неизвестным исходом. И наоборот, защита от повторов не исправляет неверную подпись ссылки в первой заметке.

Проверяйте сохранение смысла

В набор проверок включите обычный латинский текст, кириллицу, эмодзи перед выделенным участком, несколько entities в одном сообщении и подпись к медиа со ссылкой. Отдельно добавьте подпись, которая не совпадает с адресом назначения. Это тестовые данные, а не настоящие клиентские разговоры и не доказательство того, что действующий коннектор уже прошёл испытания.

Для каждого случая запишите выбранное поле, точную исходную строку, координаты и ожидаемый фрагмент. Затем отдельно определите представление в CRM. Система с обычным текстом может намеренно убрать жирное оформление, сохранив все слова. Если владелец процесса согласовал это решение и оно отражено в проверке, такое преобразование не считается потерей данных.

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

Наконец, проверьте редактирование: измените тело так, чтобы участок сдвинулся, затем смените адрес ссылки, оставив подпись прежней. Новое представление не должно сохранять старые координаты или старое назначение. Сравнивайте перечитанную запись, а не только отправленный JSON. Это план приёмки интеграции; локальная диагностика извлечения подтверждает лишь собственные искусственные случаи.

Завершайте расследование на проверенной границе

Точный результат может звучать так: преобразователь использовал индексы Python вместо смещений UTF-16; новая версия прошла локальный тест извлечения, но чтение результата CRM ещё предстоит. Это полезнее заявления, что Telegram исправлен или вся воронка работает. Если исходную строку меняли до разбора диапазонов, исправьте и порядок преобразований, а не только арифметику.

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

Источники

  • Telegram Bot API: Message: поля текста, подписи и их метаданные; проверено в редакционную дату.
  • Telegram Bot API: MessageEntity: координаты UTF-16 и поля отдельных типов; проверено в редакционную дату.