Пользователи и чаты

chat_id и user_id

Разница между chat_id и user_id в Telegram Bot API. Как правильно извлекать идентификаторы на PHP, избегать переполнения типов данных и проверять права бота.

Понимание разницы между chat_id и user_id — это фундамент для работы с пользователями и чатами в Telegram. Ошибки в их обработке приводят к тому, что бот отправляет сообщения не в те чаты, путает профили пользователей в базе данных или падает с ошибками авторизации. В этой главе мы разберем анатомию этих идентификаторов, научимся правильно извлекать их из обновлений на PHP и проверять права бота перед отправкой сообщений.

В чем разница: user_id vs chat_id

Хотя оба идентификатора представляют собой целые числа, они выполняют принципиально разные роли в архитектуре Telegram Bot API.

  • user_id (Идентификатор пользователя) — это уникальный, всегда положительный 64-битный номер конкретного человека или бота. Он жестко привязан к аккаунту и никогда не меняется, даже если пользователь сменит юзернейм (@username) или имя. Этот ID всегда находится в поле from.id внутри объектов сообщений, нажатий на кнопки и запросов.
  • chat_id (Идентификатор чата) — это адрес конкретного пространства, куда отправляются сообщения. Формат chat_id напрямую зависит от типа чата.

Тип чата определяет структуру его идентификатора:

  • Личный чат (Private): chat_id всегда положительный и в точности равен user_id вашего собеседника.
  • Обычная группа (Legacy Group): отрицательное число (например, -456789012).
  • Супергруппа (Supergroup) и Канал (Channel): отрицательное число, которое всегда начинается с префикса -100 (например, -1001234567890). Префикс -100 — это не визуальный элемент, а часть математического значения идентификатора.

Извлечение ID из входящих обновлений

Когда ваш скрипт принимает вебхук от Telegram, структура JSON может отличаться в зависимости от того, прислал ли пользователь текстовое сообщение, нажал ли на inline-кнопку или опубликовал пост в канале. Нам нужен надежный способ извлечения обоих ID.

Ниже представлен PHP-скрипт обработки входящего вебхука, который безопасно извлекает chat_id и user_id, учитывая особенности структуры разных типов обновлений.

<?php
// Получаем сырые данные от вебхука
$rawInput = file_get_contents('php://input');
$update = json_decode($rawInput, true);

if (json_last_error() !== JSON_ERROR_NONE) {
error_log('Ошибка декодирования JSON: ' . json_last_error_msg());
http_response_code(400);
exit;
}

$chatId = null;
$userId = null;

if (isset($update['message'])) {
// Обычное текстовое сообщение, фото, документ и т.д.
$chatId = $update['message']['chat']['id'];
$userId = $update['message']['from']['id'];
} elseif (isset($update['callback_query'])) {
// Нажатие на inline-кнопку
$chatId = $update['callback_query']['message']['chat']['id'] ?? null;
$userId = $update['callback_query']['from']['id'];
} elseif (isset($update['channel_post'])) {
// Пост в канале (у каналов нет отправителя-пользователя в from.id)
$chatId = $update['channel_post']['chat']['id'];
$userId = null;
}

if ($chatId === null) {
// Если это техническое обновление без чата (например, inline_query)
http_response_code(200);
exit;
}

// Логируем результат для отладки
error_log(sprintf("Получен запрос. Chat ID: %s, User ID: %s", $chatId, $userId ?? 'NULL'));

Рассмотрим пример JSON-структуры входящего сообщения, отправленного пользователем в супергруппу. Обратите внимание на разницу между message.from.id (кто отправил) и message.chat.id (куда отправил):

{
"update_id": 876543210,
"message": {
"message_id": 452,
"from": {
"id": 987654321,
"is_bot": false,
"first_name": "Алексей",
"username": "alex_dev"
},
"chat": {
"id": -1001500300200,
"title": "PHP Чат",
"type": "supergroup"
},
"date": 1700000000,
"text": "Всем привет!"
}
}

Типы чатов и ветвление логики

Поле chat.type определяет, в каком контексте работает бот. Это критично, так как логика работы в личке и в группе должна различаться. Например, в личке бот может сразу отвечать на сообщения, а в группе — должен реагировать только на упоминания или команды.

<?php
$chatType = $update['message']['chat']['type'] ?? null;

