Миграция группы Telegram: как сохранить привязку к CRM при смене chat_id

9 мин чтения

Как обработать migrate_to_chat_id и migrate_from_chat_id, сохранить беседу в CRM и остановить конфликт привязок. Таблица решений и локальный Python-тест.

Миграция группы Telegram: как сохранить привязку к CRM при смене chat_id

После преобразования группы Telegram в супергруппу не считайте новый chat_id новой беседой с клиентом. Зафиксируйте связь старого и нового идентификаторов, сохраните внутренний идентификатор беседы, а перед разрешённой отправкой определяйте её текущий адрес. Если два chat_id уже принадлежат разным беседам, остановите автоматическую обработку и разберите конфликт вместо того, чтобы незаметно объединять карточки.

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

В какую сторону читать поля миграции

В официальном описании Message указаны два необязательных поля. Они показывают одну связь с разных сторон:

Полученное свидетельствоСтарый ID группыНовый ID супергруппы
Сообщение с migrate_to_chat_idchat.id этого сообщенияЗначение migrate_to_chat_id
Сообщение с migrate_from_chat_idЗначение migrate_from_chat_idchat.id этого сообщения
Неуспешный запрос с parameters.migrate_to_chat_idchat_id, использованный именно в этом запросеЗначение параметра
Третья строка относится к ResponseParameters, а не к телу служебного сообщения. Сохраняйте контекст запроса: без исходного адреса ответ с ошибкой не определяет оба конца связи. Не извлекайте старый идентификатор из переведённого описания ошибки. Ветвитесь по документированному структурному полю, а не по конкретной английской фразе или любому HTTP 400.

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

Храните идентификаторы без сужения до 32-битного целого. Telegram указывает, что у migration ID может быть больше 32 значащих бит, но не больше 52. Знаковое 64-битное целое в БД - прямой вариант хранения. Если пользовательское поле CRM не сохраняет такое целое точно, используйте на этой границе проверенную десятичную строку и проверьте обратное преобразование. Не вычисляйте новый ID, добавляя префикс к старому: используйте значение, переданное Telegram.

Отделите беседу от транспортного адреса

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

В своей модели разделите четыре понятия:

  • conversation_id - стабильный внутренний идентификатор рабочей беседы.
  • chat_alias - идентификатор Telegram, который относится к этой беседе в пределах вашего клиента системы и бота.
  • active_chat_id - текущий адрес для новых операций; его применимость всё равно зависит от актуальных прав и правил приложения.
  • received_chat_id - фактический ID, записанный в историческом событии; его не переписывают.
Область изоляции задаёт ваша интеграция, а не формат Telegram ID. Включайте tenant и bot в ключи поиска, чтобы одна установка не могла случайно привязать свидетельство к беседе другого клиента системы. Уникальное ограничение по области и alias должно запрещать принадлежность одного alias двум внутренним беседам. При нескольких обработчиках нужны транзакционные проверки: словарь в памяти их не заменяет.

Alias сохраняет непрерывность маршрутизации. Он не доказывает, что два человека в CRM - один человек, что все сделки нужно объединить или что идентификатор сообщения старого чата действителен в новом. Сохраняйте ссылки на сообщения как исходную пару «чат-сообщение». Поведение старых ссылок и ответов проверяйте отдельно, а не переписывайте message_id только на основании миграции чата.

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

Принимайте решение о миграции атомарно

Ветку миграции обрабатывайте раньше универсального правила «неизвестный чат - создать беседу в CRM». Иначе служебное сообщение из супергруппы может создать ненужную карточку до того, как обработчик проверит поля миграции.

  1. Проверьте обновление на обычной доверенной границе приёма. Сохраните входящее свидетельство и ключ исходного события. Текст пользователя с каким-либо ID не равен структурному полю миграции Telegram.
  2. Извлеките старый и новый ID по таблице. Отклоните некорректные значения и случай, когда старый и новый ID одинаковы. Сохраните, какое поле подтвердило связь.
  3. В транзакции БД заблокируйте затронутые scoped alias или иным способом сериализуйте работу с ними. Заново прочитайте принадлежность обоих alias внутри транзакции.
  4. Если у старого alias одна беседа, а новый свободен, привяжите новый alias к существующей беседе. Запишите связь миграции и обновите активный адрес беседы.
  5. Если оба alias уже принадлежат той же беседе, считайте операцию повтором. Сохраните свидетельство, но не создавайте новый объект CRM или новое задание уведомления.
  6. Если alias принадлежат разным беседам, отправьте операцию на сверку. Не выбирайте более свежую строку, не перезаписывайте внешний ключ и не удаляйте одну беседу автоматически.
  7. Зафиксируйте привязку и локальное последующее задание вместе. Только после этого подтверждайте соответствующий шаг обработки согласно контракту вашего приёмника.
Если неизвестны оба alias, сохраните неразрешённое свидетельство либо примените отдельное правило подключения, согласованное для этой группы. Не угадывайте владельца среди существующих карточек по названию группы. Если известен только новый alias, старый можно привязать после тех же проверок области и свидетельства; обратный поиск не разрешает перезаписывать конфликт.

Транзакция вашей БД не охватывает удалённую запись в CRM. Если CRM тоже хранит текущий адрес, обновляйте его отдельной отслеживаемой операцией и сохраняйте её результат. После неоднозначного таймаута сначала установите фактический результат, а не считайте запись неудачной. Таблица alias должна оставаться источником маршрутизации, пока внешняя копия догоняет её по заранее определённому правилу.

