Наш канал в telegram

Инструкция по созданию Telegram ботов. Часть 5. Пишем Telegram бота на php для работы через longpolling

Введение

Чуть меньше года назад я обещал написать, как сделать на php телеграм-бота, работающего через longpolling. И вот, наконец-то, у меня дошли руки, чтобы выполнить это обещание.

В чём идея этого метода и чем он принципиально отличается от вебхуков, я уже описывал в первой части этой статьи. Главный смысл тут в том, что данные не присылают по заранее указанному адресу, а мы должны сами посылать на сервера телеграм запросы на их получение. На самом деле я бы назвал этот способ просто polling, поскольку запросы могут быть как «долгими» (или «длинными», — longpolling), так и обычными (или «короткими», — shortpolling).

Разница между «длинными» и обычными опросами состоит в том, что в случае с «длинными» опросами клиент может ждать ответ на отправленный запрос в течении довольно длительного, заранее указанного времени (всё это время соединение не будет разрываться), а в случае с обычными запросами ответ должен поступить сразу, иначе клиент просто будет считать, что сервер не отвечает. В случае с telegram это означает следующее: для «длинных» опросов при отсутствии новых апдейтов сервер telegram сначала подождёт указанное время и если за это время апдейты не появились, то пришлёт ответ с пустым массивом, а для обычных опросов при отсутствии новых апдейтов сервер telegram сразу пришлёт ответ с пустым массивом.

Учтите, что оба способа одновременно (вебхуки и поллинги) использоваться не могут, — если установлен вебхук, то отвечать на запросы серверы телеграм не будут (как снять установленный ранее вебхук читайте в статье про работу с telegram через вебхуки).

Какие преимущества мы получаем при использовании поллингов вместо вебхуков? Ну, самое главное, что нам в этом случае не нужен обратный адрес (хостинг и доменное имя), то есть можно хоть на домашнем компе Telegram-бота поднять. Есть и, как бы это сказать, не недостатки, но дополнительные сложности, — апдейты теперь будут прилетать не по одному, а массивами, соответственно, сначала нужно будет разобрать полученные данные на отдельные апдейты, а потом уже обрабатывать их аналогично тому, как мы это делали с вебхуками.

На этом закончим с введением и перейдём к более конкретным вещам, — разберём API телеграмма для работы через поллинги.

API telegram для работы через поллинги

Итак, для получения массива апдейтов нужно воспользоваться методом getUpdates и отправить на сервера телеграм запрос следующего вида: https://api.telegram.org/botYOUR_BOT_TOKEN/getUpdates[?options], где YOUR_BOT_TOKEN — токен вашего бота, options — список дополнительных опций.

Опции могут быть следующими:

  • offset — идентификатор первого запрашиваемого апдейта. С этой опцией будут получены все апдейты, начиная с апдейта с указанным в поле offset идентификатором. То есть, для того, чтобы получать все апдейты — мы должны каждый раз в запросе указывать идентификатор на единицу больше, чем идентификатор (update_id) последнего полученного апдейта. Все апдейты с идентификаторами меньше присланного значения offset «забываются»;
  • limit — максимальное количество апдейтов, которое будет прислано за один раз. Может принимать значения от 1 до 100 (по умолчанию 100).
  • timeout — время ожидания для «длинного» запроса. По умолчанию = 0 (то есть по умолчанию используется обычный опрос);
  • allowed_updates — тип сообщений, которые мы хотели бы получать (например можно получать только сообщения типа message). Эта опция добавлена недавно и на момент написания этой статьи мне не удалось добиться её заявленной работы (выбранные типы сообщений приходят не от всех пользователей).

Вот и весь API, можно переходить к переделыванию нашего бота, описанного в четвёртой части.

Переделываем нашего telegram-бота на php для работы через поллинги

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

  • Нужно написать код, формирующий и отправляющий запрос на сервера телеграм. Здесь следует учесть, что функция file_get_content не умеет работать через https (или я не смог разобраться как это сделать). К счастью, у нас есть curl, который умеет всё, что нам нужно.
  • Полученный ответ нужно будет разобрать на отдельные апдейты. Здесь нам поможет уже известная нам функция json_decode.
  • Далее нужно будет перебрать все апдейты (их количество можно подсчитать функцией count) и обработать их аналогично тому, как это было в случае с вебхуками.
  • Для каждого апдейта нужно извлекать update_id и сохранять в БД update_id последнего полученного апдейта. Это нужно для того, чтобы далее, при формировании запроса, мы могли извлечь из БД сохранённый там ранее update_id и прибавив к нему единицу сообщить в запросе, какие апдейты нам нужны (параметр offset).

Для сохранения в базе данных MySQL значения update_id заведём отдельную таблицу tlgrm_updates с полями update_id и number, типа integer. Нужное нам значение update_id будем хранить в ячейке update_id с индексом number=0.

