Контакт в Telegram-боте: как не записать чужой телефон в CRM
Проверка контакта перед записью телефона в 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 совпадают, номер присутствует | Разрешить следующий шаг согласно вашей политике хранения |
Совпадение не должно молча перезаписывать ранее подтверждённый номер в 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.
Что отдельно проверить на настоящем боте
- В тестовом личном чате отправьте reply-кнопку, нажмите её и проверьте полученные
from.idиcontact.user_id. Не публикуйте реальный телефон в журнале теста. - В контролируемой проверке отправьте контакт другого тестового участника с его разрешения. При несовпадении ID автоматическая привязка должна быть отклонена; при отсутствии ID ожидайте
unlinked. - Отправьте обычный текст после контакта. Он не должен стирать сохранённый телефон. Отказ поделиться номером не должен прерывать доступный диалог в Telegram.
- Проверьте входной HTTP-контур. setWebhook поддерживает
secret_tokenи заголовокX-Telegram-Bot-Api-Secret-Token. Обработчик должен проверить настроенный секрет до обработки события. Сравнение ID внутри поддельного запроса само по себе ничего не удостоверяет. - Повторно доставьте то же событие в тестовой среде. Оно не должно создавать вторую операцию записи. Это отдельная задача идемпотентности вебхуков, а не свойство проверки контакта.
Где заканчивается эта проверка
Запрашивайте телефон только для цели, понятной пользователю. Ограничьте доступ к номеру, установите срок хранения и предусмотрите способ его исправления или удаления. Связь контакта с Telegram ID не доказывает право звонить с рекламой, не подтверждает личность и не гарантирует, что номер сейчас доступен. Требования к обработке данных и сообщениям отдельно оцените для своего сценария.
Критерий готовности: контакт с неизвестным или отличающимся ID не становится телефоном отправителя, отказ не прерывает диалог, а повторная доставка не добавляет лишнюю запись. Подключайте эту процедуру к рабочей CRM только после таких проверок.