Skip to content

Commit bc45e04

Browse files
authored
Merge pull request #397 from jacklul/limiter_test
Request limiter - prevent hitting Telegram's API limits
2 parents 518e9dd + 331e149 commit bc45e04

File tree

7 files changed

+190
-0
lines changed

7 files changed

+190
-0
lines changed

examples/cron.php

+3
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@
6060
//$telegram->setDownloadPath('../Download');
6161
//$telegram->setUploadPath('../Upload');
6262

63+
// Requests Limiter (tries to prevent reaching Telegram API limits)
64+
$telegram->enableLimiter();
65+
6366
// Run user selected commands
6467
$telegram->runCommands($commands);
6568
} catch (Longman\TelegramBot\Exception\TelegramException $e) {

examples/getUpdatesCLI.php

+3
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@
6666
//$telegram->enableBotan('your_token');
6767
//$telegram->enableBotan('your_token', ['timeout' => 3]);
6868

69+
// Requests Limiter (tries to prevent reaching Telegram API limits)
70+
$telegram->enableLimiter();
71+
6972
// Handle telegram getUpdates request
7073
$serverResponse = $telegram->handleGetUpdates();
7174

examples/hook.php

+3
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@
6565
//$telegram->enableBotan('your_token');
6666
//$telegram->enableBotan('your_token', ['timeout' => 3]);
6767

68+
// Requests Limiter (tries to prevent reaching Telegram API limits)
69+
$telegram->enableLimiter();
70+
6871
// Handle telegram webhook request
6972
$telegram->handle();
7073
} catch (Longman\TelegramBot\Exception\TelegramException $e) {

src/DB.php

+80
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ protected static function defineTables()
136136
'edited_message',
137137
'inline_query',
138138
'message',
139+
'request_limiter',
139140
'telegram_update',
140141
'user',
141142
'user_chat',
@@ -1065,4 +1066,83 @@ public static function selectChats(
10651066
throw new TelegramException($e->getMessage());
10661067
}
10671068
}
1069+
1070+
/**
1071+
* Get Telegram API request count for current chat / message
1072+
*
1073+
* @param integer $chat_id
1074+
* @param string $inline_message_id
1075+
*
1076+
* @return array|bool (Array containing TOTAL and CURRENT fields or false on invalid arguments)
1077+
* @throws \Longman\TelegramBot\Exception\TelegramException
1078+
*/
1079+
public static function getTelegramRequestCount($chat_id = null, $inline_message_id = null)
1080+
{
1081+
if (!self::isDbConnected()) {
1082+
return false;
1083+
}
1084+
1085+
try {
1086+
$sth = self::$pdo->prepare('SELECT
1087+
(SELECT COUNT(*) FROM `' . TB_REQUEST_LIMITER . '` WHERE `created_at` >= :date) as LIMIT_PER_SEC_ALL,
1088+
(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,
1089+
(SELECT COUNT(*) FROM `' . TB_REQUEST_LIMITER . '` WHERE `chat_id` = :chat_id AND `created_at` >= :date_minute) as LIMIT_PER_MINUTE
1090+
');
1091+
1092+
$date = self::getTimestamp(time());
1093+
$date_minute = self::getTimestamp(strtotime('-1 minute'));
1094+
1095+
$sth->bindParam(':chat_id', $chat_id, \PDO::PARAM_STR);
1096+
$sth->bindParam(':inline_message_id', $inline_message_id, \PDO::PARAM_STR);
1097+
$sth->bindParam(':date', $date, \PDO::PARAM_STR);
1098+
$sth->bindParam(':date_minute', $date_minute, \PDO::PARAM_STR);
1099+
1100+
$sth->execute();
1101+
1102+
return $sth->fetch();
1103+
} catch (\Exception $e) {
1104+
throw new TelegramException($e->getMessage());
1105+
}
1106+
}
1107+
1108+
/**
1109+
* Insert Telegram API request in db
1110+
*
1111+
* @param string $method
1112+
* @param array $data
1113+
*
1114+
* @return bool If the insert was successful
1115+
* @throws \Longman\TelegramBot\Exception\TelegramException
1116+
*/
1117+
public static function insertTelegramRequest($method, $data)
1118+
{
1119+
if (!self::isDbConnected()) {
1120+
return false;
1121+
}
1122+
1123+
$chat_id = ((isset($data['chat_id'])) ? $data['chat_id'] : null);
1124+
$inline_message_id = (isset($data['inline_message_id']) ? $data['inline_message_id'] : null);
1125+
1126+
try {
1127+
$sth = self::$pdo->prepare('INSERT INTO `' . TB_REQUEST_LIMITER . '`
1128+
(
1129+
`method`, `chat_id`, `inline_message_id`, `created_at`
1130+
)
1131+
VALUES (
1132+
:method, :chat_id, :inline_message_id, :date
1133+
);
1134+
');
1135+
1136+
$created_at = self::getTimestamp();
1137+
1138+
$sth->bindParam(':chat_id', $chat_id, \PDO::PARAM_STR);
1139+
$sth->bindParam(':inline_message_id', $inline_message_id, \PDO::PARAM_STR);
1140+
$sth->bindParam(':method', $method, \PDO::PARAM_STR);
1141+
$sth->bindParam(':date', $created_at, \PDO::PARAM_STR);
1142+
1143+
return $sth->execute();
1144+
} catch (\Exception $e) {
1145+
throw new TelegramException($e->getMessage());
1146+
}
1147+
}
10681148
}

src/Request.php

+81
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ class Request
4646
*/
4747
private static $input;
4848

49+
/**
50+
* Request limiter
51+
*
52+
* @var boolean
53+
*/
54+
private static $limiter_enabled;
55+
4956
/**
5057
* Available actions to send
5158
*
@@ -318,6 +325,8 @@ public static function send($action, array $data = [])
318325

319326
self::ensureNonEmptyData($data);
320327

328+
self::limitTelegramRequests($action, $data);
329+
321330
$response = json_decode(self::execute($action, $data), true);
322331

323332
if (null === $response) {
@@ -976,4 +985,76 @@ public static function getWebhookInfo()
976985
// Must send some arbitrary data for this to work for now...
977986
return self::send('getWebhookInfo', ['info']);
978987
}
988+
989+
/**
990+
* Enable request limiter
991+
*
992+
* @param boolean $value
993+
*/
994+
public static function setLimiter($value = true)
995+
{
996+
if (DB::isDbConnected()) {
997+
self::$limiter_enabled = $value;
998+
}
999+
}
1000+
1001+
/**
1002+
* This functions delays API requests to prevent reaching Telegram API limits
1003+
* Can be disabled while in execution by 'Request::setLimiter(false)'
1004+
*
1005+
* @link https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this
1006+
*
1007+
* @param string $action
1008+
* @param array $data
1009+
*
1010+
* @throws \Longman\TelegramBot\Exception\TelegramException
1011+
*/
1012+
private static function limitTelegramRequests($action, array $data = [])
1013+
{
1014+
if (self::$limiter_enabled) {
1015+
$limited_methods = [
1016+
'sendMessage',
1017+
'forwardMessage',
1018+
'sendPhoto',
1019+
'sendAudio',
1020+
'sendDocument',
1021+
'sendSticker',
1022+
'sendVideo',
1023+
'sendVoice',
1024+
'sendLocation',
1025+
'sendVenue',
1026+
'sendContact',
1027+
'editMessageText',
1028+
'editMessageCaption',
1029+
'editMessageReplyMarkup',
1030+
];
1031+
1032+
$chat_id = isset($data['chat_id']) ? $data['chat_id'] : null;
1033+
$inline_message_id = isset($data['inline_message_id']) ? $data['inline_message_id'] : null;
1034+
1035+
if (($chat_id || $inline_message_id) && in_array($action, $limited_methods)) {
1036+
$timeout = 60;
1037+
1038+
while (true) {
1039+
if ($timeout <= 0) {
1040+
throw new TelegramException('Timed out while waiting for a request spot!');
1041+
}
1042+
1043+
$requests = DB::getTelegramRequestCount($chat_id, $inline_message_id);
1044+
1045+
if ($requests['LIMIT_PER_SEC'] == 0 // No more than one message per second inside a particular chat
1046+
&& ((($chat_id > 0 || $inline_message_id) && $requests['LIMIT_PER_SEC_ALL'] < 30) // No more than 30 messages per second globally
1047+
|| ($chat_id < 0 && $requests['LIMIT_PER_MINUTE'] < 20))
1048+
) {
1049+
break;
1050+
}
1051+
1052+
$timeout--;
1053+
sleep(1);
1054+
}
1055+
1056+
DB::insertTelegramRequest($action, $data);
1057+
}
1058+
}
1059+
}
9791060
}

src/Telegram.php

+10
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,16 @@ public function enableBotan($token, array $options = [])
841841
return $this;
842842
}
843843

844+
/**
845+
* Enable requests limiter
846+
*/
847+
public function enableLimiter()
848+
{
849+
Request::setLimiter(true);
850+
851+
return $this;
852+
}
853+
844854
/**
845855
* Run provided commands
846856
*

structure.sql

+10
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,13 @@ CREATE TABLE IF NOT EXISTS `botan_shortener` (
212212

213213
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
214214
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
215+
216+
CREATE TABLE IF NOT EXISTS `request_limiter` (
217+
`id` bigint UNSIGNED AUTO_INCREMENT COMMENT 'Unique identifier for this entry',
218+
`chat_id` char(255) NULL DEFAULT NULL COMMENT 'Unique chat identifier',
219+
`inline_message_id` char(255) NULL DEFAULT NULL COMMENT 'Identifier of the sent inline message',
220+
`method` char(255) DEFAULT NULL COMMENT 'Request method',
221+
`created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation',
222+
223+
PRIMARY KEY (`id`)
224+
) ENGINE=InnoDB DEFAULT charSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;

0 commit comments

Comments
 (0)