switch ($chatType) {
case 'private':
// Личный диалог с пользователем.
// Здесь chat_id гарантированно равен user_id.
// Безопасно храним состояние FSM (конечного автомата) для этого пользователя.
break;

case 'group':
case 'supergroup':
// Групповой чат.
// chat_id отрицательный. Здесь могут находиться сотни пользователей.
// Нельзя сохранять личные состояния пользователя на весь chat_id.
break;

case 'channel':
// Публичный или приватный канал.
// Бот здесь выступает только как вещатель. Отправителя (from) обычно нет.
break;
}

Проверка прав бота через getChatMember

Перед тем как отправить сообщение в группу или канал, важно убедиться, что бот вообще имеет на это право (например, его не заблокировали или он является администратором в канале). Для этого используется метод getChatMember.

Чтобы узнать статус самого бота, нам нужно передать его собственный user_id. Самый простой способ получить user_id бота без лишних запросов — извлечь его из токена API. Токен всегда начинается с ID бота (число до двоеточия).

Используем функцию telegramApi() для отправки запросов к API. Её реализацию на основе cURL мы подробно разобрали в главе Первый запрос: getMe. Здесь мы вызываем её для проверки прав.

<?php
function getBotStatusInChat(string|int $chatId): ?string
{
$token = getenv('TELEGRAM_BOT_TOKEN');
if (!$token) {
error_log('TELEGRAM_BOT_TOKEN не задан в окружении');
return null;
}

// Извлекаем ID бота из токена (например, "123456:ABC-DEF" -> 123456)
$botUserId = (int) explode(':', $token)[0];

// Запрашиваем статус бота в конкретном чате
$response = telegramApi('getChatMember', [
'chat_id' => $chatId,
'user_id' => $botUserId
]);

if (!$response['ok']) {
error_log('Ошибка getChatMember: ' . ($response['description'] ?? 'Неизвестная ошибка'));
return null;
}

// Возвращает статус: "creator", "administrator", "member", "restricted", "left", "kicked"
return $response['result']['status'] ?? null;
}

// Пример использования перед отправкой сообщения
$status = getBotStatusInChat($chatId);
if ($status === 'kicked' || $status === 'left') {
error_log("Бот был удален из чата {$chatId}. Отправка невозможна.");
exit;
}

При успешном запросе getChatMember возвращает объект ChatMember. Вот пример ответа для бота, который является администратором группы:

{
"ok": true,
"result": {
"user": {
"id": 555666777,
"is_bot": true,
"first_name": "HelperBot",
"username": "my_helper_bot"
},
"status": "administrator",
"can_be_edited": false,
"can_manage_chat": true,
"can_change_info": true,
"can_post_messages": true,
"can_edit_messages": true,
"can_delete_messages": true,
"can_invite_users": true,
"can_restrict_members": true,
"can_pin_messages": true,
"can_manage_topics": true
}
}

Специфика PHP: Проблема переполнения 32-битных систем

Идентификаторы Telegram (как пользователей, так и чатов) уже давно превысили лимит 32-битных знаковых целых чисел (2 147 483 647). На данный момент ID укладываются в 64-битный диапазон.

Если ваш PHP-скрипт работает на устаревшей 32-битной платформе (или вы используете старые версии PHP), приведение большого ID к (int) приведет к искажению числа (переполнению). Для предотвращения этой проблемы соблюдайте три правила:

  1. Используйте 64-битный PHP. На современных 64-битных серверах тип int в PHP по умолчанию является 64-битным и легко вмещает любые ID от Telegram.
  2. Храните ID в базе данных как строки или BIGINT. В MySQL используйте тип BIGINT (или VARCHAR(64)), в PostgreSQL — bigint. Никогда не используйте обычный INT.
  3. Декодируйте JSON без потери точности. При использовании json_decode() на 32-битных системах передавайте флаг JSON_BIGINT_AS_STRING, чтобы огромные числа автоматически конвертировались в строки PHP, сохраняя точность:
<?php
// Безопасное декодирование на любых платформах
$update = json_decode($rawInput, true, 512, JSON_BIGINT_AS_STRING);

Как отлаживать входящие ID

Самый простой способ понять, какие именно ID приходят вашему боту — записывать сырой JSON от вебхука в файл лога. Вы сможете в реальном времени наблюдать за структурой данных с помощью утилиты tail в терминале Linux.

Запись входящих обновлений в файл:

<?php
$rawInput = file_get_contents('php://input');
if (!empty($rawInput)) {
// Записываем лог в файл рядом со скриптом
file_put_contents(__DIR__ . '/telegram_updates.log', $rawInput . PHP_EOL, FILE_APPEND);
}

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

tail -f telegram_updates.log | jq .

Дальше: Группы