Completed
Push — master ( 8cfeb9...1b1a3a )
by Danilo
02:16
created

CoreBot::execRequest()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 28
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 18
nc 5
nop 2
dl 0
loc 28
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
namespace PhpBotFramework\Core;
4
5
use \PhpBotFramework\Exceptions\BotException;
6
7
use \PhpBotFramework\Entities\InlineKeyboard;
8
9
/**
10
 * \mainpage
11
 * \section Description
12
 * PhpBotFramework is a lightweight framework for [Telegram Bot API](https://core.telegram.org/bots/api).
13
 * Designed to be fast and easy to use, it provides all the features a user need in order to start
14
 * developing Telegram bots..
15
 *
16
 * \section Installation
17
 * You can install PhpBotFramework using **Composer**.
18
 *
19
 * Go to your project's folder and type:
20
 *
21
 *     composer require danyspin97/php-bot-framework
22
 *     composer install --no-dev
23
 *
24
 * \section Usage
25
 * You can start working on your bot creating a new instance of Bot or by creating a
26
 * class that inherits from it.
27
 *
28
 * Each API call will have <code>$_chat_id</code> set to the current user:
29
 * you can use CoreBot::setChatID() to change it.
30
 *
31
 * Below an example bot you can look to:
32
 *
33
 *
34
 *     <?php
35
 *
36
 *     // Include the framework
37
 *     require './vendor/autoload.php';
38
 *
39
 *     // Create the bot
40
 *     $bot = new PhpBotFramework\Bot("token");
41
 *
42
 *     // Add a command that will be triggered every time the user send /start
43
 *     $bot->addMessageCommand("start", function($bot, $message) {
44
 *         $bot->sendMessage("Hello, folks!");
45
 *     });
46
 *
47
 *     // Receive updates from Telegram using getUpdates
48
 *     $bot->getUpdatesLocal();
49
 *
50
 * \subsection Bot-Intherited Inheriting by Bot class
51
 *
52
 *     <?php
53
 *
54
 *     // Include the framework
55
 *     require './vendor/autoload.php';
56
 *
57
 *     // Create the class that will extends Bot class
58
 *     class EchoBot extends PhpBotFramework\Bot {
59
 *
60
 *         // Add the function for processing messages
61
 *         protected function processMessage($message) {
62
 *
63
 *             // Answer each message with the text received
64
 *             $this->sendMessage($message['text']);
65
 *         }
66
 *     }
67
 *
68
 *     $bot = new EchoBot("token");
69
 *
70
 *     // Process updates using webhook
71
 *     $bot->processWebhookUpdate();
72
 *
73
 * Override these method to make your bot handle each update type:
74
 * - Bot::processMessage($message)
75
 * - Bot::processCallbackQuery($callback_query)
76
 * - Bot::processInlineQuery($inline_query)
77
 * - Bot::processChosenInlineResult($_chosen_inline_result)
78
 * - Bot::processEditedMessage($edited_message)
79
 * - Bot::processChannelPost($post)
80
 * - Bot::processEditedChannelPost($edited_post)
81
 *
82
 * \section Features
83
 * - Modular: take only what you need
84
 * - Flexible HTTP requests with [Guzzle](https://github.com/guzzle/guzzle)
85
 * - Designed to be fast and easy to use
86
 * - Support for local updates and webhooks
87
 * - Support for the most important API methods
88
 * - Command-handle system for messages and callback queries
89
 * - Update type based processing
90
 * - Easy **inline keyboard** creation
91
 * - Inline query results handler
92
 * - Database support and facilities
93
 * - Redis support
94
 * - Support for multilanguage bots
95
 * - Support for bot states
96
 * - Highly-documented
97
 *
98
 * \section Requirements
99
 * - PHP >= 7.0
100
 * - php-mbstring
101
 * - Composer (to install the framework)
102
 * - Web server: *required for webhook* (we recommend [nginx](http://nginx.org/))
103
 * - SSL certificate: *required for webhook* (follow [these steps](https://devcenter.heroku.com/articles/ssl-certificate-self) to make a self-signed certificate or use [Let's Encrypt](https://letsencrypt.org/))
104
 *
105
 * \section GetUpdates-section Getting updates
106
 * Everytime a user interacts with the bot, an `update` is generated by Telegram's servers.
107
 *
108
 * There are two ways of receiving this updates:
109
 * - use [Telegram Bot API's `getUpdates`](https://core.telegram.org/bots/api#getupdates) method
110
 * - use webhooks (it's covered in the next section)
111
 *
112
 * If you want to use `getUpdates` in order to receive updates,
113
 * add one of these functions at the end of your bot:
114
 * - Bot::getUpdatesLocal()
115
 * - Bot::getUpdatesDatabase()
116
 * - Bot::getUpdatesRedis()
117
 *
118
 * The bot will process updates one a time and will call Bot::processUpdate() for each.
119
 *
120
 * The connection will be opened at the creation and used for the entire life of the bot.
121
 *
122
 * \section Webhook-section Webhook
123
 * An alternative way to receive updates is using **webhooks**.
124
 *
125
 * Everytime a user interacts with the bot, Telegram servers send the update through
126
 * a POST request to a URL chose by you.
127
 *
128
 * A web server will create an instance of the bot for every update received.
129
 *
130
 * If you want to use webhook: call Bot::processWebhookUpdate() at the end of your bot.
131
 *
132
 * The bot will get data from <code>php://input</code> and process it using Bot::processUpdate().
133
 * Each instance of the bot will open its connection.
134
 *
135
 * \subsection Setwebhooks-subsection Set webhook
136
 * You can set a URL for your bot's webhook using CoreBot::setWebhook():
137
 *
138
 *     //...
139
 *     $bot->setWebhook([ 'url' => 'https://example.com/mybotSECRETPATH' ])
140
 *
141
 * You can learn more about `setWebhook` and webhooks [here](https://core.telegram.org/bots/api#setwebhook).
142
 *
143
 * \section Message-commands Bot's commands
144
 * One of the most important tasks during a Telegram bot's development is register
145
 * the commands the bot will respond to.
146
 *
147
 * PhpBotFrameworks makes it easy:
148
 *
149
 *     $bot->addMessageCommand("start", function($bot, $message) {
150
 *         $bot->sendMessage("I am your personal bot, try /help command");
151
 *     });
152
 *
153
 *     $help_function = function($bot, $message) {
154
 *         $bot->sendMessage("This is the help message")
155
 *     };
156
 *
157
 *     $bot->addMessageCommand("/help", $help_function);
158
 *
159
 * \subsection Bot-commands-regex Check commands using regex
160
 *
161
 * You can also use **regular expressions** to check for the given command:
162
 *
163
 *     $bot->addMessageCommandRegex("number\d", $help_function);
164
 *
165
 * The closure will be called when the user send a command that match the given regex,
166
 * in this example: both <code>/number1</code> or <code>/number135</code>.
167
 *
168
 * \subsection Callback-commands Callback commands
169
 * You can also check for a callback query containing a particular string as data:
170
 *
171
 *     $bot->addCallbackCommand("back", function($bot, $callback_query) {
172
 *         $bot->editMessageText($callback_query['message']['message_id'], "You pressed back");
173
 *     });
174
 *
175
 * You should absolutely check Bot::addCallbackCommand() for learning more.
176
 *
177
 * \section InlineKeyboard-Usage Inline keyboards
178
 *
179
 * Telegram implements something called [inline keyboards](https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating) which allows users to send commands to a
180
 * bot tapping on buttons instead of typing text.
181
 *
182
 * PhpBotFrameworks supports **inline keyboard** and you can easily integrate it with your bot:
183
 *
184
 *     $bot = new PhpBotFramework\Bot("token");
185
 *
186
 *     $command_function = function($bot, $message) {
187
 *         // Add a button to the inline keyboard with written 'Click me!' and
188
 *         // that open the Telegram site if pressed.
189
 *         $bot->inline_keyboard->addLevelButtons([
190
 *             'text' => 'Click me!',
191
 *             'url' => 'telegram.me'
192
 *         ]);
193
 *
194
 *         // Then send a message, with our keyboard in the parameter $reply_markup of sendMessage
195
 *         $bot->sendMessage("This is a test message", $bot->inline_keyboard->get());
196
 *     };
197
 *
198
 *     // Add the command
199
 *     $bot->addMessageCommand("start", $command_function);
200
 *
201
 * \section Sql-Database Database
202
 * A database is required in order to save offsets (if you use local updates)
203
 * and save user's language.
204
 *
205
 * We implemented a simpler way to connect to a database which is based on PDO:
206
 *
207
 *     $bot->connect([
208
 *         'adapter' => 'pgsql',
209
 *         'username' => 'sysuser',
210
 *         'password' => 'myshinypassword',
211
 *         'dbname' => 'my_shiny_bot'
212
 *     ]);
213
 *
214
 * This method will istantiate a new PDO connection and a new PDO object you can
215
 * access through `$bot->pdo`.
216
 *
217
 * If no adapter and host are specified: `mysql` and `localhost` are assigned.
218
 *
219
 * \subsection Redis-database Redis
220
 * **Redis** is used across PhpBotFramework in order to save offsets for local updates,
221
 * to store user's language (both as cache and persistent) and save bot states.
222
 *
223
 * Redis and the main database are complementary so you need to set both.
224
 *
225
 * All you need to do, in order to enable Redis for your bot, is create a new Redis object:
226
 *
227
 *     $bot->redis = new Redis();
228
 *
229
 * \section Multilanguage-section Multi-language Bot
230
 * This framework offers methods and facilities for develop a multi-language bot.
231
 *
232
 * All you need to do is create a `localization` folder in your project's root folder
233
 * and store there the JSON files with bot's messages:
234
 *
235
 * <code>localization/en.json</code>:
236
 *
237
 *     { "Welcome_Message": "Hello, folks!" }
238
 *
239
 * <code>localization/it.json</code>:
240
 *
241
 *     { "Welcome_Message": "Ciao, gente!" }
242
 *
243
 * <code>main.php</code>:
244
 *
245
 *     // ...
246
 *     // Load JSON files
247
 *     $bot->loadLocalization();
248
 *
249
 *     $start_function = function($bot, $message) {
250
 *         // Fetch user's language from database
251
 *         $user_language = $bot->getLanguageDatabase();
252
 *         $bot->sendMessage($this->localization[$user_language]['Greetings_Msg']);
253
 *     };
254
 *
255
 *     $bot->addMessageCommand("start", $start_function);
256
 *
257
 * So you can have a wonderful (multi-language) bot with a small effort.
258
 *
259
 * \section Source
260
 * **PhpBotFramework** is an open-source project so everyone can contribute to it.
261
 *
262
 * It's currently hosted on GitHub [here](https://github.com/DanySpin97/PhpBotFramework).
263
 *
264
 * \section Createdwith-section Made with PhpBotFramework
265
 *
266
 * - [MyAddressBookBot](https://github.com/DanySpin97/MyAddressBookBot): [Try it on Telegram](https://telegram.me/myaddressbookbot)
267
 * - [Giveaways_Bot](https://github.com/DanySpin97/GiveawaysBot): [Try it on Telegram](https://telegram.me/giveaways_bot)
268
 *
269
 * \section Testing
270
 * PhpBotFramework comes with a test suite you can run using **PHPUnit**.
271
 *
272
 * You need a valid bot token and chat ID in order to run tests:
273
 *
274
 *      export BOT_TOKEN=YOURBOTTOKEN
275
 *      export CHAT_ID=YOURCHATID
276
 *
277
 * After you've set the necessary, you can run the test suite typing:
278
 *
279
 *      phpunit
280
 *
281
 * \section Authors
282
 * This framework is developed and mantained by [Danilo Spinella](https://github.com/DanySpin97).
283
 *
284
 * \section License
285
 * PhpBotFramework is released under [GNU Lesser General Public License v3](https://www.gnu.org/licenses/lgpl-3.0.en.html).
286
 *
287
 * You may copy, distribute and modify the software provided that modifications are described and licensed for free under LGPL-3.
288
 *
289
 * Derivatives works (including modifications) can only be redistributed under LGPL-3, but applications that use the wrapper don't have to be.
290
 *
291
 */