Всё, теперь осталось только написать код.

Первое. Заменим file_get_content на curl. Для этого напишем новую функцию execRequest такого вида:

Код под катом

function execRequest($telegram_req_url){
	$telegram_ch = curl_init();
	curl_setopt($telegram_ch, CURLOPT_URL, $telegram_req_url);
	curl_setopt($telegram_ch, CURLOPT_RETURNTRANSFER, 1);
	curl_setopt($telegram_ch, CURLOPT_HTTPGET, true);		// необязательно
	curl_setopt($telegram_ch, CURLOPT_SSL_VERIFYPEER, false);	// отменяем проверку сертификатов
	curl_setopt($telegram_ch, CURLOPT_SSL_VERIFYHOST, false);	// (это для тестов, ну а что делать)
	curl_setopt($telegram_ch, CURLOPT_MAXREDIRS, 10);		// необязательно
	curl_setopt($telegram_ch, CURLOPT_CONNECTTIMEOUT, 5);		// необязательно (таймаут попытки подключения)
	curl_setopt($telegram_ch, CURLOPT_TIMEOUT, 20);			// необязательно (таймаут выполнения запроса)
 
	$telegram_ch_result = curl_exec($telegram_ch);
	return $telegram_ch_result;
}

[свернуть]

Теперь во всех местах, где мы пользовались функцией file_get_content будем просто формировать нужный url, а сам запрос выполнять новой функцией execRequest, задавая для неё в качестве параметра нужный url.

Второе. Код обработки апдейта, который у нас ранее располагался в конструкции вот такого вида:

Код под катом

$input_array = json_decode(file_get_contents('php://input'),TRUE)
if($input_array){
 
	код обработки апдейта
 
}
else{
    print("Hello, I am bot! My name is @[BOTNAME]. Bla-bla-bla...");
}

[свернуть]

теперь вместо этого заключим вот в такую конструкцию:

Код под катом

//**************************************************************************
//запрашиваем из БД update_id - идентификатор последнего полученного апдейта
//**************************************************************************
// пытаемся подключиться к БД
$telegram_mysqli = new mysqli($dbhost, $dbuser, $dbpwd, $dbname);
if ($mysqli->connect_errno) {
	//sendMessage($admin_chat_id,'Не удалось подключиться к БД ('.$mysqli->connect_errno.': '.$mysqli->connect_error.') для чтения update_id');
	exit();	// если не получилось - выходим
}
 
$sel_number = 2;		// номер интересующего нас запроса
$sql ="";			// здесь формируем строку запроса
$sql .= 'SET NAMES utf8;';						// запрос 1 - выставляем кодировку
$sql .= 'SELECT update_id FROM tlgrm_updates WHERE number="0";';	// запрос 2 - формируем запрос на чтение update_id из базы
 
if (!$mysqli->multi_query($sql)) {	// пытаемся выполнить мультизапрос
	sendMessage($admin_chat_id,'Не удалось выполнить мультизапрос ('.$mysqli->errno.': '.$mysqli->error.') для чтения update_id');
	exit();	// если не получилось - выходим, подключение отвалится само при завершении скрипта
}
 
$update_id = NULL;		// вначале номер последнего прочитанного апдейта нам неизвестен
$counter = 0;			// инициализируем счётчик обрабатываемых результатов
do {	$counter += 1;					// увеличиваем счётчик
		$res = $mysqli->store_result();		// получаем результат i-того запроса
		if ($res){				// если результат запроса не нулевой
			if($counter == $sel_number){	// если это запрос select update_id
				$row = $res->fetch_row();	// получаем первую строку ответа на очередной запрос (она у нас всего одна и должна быть)
				if(isset($row[0])){		// если нулевой элемент этой строки существует (значение update_id, поскольку мы запрашивали только его),
					$update_id = $row[0];	// то update_id равен прочитанному
				}
			}
			$res->free();				// если результат был не пустой, то освобождаем его
		}
} while ($mysqli->more_results() && $mysqli->next_result());	//перебираем все результаты мультизапроса
$mysqli->close();		// закрываем подключение к базе
 
// проверяем что у нас получилось
if(!isset($update_id)){		// если номер последнего апдейта не прочитан
	sendMessage($admin_chat_id,'Не удалось прочитать update_id.');	//сообщаем админу
	exit();					//и выходим
}
 