Локально проверьте направление, повтор и конфликт

Сохраните пример в migration_check.py и выполните python3 migration_check.py. Нужна только стандартная библиотека Python; используются искусственные ID и память процесса. Пример не обращается к Telegram или CRM и не создаёт базу данных. Кортеж scope обозначает одного клиента системы и одного бота. Два словаря сообщений - намеренно упрощённые входные данные, а не записи событий реальной группы.


def pair(message):
    chat = message["chat"]["id"]
    if "migrate_to_chat_id" in message:
        return chat, message["migrate_to_chat_id"]
    if "migrate_from_chat_id" in message:
        return message["migrate_from_chat_id"], chat
    raise ValueError("no migration field")


def apply(aliases, scope, old, new):
    if type(old) is not int or type(new) is not int or old == new:
        raise ValueError("invalid pair")
    before = aliases.get((scope, old))
    after = aliases.get((scope, new))
    if before is None:
        raise ValueError("old alias needs reconciliation")
    if after is not None and after != before:
        raise ValueError("conversation conflict")
    aliases[(scope, new)] = before
    return before


scope = ("tenant-demo", "bot-demo")
old, new = -12345, -1009876543210
left = {"chat": {"id": old}, "migrate_to_chat_id": new}
right = {"chat": {"id": new}, "migrate_from_chat_id": old}
assert pair(left) == pair(right) == (old, new)
aliases = {(scope, old): "conversation-A"}
assert apply(aliases, scope, *pair(left)) == "conversation-A"
assert apply(aliases, scope, *pair(right)) == "conversation-A"
assert len(aliases) == 2
aliases[(scope, -1001111111111)] = "conversation-B"
try:
    apply(aliases, scope, old, -1001111111111)
except ValueError as exc:
    assert str(exc) == "conversation conflict"
else:
    raise AssertionError("conflict accepted")
assert aliases[(scope, -1001111111111)] == "conversation-B"
try:
    apply(aliases, ("other-tenant", "bot-demo"), old, new)
except ValueError as exc:
    assert str(exc) == "old alias needs reconciliation"
else:
    raise AssertionError("scope crossed")
print("direction=ok repeat=ok conflict=blocked scope=isolated")

Ожидаемая последняя строка: direction=ok repeat=ok conflict=blocked scope=isolated. Ошибка assertion означает, что проверяемое правило не выдержано; не удаляйте проверку ради нужной строки. Пример намеренно останавливается при неизвестном старом alias. Он не реализует необязательные ветки обратной привязки и первичного подключения, описанные выше.

Такая проверка устанавливает направление полей и небольшое последовательное правило. Она не проверяет полную схему Telegram update, допустимый диапазон целых чисел, доверенный приём, конкурентные транзакции, восстановление после сбоя, смену активного адреса или внешнюю копию в CRM. Эти проверки добавляют на тех уровнях, где соответствующее поведение действительно реализовано. Небольшой пример позволяет проверить результат, не выдавая учебный словарь за промышленную интеграцию.

Обновляйте маршрут задания, а не его прошлое

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

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

Если система проходит цепочки alias, ограничьте число переходов и обнаруживайте циклы. Повтор связи старого ID с новым не должен создавать обратное ребро только потому, что встретилось migrate_from_chat_id. Оба вида служебного сообщения задают направление от старого к новому. Если цепочка зацикливается или противоречит принадлежности беседы, остановите задание и сохраните свидетельства для оператора. Ограничение числа переходов - ваше защитное правило, а не заявленный лимит миграций Telegram.

Для закешированных адресов нужен ясный способ сброса. Иначе данные в БД могут быть верными, а обработчик продолжит обращаться по старому адресу. В тестовом окружении оставьте одному обработчику старое значение в кеше и проверьте, как версия миграции или истечение срока кеша заставляет перечитать запись. Успешный поиск в новом процессе не доказывает, что обновился старый кеш.

Прежде чем править CRM, определите причину

НаблюдениеСледующая проверкаЧего избегать
В ошибке есть структурный migration targetСвязать его с сохранённым запросом и scoped беседойПолучать новый ID из любого текста ошибки
Трафик новой группы создаёт вторую беседуПорядок веток обработчика и поиск aliasУдалять новую карточку до проверки принадлежности
В БД новый ID, обработчик отправляет на старыйВыбор адреса в очереди и сброс кешаПовторять все задания подряд
Новый alias занят другой беседойОбласть tenant/bot и предыдущее подключениеАвтоматически объединять беседы
Новый адрес всё равно отклоняет действиеАктуальные права и фактическая ошибкаСчитать миграцию универсальным восстановлением доступа
Оператору нужны исходное свидетельство, оба ID, принадлежность бесед и принятое решение, а не доступ ко всей переписке. Записывайте только необходимые для диагностики метаданные согласно своей политике хранения. Не включайте токены бота и лишние персональные данные в технические квитанции.

Проверьте настоящую интеграцию до возобновления работы

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

Затем проверьте старое задание из очереди. Его исходный chat_id должен остаться неизменным в истории, текущий адрес должен определяться по принятой связи, а предыдущее состояние доставки должно по-прежнему влиять на решение о запуске. Отдельно проверьте внешнюю копию адреса в CRM, а не только таблицу alias. Наконец, выполните обычное разрешённое взаимодействие в текущей группе и запишите фактический результат: строка локального теста не подтверждает доставку.

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