292
293
/**
294
 * \addtogroup Core Core(Internal)
295
 * \brief Core of the framework.
296
 * @{
297
 */
298
299
/**
300
 * \class CoreBot
301
 * \brief Core of the framework
302
 * \details Contains data used by the bot to works, curl request handling, and all api methods (sendMessage, editMessageText, etc).
303
 */
304
class CoreBot {
305
306
    /** @} */
307
308
    use Updates,
309
        Send,
310
        Edit,
311
        Inline,
312
        Chat;
313
314
    /**
315
     * \addtogroup Bot Bot
316
     * @{
317
     */
318
319
    /** \brief Chat_id of the user that interacted with the bot */
320
    protected $_chat_id;
321
322
    /** @} */
323
324
    /**
325
     * \addtogroup Core Core(Internal)
326
     * \brief Core of the framework.
327
     * @{
328
     */
329
    /** \brief Url request (containing $token). */
330
    protected $_api_url;
331
332
    /** \brief Implements interface for execute HTTP requests. */
333
    protected $_http;
334
335
    /**
336
     * \brief Initialize a new bot.
337
     * \details Initialize a new bot passing its token.
338
     * @param $token Bot's token given by @botfather.
339
     */
340
    public function __construct(string $token) {
341
342
        // Check token is valid
343
        if (is_numeric($token) || $token === '') {
344
345
            throw new BotException('Token is not valid or empty');
346
347
        }
348
349
        $this->_api_url = "https://api.telegram.org/bot$token/";
350
351
        // Init connection and config it
352
        $this->_http = new \GuzzleHttp\Client([
353
            'base_uri' => $this->_api_url,
354
            'connect_timeout' => 5,
355
            'verify' => false,
356
            'timeout' => 60,
357
            'http_errors' => false
358
        ]);
359
360
    }
361
362
    /** @} */
363
364
    /**
365
     * \addtogroup Bot Bot
366
     * @{
367
     */
368
369
    /**
370
     * \brief Get chat ID of the current user.
371
     * @return Chat ID of the user.
0 ignored issues
show
Comprehensibility Bug introduced by
The return type Chat is a trait, and thus cannot be used for type-hinting in PHP. Maybe consider adding an interface and use that for type-hinting?

In PHP traits cannot be used for type-hinting as they do not define a well-defined structure. This is because any class that uses a trait can rename that trait’s methods.

If you would like to return an object that has a guaranteed set of methods, you could create a companion interface that lists these methods explicitly.

Loading history...
372
     */
373
    public function getChatID() {
374
375
        return $this->_chat_id;
376
377
    }
378
379
    /**
380
     * \brief Set current chat ID.
381
     * \details Change the chat ID on which the bot acts.
382
     * @param $_chat_id The new chat ID to set.
383
     */
384
    public function setChatID($_chat_id) {
385
386
        $this->_chat_id = $_chat_id;
387
388
    }
389
390
    /**
391
     * \brief Get bot ID using `getMe` method.
392
     */
393
    public function getBotID() : int {
394
395
        // Get the id of the bot
396
        static $bot_id;
397
        $bot_id = ($this->getMe())['id'];
398
399
        // If it is not valid
400
        if (!isset($bot_id) || $bot_id == 0) {
401
402
            // get it again
403
            $bot_id = ($this->getMe())['id'];
404
405
        }
406
407
        return $bot_id ?? 0;
408
409
    }
410
411
    /** @} */
412
413
    /**
414
     * \addtogroup Api Api Methods
415
     * \brief Implementations for Telegram Bot API's methods.
416
     * @{
417
     */
418
419
    /**
420
     * \brief Exec any api request using this method.
421
     * \details Use this method for custom api calls using this syntax:
422
     *
423
     *     $param = [
424
     *             'chat_id' => $_chat_id,
425
     *             'text' => 'Hello!'
426
     *     ];
427
     *     apiRequest("sendMessage", $param);
428
     *
429
     * @param $method The method to call.
430
     * @param $parameters Parameters to add.
431
     * @return Depends on api method.
432
     */
433
    public function apiRequest(string $method, array $parameters) {
434
435
        return $this->execRequest($method . '?' . http_build_query($parameters));
436
437
    }
438
439
    /** @} */
440
441
    /**
442
     * \addtogroup Core Core(internal)
443
     * @{
444
     */
445
446
    protected function processRequest(string $method, array $param, string $class = '') {
447
448
        $response = $this->execRequest("$method?" . http_build_query($param));
449
450
        if ($response === false) {
451
452
            return false;
453
454
        }
455
456
        // if (!$this->_async)
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
457
        if ($class !== '') {
458
459
            $object_class = "PhpBotFramework\Entities\\$class";
460
461
            return new $object_class($response);
462
463
        }
464
465
        return $response;
466
    }
467
468
469
    /** \brief Core function to execute HTTP request.
470
     * @param $url The request's URL.
471
     * @param $method The request's HTTP method, POST by default.
472
     * @return Url response, false on error.
473
     */
474
    protected function execRequest(string $url, string $method = 'POST') {
475
476
        $response = $this->_http->request($method, $url);
477
        $http_code = $response->getStatusCode();
478
479
        if ($http_code === 200) {
480
            $response = json_decode($response->getBody(), true);
481
482
            if (isset($response['desc'])) {
483
                error_log("Request was successfull: {$response['description']}\n");
484
            }
485
486
            return $response['result'];
487
        } elseif ($http_code >= 500) {
488
            // do not wat to DDOS server if something goes wrong
489
            sleep(10);
490
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by PhpBotFramework\Core\CoreBot::execRequest of type PhpBotFramework\Core\Url.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
491
        } else {
492
            $response = json_decode($response->getBody(), true);
493
            error_log("Request has failed with error {$response['error_code']}: {$response['description']}\n");
494
            if ($http_code === 401) {
495
                throw new BotException('Invalid access token provided');
496
            }
497
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by PhpBotFramework\Core\CoreBot::execRequest of type PhpBotFramework\Core\Url.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
498
        }
499
500
        return $response;
0 ignored issues
show
Unused Code introduced by
return $response; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
501
    }
502
503
    /** @} */
504
505
}
506