Контакт в Telegram-боте: как не записать чужой телефон в CRM

5 мин чтения

Проверка контакта перед записью телефона в CRM: reply-кнопка, сравнение идентификаторов, таблица отказов и воспроизводимый локальный тест.

Контакт в Telegram-боте: как не записать чужой телефон в CRM

Не записывайте любой полученный contact.phone_number как телефон отправителя. Для автоматической привязки в личном чате сопоставьте message.contact.user_id и message.from.id. Если идентификатор контакта отсутствует или отличается, оставьте телефон непривязанным и попросите пользователя поделиться своим контактом через кнопку.

Это правило для собственного обработчика Telegram Bot API, а не инструкция по настройке функции 24go.site. Предполагается, что сервер уже проверил подлинность доставки и структуру обновления. Совпадение идентификаторов связывает контакт с Telegram-пользователем в полученном событии. Это не замена SMS-проверке, установлению личности или разрешению на маркетинговые сообщения.

Сначала запросите нужный контакт

В KeyboardButton поле request_contact: true означает, что при нажатии кнопки телефон пользователя отправляется как контакт. Эта возможность доступна только в личных чатах. Используйте обычную reply-клавиатуру, а не inline-кнопку.

Например, передайте такой объект как reply_markup в существующем вызове sendMessage вашего бота:

{
  "keyboard": [[
    {"text": "Поделиться своим телефоном", "request_contact": true}
  ]],
  "resize_keyboard": true,
  "one_time_keyboard": true
}

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

Пользователь также может прислать контакт вручную. В объекте Contact поле user_id необязательно. Поэтому обработчику нужна ветка для отсутствующего ID, даже если успешный тест с вашей кнопкой всегда возвращал ID.

Решение перед записью в CRM

Сравнивайте ID пользователя из message.from.id, а не имя, username или строку телефона. User.id обозначает идентификатор пользователя или бота. Это не номер телефона.

ВходРешение обработчика
Чат не личныйНе выполнять эту процедуру привязки
В сообщении нет контактаПродолжить обычный диалог, не менять телефон
Нет ID отправителя или contact.user_idОставить контакт непривязанным
Оба ID есть, но различаютсяНе записывать контакт как телефон отправителя
ID совпадают, номер присутствуетРазрешить следующий шаг согласно вашей политике хранения
Отсутствующий ID не доказывает обман. Несовпадение тоже не повод обвинять человека: он мог отправить контакт коллеги. Ответьте нейтрально: «Не удалось связать этот контакт с вашим Telegram-профилем. Если хотите, чтобы мы позвонили вам, воспользуйтесь кнопкой, чтобы отправить свой контакт». Для обращения от имени другого человека нужен отдельный процесс, а не ослабление проверки.

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

Локальный тест без бота и CRM

Код ниже проверяет только выбор ветки на искусственных данных. Он не принимает HTTP-запросы, не проверяет секрет вебхука, не нормализует номера и не сохраняет их. Нужен Python 3, дополнительные библиотеки не требуются. Сохраните код в contact_policy_test.py и выполните python contact_policy_test.py без флага -O, который отключает assert.

import copy
import json

def contact_status(message):
if message.get("chat", {}).get("type") != "private":
return "outside_private"
contact = message.get("contact")
if not isinstance(contact, dict):
return "no_contact"
sender_id = message.get("from", {}).get("id")
contact_id = contact.get("user_id")
if type(sender_id) is not int or type(contact_id) is not int:
return "unlinked"
if sender_id != contact_id:
return "different_user"
phone = contact.get("phone_number")
if not isinstance(phone, str) or not phone.strip():
return "unlinked"
return "same_user"

base = {
"chat": {"type": "private"},
"from": {"id": 1001},
"contact": {"user_id": 1001, "phone_number": "TEST_ONLY"},
}
cases = [copy.deepcopy(base) for _ in range(7)]
cases[1]["contact"]["user_id"] = 2002
del cases[2]["contact"]["user_id"]
del cases[3]["contact"]
cases[4]["chat"]["type"] = "group"
cases[5]["contact"]["phone_number"] = ""
cases[6]["contact"]["user_id"] = "1001"
results = [contact_status(case) for case in cases]
assert results == [
"same_user", "different_user", "unlinked", "no_contact",
"outside_private", "unlinked", "unlinked",
]
print(json.dumps(results))

Ожидаемый вывод:

["same_user", "different_user", "unlinked", "no_contact", "outside_private", "unlinked", "unlinked"]
TEST_ONLY намеренно не является настоящим номером. Результат same_user означает только, что вход прошёл показанные проверки. Он не подтверждает, что по этой строке можно позвонить. В последнем случае отклоняется строковый ID: пример ожидает числовые ID из JSON Bot API. Если ваша библиотека преобразует типы, явно задайте правила такого преобразования на границе и протестируйте их.

При AssertionError выясните, какое правило изменилось, а не удаляйте проверку. Перед вызовом этой функции проверьте структуру: например, chat и from должны быть объектами ожидаемой формы. Это не универсальный парсер произвольного JSON.

Что отдельно проверить на настоящем боте

  1. В тестовом личном чате отправьте reply-кнопку, нажмите её и проверьте полученные from.id и contact.user_id. Не публикуйте реальный телефон в журнале теста.
  2. В контролируемой проверке отправьте контакт другого тестового участника с его разрешения. При несовпадении ID автоматическая привязка должна быть отклонена; при отсутствии ID ожидайте unlinked.
  3. Отправьте обычный текст после контакта. Он не должен стирать сохранённый телефон. Отказ поделиться номером не должен прерывать доступный диалог в Telegram.
  4. Проверьте входной HTTP-контур. setWebhook поддерживает secret_token и заголовок X-Telegram-Bot-Api-Secret-Token. Обработчик должен проверить настроенный секрет до обработки события. Сравнение ID внутри поддельного запроса само по себе ничего не удостоверяет.
  5. Повторно доставьте то же событие в тестовой среде. Оно не должно создавать вторую операцию записи. Это отдельная задача идемпотентности вебхуков, а не свойство проверки контакта.
Локальный пример не подтверждает результаты этих интеграционных проверок. Отдельно зафиксируйте клиент, время, выбранную ветку и изменение в тестовой CRM. В технических логах обычно достаточно результата и внутренних ссылок на запись. Не переносите туда полные контакты и секреты без необходимости.

Где заканчивается эта проверка

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

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