//**************************************************************************
//посылаем запрос getUpdates серверу телеграм, в качестве параметра указываем offset = update_id+1,
//получаем ответ с массивом объектов update
//**************************************************************************
$update_id += 1;			// увеличиваем прочитанный номер на 1 (вычисляем offset)
$getUpdates_url = $bot_api.'/getUpdates?offset='.$update_id.'&limit=10';	// формируем адрес для запроса апдейтов (максимум 10 штук)
$answer_source = execRequest($getUpdates_url);		// запрашиваем последние апдейты (получаем массив апдейтов в виде json)
$answer = json_decode($answer_source, TRUE);		// распарсиваем в ассоциативный массив
if($answer){				// если всё нормально получено и распарсено
 
//**************************************************************************
//вычисляем количество прилетевших апдейтов
//**************************************************************************
	$number_of_updates = 0;			// начальное значение ноль
	$number_of_updates = count($answer['result']);	// количество элементов массива result
	if($number_of_updates==0){		// если новых апдейтов нет
		exit();						// выходим
	}
//**************************************************************************
//перебираем все объекты update (которые также представляют из себя массивы) и
//обрабатываем каждый из них так же, как для случая с вебхуками
//**************************************************************************
	foreach($answer['result'] as $update_value){
		if(isset($update_value['message']['from']['id'])){	// если в сообщении есть идентификатор юзера - обрабатываем это сообщение
 
			код обработки апдейта
 
		}
	}
//**************************************************************************
// здесь надо сохранить себе в базу очередное значение $update_id
//**************************************************************************
	// пытаемся подключиться к БД
	$mysqli = new mysqli($dbhost, $dbuser, $dbpwd, $dbname);
	if ($mysqli->connect_errno) {
		sendMessage($admin_chat_id,'Не удалось подключиться к БД ('.$mysqli->connect_errno.': '.$mysqli->connect_error.') для записи update_id');
		exit();	// если не получилось - выходим
	}
 
	$sql ="";					// здесь формируем строку запроса
	$sql .= 'SET NAMES utf8;';			// запрос 1 - выставляем кодировку
	$sql .= 'UPDATE IGNORE tlgrm_updates SET update_id="'.$update_id.'" WHERE number="0";'; // запрос 2 - формируем запрос на обновление update_id в базе
 
	if (!$mysqli->multi_query($sql)) {	// пытаемся выполнить мультизапрос
		sendMessage($admin_chat_id,'Не удалось выполнить мультизапрос ('.$mysqli->errno.': '.$mysqli->error.') для записи update_id');
		exit();	// если не получилось - выходим, подключение отвалится само при завершении скрипта
	}
	$mysqli->close();				// закрываем подключение к базе
	// Тут есть один нюанс. Если весь скрипт нормально выполнился, но не выполнилась запись в БД
	// нового значения update_id, то в следующий раз снова будут обрабатываться те же самые мессаджи
	// но лучше уж мессаджи дважды обработать, чем совсем потерять
}
else{	// если во время чтения и распарсивания массива апдейтов произошла ошибка
	print("Hello, I am bot! My name is @[BOTNAME]. Bla-bla-bla...");
}

[свернуть]

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

Замечания и дополнения

В дополнение ко всему изложенному хотелось бы добавить несколько замечаний:

  • Как известно, в нашей стране Telegram в настоящее время запрещён, его пытается лочить РКН. Однако, РКН не умеет лочить IPv6. То есть, если вы подключены к интернету по IPv4, то ваши запросы могут не доходить до серверов Telegram. Чтобы они до серверов Telegram доходили — придётся подключаться к интернету по IPv6 (если провайдер позволяет) или что-то колхозить с vpn и проксяшниками.
  • Вот здесь можно почитать про пример организации домашней сети таким образом, чтобы одновременно пользоваться плюшками и от IPv6, и от IPv4.
  • Если домашний роутер раздаёт IPv6, то можно поднять бота на отдельном, воткнутом в домашнюю сеть микрокомпьютере (например Omega2), который будет включен в режиме 24/7, и будет заниматься только этим ботом. К сожалению, родная прошивка микрокомпьютера Omega2 не поддерживает IPv6, однако его поддерживает вот эта самодельная прошивка от виртуала. Её также можно скачать с нашего форума (там же можно почитать подробнее о том, как с ней работать).
  • Для запуска на микрокомпьютере Omega2 описанного в этой статье Telegram-бота с использованием интерпретатора php7, на омеге должны быть установлены следующие модули:
  • Список под катом
    • php7
    • php7-cgi
    • php7-fastcgi
    • php7-mod-curl
    • php7-mod-hash
    • php7-mod-iconv
    • php7-mod-json
    • php7-mod-mysqli
    • php7-mod-mysqlnd
    • php7-mod-openssl
    • php7-mod-sockets
    • php7-mod-xml

    [свернуть]
  1. Часть 1. Что такое Telegram боты и как они работают
  2. Часть 2. Регистрация аккаунтов Telegram ботов в картинках
  3. Часть 3. Пишем простого чат-бота для Telegram на чистом php (webhook)
  4. Часть 4. Прикручиваем MySQL к чат-боту для Telegram на php (webhook)
  5. Часть 5. Пишем Telegram бота на php для работы через longpolling

Добавить комментарий