Commit bc45e044 authored by Armando Lüscher's avatar Armando Lüscher Committed by GitHub

Merge pull request #397 from jacklul/limiter_test

Request limiter - prevent hitting Telegram's API limits
parents 518e9dd8 331e1497
......@@ -60,6 +60,9 @@ try {
//$telegram->setDownloadPath('../Download');
//$telegram->setUploadPath('../Upload');
// Requests Limiter (tries to prevent reaching Telegram API limits)
$telegram->enableLimiter();
// Run user selected commands
$telegram->runCommands($commands);
} catch (Longman\TelegramBot\Exception\TelegramException $e) {
......
......@@ -66,6 +66,9 @@ try {
//$telegram->enableBotan('your_token');
//$telegram->enableBotan('your_token', ['timeout' => 3]);
// Requests Limiter (tries to prevent reaching Telegram API limits)
$telegram->enableLimiter();
// Handle telegram getUpdates request
$serverResponse = $telegram->handleGetUpdates();
......
......@@ -65,6 +65,9 @@ try {
//$telegram->enableBotan('your_token');
//$telegram->enableBotan('your_token', ['timeout' => 3]);
// Requests Limiter (tries to prevent reaching Telegram API limits)
$telegram->enableLimiter();
// Handle telegram webhook request
$telegram->handle();
} catch (Longman\TelegramBot\Exception\TelegramException $e) {
......
......@@ -136,6 +136,7 @@ class DB
'edited_message',
'inline_query',
'message',
'request_limiter',
'telegram_update',
'user',
'user_chat',
......@@ -1065,4 +1066,83 @@ class DB
throw new TelegramException($e->getMessage());
}
}
/**
* Get Telegram API request count for current chat / message
*
* @param integer $chat_id
* @param string $inline_message_id
*
* @return array|bool (Array containing TOTAL and CURRENT fields or false on invalid arguments)
* @throws \Longman\TelegramBot\Exception\TelegramException
*/
public static function getTelegramRequestCount($chat_id = null, $inline_message_id = null)
{
if (!self::isDbConnected()) {
return false;
}
try {
$sth = self::$pdo->prepare('SELECT
(SELECT COUNT(*) FROM `' . TB_REQUEST_LIMITER . '` WHERE `created_at` >= :date) as LIMIT_PER_SEC_ALL,
(SELECT COUNT(*) FROM `' . TB_REQUEST_LIMITER . '` WHERE ((`chat_id` = :chat_id AND `inline_message_id` IS NULL) OR (`inline_message_id` = :inline_message_id AND `chat_id` IS NULL)) AND `created_at` >= :date) as LIMIT_PER_SEC,
(SELECT COUNT(*) FROM `' . TB_REQUEST_LIMITER . '` WHERE `chat_id` = :chat_id AND `created_at` >= :date_minute) as LIMIT_PER_MINUTE
');
$date = self::getTimestamp(time());
$date_minute = self::getTimestamp(strtotime('-1 minute'));
$sth->bindParam(':chat_id', $chat_id, \PDO::PARAM_STR);
$sth->bindParam(':inline_message_id', $inline_message_id, \PDO::PARAM_STR);
$sth->bindParam(':date', $date, \PDO::PARAM_STR);
$sth->bindParam(':date_minute', $date_minute, \PDO::PARAM_STR);
$sth->execute();
return $sth->fetch();
} catch (\Exception $e) {
throw new TelegramException($e->getMessage());
}
}
/**
* Insert Telegram API request in db
*
* @param string $method
* @param array $data
*
* @return bool If the insert was successful
* @throws \Longman\TelegramBot\Exception\TelegramException
*/
public static function insertTelegramRequest($method, $data)
{
if (!self::isDbConnected()) {
return false;
}
$chat_id = ((isset($data['chat_id'])) ? $data['chat_id'] : null);
$inline_message_id = (isset($data['inline_message_id']) ? $data['inline_message_id'] : null);
try {
$sth = self::$pdo->prepare('INSERT INTO `' . TB_REQUEST_LIMITER . '`
(
`method`, `chat_id`, `inline_message_id`, `created_at`
)
VALUES (
:method, :chat_id, :inline_message_id, :date
);
');
$created_at = self::getTimestamp();
$sth->bindParam(':chat_id', $chat_id, \PDO::PARAM_STR);
$sth->bindParam(':inline_message_id', $inline_message_id, \PDO::PARAM_STR);
$sth->bindParam(':method', $method, \PDO::PARAM_STR);
$sth->bindParam(':date', $created_at, \PDO::PARAM_STR);
return $sth->execute();
} catch (\Exception $e) {
throw new TelegramException($e->getMessage());
}
}
}
......@@ -46,6 +46,13 @@ class Request
*/
private static $input;
/**
* Request limiter
*
* @var boolean
*/
private static $limiter_enabled;
/**
* Available actions to send
*
......@@ -318,6 +325,8 @@ class Request
self::ensureNonEmptyData($data);
self::limitTelegramRequests($action, $data);
$response = json_decode(self::execute($action, $data), true);
if (null === $response) {
......@@ -976,4 +985,76 @@ class Request
// Must send some arbitrary data for this to work for now...
return self::send('getWebhookInfo', ['info']);
}
/**
* Enable request limiter
*
* @param boolean $value
*/
public static function setLimiter($value = true)
{
if (DB::isDbConnected()) {
self::$limiter_enabled = $value;
}
}
/**
* This functions delays API requests to prevent reaching Telegram API limits
* Can be disabled while in execution by 'Request::setLimiter(false)'
*
* @link https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this
*
* @param string $action
* @param array $data
*
* @throws \Longman\TelegramBot\Exception\TelegramException
*/
private static function limitTelegramRequests($action, array $data = [])
{
if (self::$limiter_enabled) {
$limited_methods = [
'sendMessage',
'forwardMessage',
'sendPhoto',
'sendAudio',
'sendDocument',
'sendSticker',
'sendVideo',
'sendVoice',
'sendLocation',
'sendVenue',
'sendContact',
'editMessageText',
'editMessageCaption',
'editMessageReplyMarkup',
];
$chat_id = isset($data['chat_id']) ? $data['chat_id'] : null;
$inline_message_id = isset($data['inline_message_id']) ? $data['inline_message_id'] : null;
if (($chat_id || $inline_message_id) && in_array($action, $limited_methods)) {
$timeout = 60;
while (true) {
if ($timeout <= 0) {
throw new TelegramException('Timed out while waiting for a request spot!');
}
$requests = DB::getTelegramRequestCount($chat_id, $inline_message_id);
if ($requests['LIMIT_PER_SEC'] == 0 // No more than one message per second inside a particular chat
&& ((($chat_id > 0 || $inline_message_id) && $requests['LIMIT_PER_SEC_ALL'] < 30) // No more than 30 messages per second globally
|| ($chat_id < 0 && $requests['LIMIT_PER_MINUTE'] < 20))
) {
break;
}
$timeout--;
sleep(1);
}
DB::insertTelegramRequest($action, $data);
}
}
}
}
......@@ -841,6 +841,16 @@ class Telegram
return $this;
}
/**
* Enable requests limiter
*/
public function enableLimiter()
{
Request::setLimiter(true);
return $this;
}
/**
* Run provided commands
*
......
......@@ -212,3 +212,13 @@ CREATE TABLE IF NOT EXISTS `botan_shortener` (
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
CREATE TABLE IF NOT EXISTS `request_limiter` (
`id` bigint UNSIGNED AUTO_INCREMENT COMMENT 'Unique identifier for this entry',
`chat_id` char(255) NULL DEFAULT NULL COMMENT 'Unique chat identifier',
`inline_message_id` char(255) NULL DEFAULT NULL COMMENT 'Identifier of the sent inline message',
`method` char(255) DEFAULT NULL COMMENT 'Request method',
`created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT charSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment