Commit 2bd97b5f authored by Jack'lul's avatar Jack'lul

Merge branch 'develop' into no_default_commands

parents aec70278 f536baed
# Changelog
The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
## [0.41.0] - 2017-03-25
### Added
- `$show_in_help` attribute for commands, to set if it should be displayed in the `/help` command.
- Link to new Telegram group: `https://telegram.me/PHP_Telegram_Bot_Support`
- Introduce change log.
## [0.40.1] - 2017-03-07
### Fixed
- Infinite message loop, caused by incorrect Entity variable.
## [0.40.0] - 2017-02-20
### Added
- Request limiter for incoming requests.
### Fixed
- Faulty formatting in logger.
## [0.39.0] - 2017-01-20
### Added
- Newest bot API changes.
- Allow direct access to PDO object (`DB::getPdo()`).
- Simple `/debug` command that displays various system information to help debugging.
- Crontab-friendly script.
### Changed
- Botan integration improvements.
- Make logger more flexible.
### Fixed
- Various bugs and recommendations by Scrutinizer.
## [0.38.1] - 2016-12-25
### Fixed
- Usage of self-signed certificates in conjunction with the new `allowed_updates` webhook parameter.
## [0.38.0] - 2016-12-25
### Added
- New `switch_inline_query_current_chat` option for inline keyboard.
- Support for `channel_post` and `edited_channel_post`.
- New alias `deleteWebhook` (for `unsetWebhook`).
### Changed
- Update WebhookInfo entity and `setWebhook` to allow passing of new arguments.
## [0.37.1] - 2016-12-24
### Fixed
- Keyboards that are built without using the KeyboardButton objects.
- Commands that are called via `/command@botname` by correctly passing them the botname.
## [0.37.0] - 2016-12-13
### Changed
- Logging improvements to Botan integration.
### Deprecated
- Move `hideKeyboard` to `removeKeyboard`.
# PHP Telegram Bot
[![Join the chat at https://gitter.im/akalongman/php-telegram-bot](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/akalongman/php-telegram-bot?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Join the bot support group on Telegram](https://img.shields.io/badge/telegram-@PHP__Telegram__Bot__Support-32a2da.svg)](https://telegram.me/PHP_Telegram_Bot_Support)
[![Build Status](https://travis-ci.org/akalongman/php-telegram-bot.svg?branch=master)](https://travis-ci.org/akalongman/php-telegram-bot)
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/akalongman/php-telegram-bot/develop.svg?style=flat-square)](https://scrutinizer-ci.com/g/akalongman/php-telegram-bot/?b=develop)
......
......@@ -69,6 +69,10 @@ class HelpCommand extends UserCommand
);
foreach ($command_objs as $command) {
if (!$command->showInHelp()) {
continue;
}
$text .= sprintf(
'/%s - %s' . PHP_EOL,
$command->getName(),
......
......@@ -63,6 +63,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) {
......
......@@ -69,6 +69,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();
......
......@@ -68,6 +68,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) {
......
......@@ -59,6 +59,13 @@ abstract class Command
*/
protected $usage = 'Command usage';
/**
* Show in Help
*
* @var bool
*/
protected $show_in_help = true;
/**
* Version
*
......@@ -252,6 +259,16 @@ abstract class Command
return $this->name;
}
/**
* Get Show in Help
*
* @return bool
*/
public function showInHelp()
{
return $this->show_in_help;
}
/**
* Check if command is enabled
*
......
......@@ -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());
}
}
}
......@@ -56,7 +56,7 @@ class Message extends Entity
'from' => User::class,
'chat' => Chat::class,
'forward_from' => User::class,
'forward_from_chat' => User::class,
'forward_from_chat' => Chat::class,
'reply_to_message' => self::class,
'entities' => MessageEntity::class,
'audio' => Audio::class,
......
......@@ -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);
}
}
}
}
......@@ -30,7 +30,7 @@ class Telegram
*
* @var string
*/
protected $version = '0.39.0';
protected $version = '0.41.0';
/**
* Telegram API key
......@@ -841,6 +841,16 @@ class Telegram
return $this;
}
/**
* Enable requests limiter
*/
public function enableLimiter()
{
Request::setLimiter(true);
return $this;
}
/**
* Run provided commands
*
......
......@@ -288,7 +288,11 @@ class TelegramLog
// Pop the $text off the array, as it gets passed via func_get_args().
array_shift($args);
// Suppress warning if placeholders don't match out.
return @vsprintf($text, $args) ?: $text;
// If no placeholders have been passed, don't parse the text.
if (empty($args)) {
return $text;
}
return vsprintf($text, $args);
}
}
......@@ -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;
......@@ -127,6 +127,12 @@ class CommandTest extends TestCase
$this->assertTrue($this->command_stub->isEnabled());
}
public function testDefaultCommandShownInHelp()
{
$this->assertAttributeEquals(true, 'show_in_help', $this->command_stub);
$this->assertTrue($this->command_stub->showInHelp());
}
public function testDefaultCommandNeedsMysql()
{
$this->assertAttributeEquals(false, 'need_mysql', $this->command_stub);
......
......@@ -39,6 +39,9 @@ class CommandTestCase extends TestCase
{
$this->telegram = new Telegram('apikey', 'testbot');
$this->telegram->addCommandsPath(BASE_COMMANDS_PATH . '/UserCommands');
// Add custom commands dedicated to do some tests.
$this->telegram->addCommandsPath(__DIR__ . '/CustomTestCommands');
$this->telegram->getCommandsList();
}
......
<?php
/**
* This file is part of the TelegramBot package.
*
* (c) Avtandil Kikabidze aka LONGMAN <akalongman@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Longman\TelegramBot\Commands\UserCommands;
use Longman\TelegramBot\Commands\UserCommand;
use Longman\TelegramBot\Request;
/**
* Test "/hidden" command to test $show_in_help
*/
class HiddenCommand extends UserCommand
{
/**
* @var string
*/
protected $name = 'hidden';
/**
* @var string
*/
protected $description = 'This command is hidden in help';
/**
* @var string
*/
protected $usage = '/hidden';
/**
* @var string
*/
protected $version = '1.0.0';
/**
* @var bool
*/
protected $show_in_help = false;
/**
* Command execute method
*
* @return mixed
* @throws \Longman\TelegramBot\Exception\TelegramException
*/
public function execute()
{
return Request::emptyResponse();
}
}
<?php
/**
* This file is part of the TelegramBot package.
*
* (c) Avtandil Kikabidze aka LONGMAN <akalongman@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Longman\TelegramBot\Commands\UserCommands;
use Longman\TelegramBot\Commands\UserCommand;
use Longman\TelegramBot\Request;
/**
* Test "/visible" command to test $show_in_help
*/
class VisibleCommand extends UserCommand
{
/**
* @var string
*/
protected $name = 'visible';
/**
* @var string
*/
protected $description = 'This command is visible in help';
/**
* @var string
*/
protected $usage = '/visible';
/**
* @var string
*/
protected $version = '1.0.0';
/**
* @var bool
*/
protected $show_in_help = true;
/**
* Command execute method
*
* @return mixed
* @throws \Longman\TelegramBot\Exception\TelegramException
*/
public function execute()
{
return Request::emptyResponse();
}
}
......@@ -10,9 +10,9 @@
namespace Longman\TelegramBot\Tests\Unit\Commands\UserCommands;
use Longman\TelegramBot\Commands\UserCommands\HelpCommand;
use Longman\TelegramBot\Tests\Unit\Commands\CommandTestCase;
use Longman\TelegramBot\Tests\Unit\TestHelpers;
use Longman\TelegramBot\Commands\UserCommands\HelpCommand;
/**
* @package TelegramTest
......@@ -71,4 +71,15 @@ class HelpCommandTest extends CommandTestCase
->getText();
$this->assertContains("Description: Show text\nUsage: /echo <text>", $text);
}
public function testHelpCommandWithHiddenCommand()
{
$text = $this->command
->setUpdate(TestHelpers::getFakeUpdateCommandObject('/help'))
->execute()
->getResult()
->getText();
$this->assertContains('/visible', $text);
$this->assertNotContains('/hidden', $text);
}
}
......@@ -83,10 +83,12 @@ class TelegramLogTest extends TestCase
$this->assertFileNotExists($file);
TelegramLog::initErrorLog($file);
TelegramLog::error('my error');
TelegramLog::error('my 50% error');
TelegramLog::error('my %s error', 'placeholder');
$this->assertFileExists($file);
$error_log = file_get_contents($file);
$this->assertContains('bot_log.ERROR: my error', $error_log);
$this->assertContains('bot_log.ERROR: my 50% error', $error_log);
$this->assertContains('bot_log.ERROR: my placeholder error', $error_log);
}
......@@ -96,10 +98,12 @@ class TelegramLogTest extends TestCase
$this->assertFileNotExists($file);
TelegramLog::initDebugLog($file);
TelegramLog::debug('my debug');
TelegramLog::debug('my 50% debug');
TelegramLog::debug('my %s debug', 'placeholder');
$this->assertFileExists($file);
$debug_log = file_get_contents($file);
$this->assertContains('bot_log.DEBUG: my debug', $debug_log);
$this->assertContains('bot_log.DEBUG: my 50% debug', $debug_log);
$this->assertContains('bot_log.DEBUG: my placeholder debug', $debug_log);
}
......@@ -109,10 +113,12 @@ class TelegramLogTest extends TestCase
$this->assertFileNotExists($file);
TelegramLog::initUpdateLog($file);
TelegramLog::update('my update');
TelegramLog::update('my 50% update');
TelegramLog::update('my %s update', 'placeholder');
$this->assertFileExists($file);
$debug_log = file_get_contents($file);
$this->assertContains('my update', $debug_log);
$this->assertContains('my 50% update', $debug_log);
$this->assertContains('my placeholder update', $debug_log);
}
......@@ -127,15 +133,19 @@ class TelegramLogTest extends TestCase
TelegramLog::initialize($external_monolog);
TelegramLog::error('my error');
TelegramLog::error('my 50% error');
TelegramLog::error('my %s error', 'placeholder');
TelegramLog::debug('my debug');
TelegramLog::debug('my 50% debug');
TelegramLog::debug('my %s debug', 'placeholder');
$this->assertFileExists($file);
$file_contents = file_get_contents($file);
$this->assertContains('bot_update_log.ERROR: my error', $file_contents);
$this->assertContains('bot_update_log.ERROR: my 50% error', $file_contents);
$this->assertContains('bot_update_log.ERROR: my placeholder error', $file_contents);
$this->assertContains('bot_update_log.DEBUG: my debug', $file_contents);
$this->assertContains('bot_update_log.DEBUG: my 50% debug', $file_contents);
$this->assertContains('bot_update_log.DEBUG: my placeholder debug', $file_contents);
}
}
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