CoreBot::execRequest()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
crap 1
1
<?php
2
3
/*
4
 * This file is part of the PhpBotFramework.
5
 *
6
 * PhpBotFramework is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as
8
 * published by the Free Software Foundation, version 3.
9
 *
10
 * PhpBotFramework is distributed in the hope that it will be useful, but
11
 * WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
 * Lesser General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU Lesser General Public License
16
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17
 */
18
19
namespace PhpBotFramework\Core;
20
21
use PhpBotFramework\Exceptions\BotException;
22
23
use PhpBotFramework\Entities\File as TelegramFile;
24
25
use PhpBotFramework\Entities\InlineKeyboard;
26
27
/**
28
 * \mainpage
29
 * \section Description
30
 * PhpBotFramework is a lightweight framework for [Telegram Bot API](https://core.telegram.org/bots/api).
31
 * Designed to be fast and easy to use, it provides all the features a user need in order to start
32
 * developing Telegram bots..
33
 *
34
 * \section Installation
35
 * You can install PhpBotFramework using **Composer**.
36
 *
37
 * Go to your project's folder and type:
38
 *
39
 *     composer require danyspin97/php-bot-framework
40
 *     composer install --no-dev
41
 *
42
 * \section Usage
43
 * You can start working on your bot creating a new instance of Bot or by creating a
44
 * class that inherits from it.
45
 *
46
 * Each API call will have <code>$_chat_id</code> set to the current user:
47
 * you can use CoreBot::setChatID() to change it.
48
 *
49
 * Below an example bot you can look to:
50
 *
51
 *
52
 *     <?php
53
 *
54
 *     // Include the framework
55
 *     require './vendor/autoload.php';
56
 *
57
 *     // Create the bot
58
 *     $bot = new PhpBotFramework\Bot("token");
59
 *
60
 *     // Create a command that will be triggered every time the user send /start
61
 *     $start_command = new PhpBotFramework\Commands\MessageCommand("start", function($bot, $message) {
62
 *         $bot->sendMessage("Hello, folks!");
63
 *     });
64
 *
65
 *     $bot->addCommand($start_command);
66
 *
67
 *     // Shortcut for add message commands.
68
 *     $bot->addMessageCommand('about', function($bot, $message) {
69
 *         $bot->sendMessage('Powered by PhpBotFramework');
70
 *     });
71
 *
72
 *     // Receive updates from Telegram using getUpdates
73
 *     $bot->getUpdatesLocal();
74
 *
75
 * \subsection Bot-Intherited Inheriting by Bot class
76
 *
77
 *     <?php
78
 *
79
 *     // Include the framework
80
 *     require './vendor/autoload.php';
81
 *
82
 *     // Create the class that will extends Bot class
83
 *     class EchoBot extends PhpBotFramework\Bot {
84
 *
85
 *         // Add the function for processing messages
86
 *         protected function processMessage($message) {
87
 *
88
 *             // Answer each message with the text received
89
 *             $this->sendMessage($message['text']);
90
 *         }
91
 *     }
92
 *
93
 *     $bot = new EchoBot("token");
94
 *
95
 *     // Process updates using webhook
96
 *     $bot->processWebhookUpdate();
97
 *
98
 * Override these method to make your bot handle each update type:
99
 * - Bot::processMessage($message)
100
 * - Bot::processCallbackQuery($callback_query)
101
 * - Bot::processInlineQuery($inline_query)
102
 * - Bot::processChosenInlineResult($_chosen_inline_result)
103
 * - Bot::processEditedMessage($edited_message)
104
 * - Bot::processChannelPost($post)
105
 * - Bot::processEditedChannelPost($edited_post)
106
 *
107
 * \section Features
108
 * - Modular: take only what you need
109
 * - Flexible HTTP requests with [Guzzle](https://github.com/guzzle/guzzle)
110
 * - Designed to be fast and easy to use
111
 * - Support for local updates and webhooks
112
 * - Support for the most important API methods
113
 * - Command-handle system for messages and callback queries
114
 * - Update type based processing
115
 * - Easy **inline keyboard** creation
116
 * - Inline query results handler
117
 * - Database support and facilities
118
 * - Redis support
119
 * - Support for multilanguage bots
120
 * - Support for bot states
121
 * - Highly-documented
122
 *
123
 * \section Requirements
124
 * - PHP >= 7.0
125
 * - php-mbstring
126
 * - Composer (to install the framework)
127
 * - Web server: *required for webhook* (we recommend [nginx](http://nginx.org/))
128
 * - 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/))
129
 *
130
 * \section GetUpdates-section Getting updates
131
 * Everytime a user interacts with the bot, an `update` is generated by Telegram's servers.
132
 *
133
 * There are two ways of receiving this updates:
134
 * - use [Telegram Bot API's `getUpdates`](https://core.telegram.org/bots/api#getupdates) method
135
 * - use webhooks (it's covered in the next section)
136
 *
137
 * If you want to use `getUpdates` in order to receive updates,
138
 * add one of these functions at the end of your bot:
139
 * - Bot::getUpdatesLocal()
140
 * - Bot::getUpdatesDatabase()
141
 * - Bot::getUpdatesRedis()
142
 *
143
 * The bot will process updates one a time and will call Bot::processUpdate() for each.
144
 *
145
 * The connection will be opened at the creation and used for the entire life of the bot.
146
 *
147
 * \section Webhook-section Webhook
148
 * An alternative way to receive updates is using **webhooks**.
149
 *
150
 * Everytime a user interacts with the bot, Telegram servers send the update through
151
 * a POST request to a URL chose by you.
152
 *
153
 * A web server will create an instance of the bot for every update received.
154
 *
155
 * If you want to use webhook: call Bot::processWebhookUpdate() at the end of your bot.
156
 *
157
 * The bot will get data from <code>php://input</code> and process it using Bot::processUpdate().
158
 * Each instance of the bot will open its connection.
159
 *
160
 * \subsection Setwebhooks-subsection Set webhook
161
 * You can set a URL for your bot's webhook using CoreBot::setWebhook():
162
 *
163
 *     //...
164
 *     $bot->setWebhook([ 'url' => 'https://example.com/mybotSECRETPATH' ])
165
 *
166
 * You can learn more about `setWebhook` and webhooks [here](https://core.telegram.org/bots/api#setwebhook).
167
 *
168
 * \section Message-commands Bot's commands
169
 * One of the most important tasks during a Telegram bot's development is register
170
 * the commands the bot will respond to.
171
 *
172
 * PhpBotFrameworks makes it easy:
173
 *
174
 *     $start_message_command = new PhpBotFramework\Commands\MessageCommand("start", function($bot, $message) {
175
 *         $bot->sendMessage("I am your personal bot, try /help command");
176
 *     });
177
 *
178
 *     $bot->addCommand($start_message_command);
179
 *
180
 *     $help_closure = function($bot, $message) {
181
 *         $bot->sendMessage("This is the help message")
182
 *     };
183
 *
184
 *     $help_message_command = new PhpBotFramework\Commands\MessageCommand("/help", $help_function);
185
 *
186
 *     $bot->addCommand($help_message_command);
187
 *
188
 * The message commands are so used that we eventually decided to add a dedicated method:
189
 *
190
 *     $bot->addMessageCommand('start', function($bot, $message) {
191
 *         $bot->sendMessage('I am your personal bot, try /help command');
192
 *     });
193
 *
194
 * \subsection Bot-commands-regex Check commands using regex
195
 *
196
 * You can also use **regular expressions** to check for the given command:
197
 *
198
 *     $regex_command = new PhpBotFramework\Commands\MessageRegexCommand("number\d", $help_function);
199
 *
200
 * The closure will be called when the user send a command that match the given regex,
201
 * in this example: both <code>/number1</code> or <code>/number135</code>.
202
 *
203
 * \subsection Callback-commands Callback commands
204
 * You can also check for a callback query containing a particular string as data:
205
 *
206
 *     $callback_command = new PhpBotFramework\Commands\CallbackCommand("back", function($bot, $callback_query) {
207
 *         $bot->editMessageText($callback_query['message']['message_id'], "You pressed back");
208
 *     });
209
 *
210
 * You should absolutely check Bot::addCommand() for learning more.
211
 *
212
 * \subsection Add multiple commands at once.
213
 * If you've to add a lot of commands you can add all of them at once:
214
 *
215
 *     $callback_command = new PhpBotFramework\Commands\CallbackCommand("back", function($bot, $callback_query) {
216
 *         $bot->editMessageText($callback_query['message']['message_id'], "You pressed back");
217
 *     });
218
 *
219
 *     $callback_command_2 = new PhpBotFramework\Commands\CallbackCommand("delete", function($bot, $callback_query) {
220
 *         $bot->editMessageText($callback_query['message']['message_id'], "You pressed delete");
221
 *     });
222
 *
223
 *     $bot->addCommands($callback_command, $callback_command_2);
224
 *
225
 * \section InlineKeyboard-Usage Inline keyboards
226
 *
227
 * 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
228
 * bot tapping on buttons instead of typing text.
229
 *
230
 * PhpBotFrameworks supports **inline keyboard** and you can easily integrate it with your bot:
231
 *
232
 *     $bot = new PhpBotFramework\Bot("token");
233
 *
234
 *     $command = ("start", function($bot, $message) {
235
 *         // Add a button to the inline keyboard with written 'Click me!' and
236
 *         // that open the Telegram site if pressed.
237
 *         $bot->inline_keyboard->addLevelButtons([
238
 *             'text' => 'Click me!',
239
 *             'url' => 'telegram.me'
240
 *         ]);
241
 *
242
 *         // Then send a message, with our keyboard in the parameter $reply_markup of sendMessage
243
 *         $bot->sendMessage("This is a test message", $bot->inline_keyboard->get());
244
 *     });
245
 *
246
 *     // Add the command
247
 *     $bot->addCommand($command);
248
 *
249
 * \section Sql-Database Database
250
 * A database is required in order to save offsets (if you use local updates)
251
 * and save user's language.
252
 *
253
 * We implemented a simpler way to connect to a database which is based on PDO:
254
 *
255
 *     $bot->database->connect([
256
 *         'adapter' => 'pgsql',
257
 *         'username' => 'sysuser',
258
 *         'password' => 'myshinypassword',
259
 *         'dbname' => 'my_shiny_bot'
260
 *     ]);
261
 *
262
 * This method will istantiate a new PDO connection and a new PDO object you can
263
 * access through `$bot->getPdo()`.
264
 *
265
 * If no adapter and host are specified: `mysql` and `localhost` are assigned.
266
 *
267
 * \subsection Redis-database Redis
268
 * **Redis** is used across PhpBotFramework in order to save offsets for local updates,
269
 * to store user's language (both as cache and persistent) and save bot states.
270
 *
271
 * Redis and the main database are complementary so you need to set both.
272
 *
273
 * All you need to do, in order to enable Redis for your bot, is create a new Redis object:
274
 *
275
 *     $bot->redis = new Redis();
276
 *
277
 * \section Multilanguage-section Multi-language Bot
278
 * This framework offers methods and facilities for develop a multi-language bot.
279
 *
280
 * All you need to do is create a `localization` folder in your project's root folder
281
 * and store there the JSON files with bot's messages:
282
 *
283
 * <code>localization/en.json</code>:
284
 *
285
 *     { "Welcome_Message": "Hello, folks!" }
286
 *
287
 * <code>localization/it.json</code>:
288
 *
289
 *     { "Welcome_Message": "Ciao, gente!" }
290
 *
291
 * <code>main.php</code>:
292
 *
293
 *     // ...
294
 *
295
 *     $start_command = PhpBotFramework\Commands\MessageCommand("start", function($bot, $message) {
296
 *         $bot->sendMessage($bot->local->getStr('Greetings_Msg'));
297
 *     });
298
 *
299
 *     $bot->addCommand($start_command);
300
 *
301
 * So you can have a wonderful (multi-language) bot with a small effort.
302
 *
303
 * \section Source
304
 * **PhpBotFramework** is an open-source project so everyone can contribute to it.
305
 *
306
 * It's currently hosted on GitHub [here](https://github.com/DanySpin97/PhpBotFramework).
307
 *
308
 * \section Createdwith-section Made with PhpBotFramework
309
 *
310
 * - [MyAddressBookBot](https://github.com/DanySpin97/MyAddressBookBot): [Try it on Telegram](https://telegram.me/myaddressbookbot)
311
 * - [Giveaways_Bot](https://github.com/DanySpin97/GiveawaysBot): [Try it on Telegram](https://telegram.me/giveaways_bot)
312
 *
313
 * \section Testing
314
 * PhpBotFramework comes with a test suite you can run using **PHPUnit**.
315
 *
316
 * You need a valid bot token and chat ID in order to run tests:
317
 *
318
 *      export BOT_TOKEN=YOURBOTTOKEN
319
 *      export CHAT_ID=YOURCHATID
320
 *
321
 * After you've set the necessary, you can run the test suite typing:
322
 *
323
 *      phpunit
324
 *
325
 * \section Authors
326
 * This framework is developed and mantained by [Danilo Spinella](https://github.com/DanySpin97) and [Dom Corvasce](https://github.com/domcorvasce).
327
 *
328
 * \section License
329
 * PhpBotFramework is released under [GNU Lesser General Public License v3](https://www.gnu.org/licenses/lgpl-3.0.en.html).
330
 *
331
 * You may copy, distribute and modify the software provided that modifications are described and licensed for free under LGPL-3.
332
 *
333
 * Derivatives works (including modifications) can only be redistributed under LGPL-3, but applications that use the wrapper don't have to be.
334
 *
335
 */
336
337
/**
338
 * \class CoreBot
339
 * \brief Core of the framework
340
 * \details Contains data used by the bot to works, curl request handling, and all api methods (sendMessage, editMessageText, etc).
341
 */
342
class CoreBot
343
{
344
    use Updates,
345
        Send,
346
        Edit,
347
        Inline,
348
        Chat;
349
350
    /** @internal
351
      * \brief Chat_id of the user that interacted with the bot. */
352
    protected $_chat_id;
353
354
    /** @internal
355
      * \brief Bot id. */
356
    protected $_bot_id;
357
358
    /** @internal
359
      * \brief API endpoint (containing $token). */
360
    protected $_api_url;
361
362
    /** @internal
363
      * \brief Implements interface for execute HTTP requests. */
364
    public $_http;
365
366
    /** @internal
367
      * \brief Object of class PhpBotFramework\Entities\File that contain a path or resource to a file that has to be sent using Telegram API Methods. */
368
    protected $_file;
369
370
    /** \@internal
371
      * brief Contains parameters of the next request. */
372
    protected $parameters;
373
374
    /**
375
     * \@internal
376
     * brief Initialize a new bot.
377
     * \details Initialize a new bot passing its token.
378
     * @param $token Bot's token given by @botfather.
379
     */
380 2
    public function __construct(string $token)
381
    {
382
        // Check if token is valid
383 2
        if (is_numeric($token) || $token === '') {
384
            throw new BotException('Token is not valid or empty');
385
        }
386
387 2
        $this->_api_url = "https://api.telegram.org/bot$token/";
388
389
        // Init connection and config it
390 2
        $this->_http = new \GuzzleHttp\Client([
391 2
            'base_uri' => $this->_api_url,
392 2
            'connect_timeout' => 5,
393
            'verify' => false,
394 2
            'timeout' => 60,
395
            'http_errors' => false
396
        ]);
397
398 2
        $this->_file = new TelegramFile();
399 2
    }
400
401
    /**
402
     * \addtogroup Bot Bot
403
     * @{
404
     */
405
406
    /**
407
     * \brief Get chat ID of the current user.
408
     * @return int Chat ID of the user.
409
     */
410 1
    public function getChatID()
411
    {
412 1
        return $this->_chat_id;
413
    }
414
415
    /**
416
     * \brief Set current chat ID.
417
     * \details Change the chat ID on which the bot acts.
418
     * @param $chat_id The new chat ID to set.
419
     */
420 6
    public function setChatID($chat_id)
421
    {
422 6
        $this->_chat_id = $chat_id;
423 6
    }
424
425
    /**
426
     * \brief Get bot ID using `getMe` method.
427
     * @return int Bot id, 0 on errors.
428
     */
429
    public function getBotID() : int
430
    {
431
        // If it is not valid
432
        if (!isset($this->_bot_id) || $this->_bot_id == 0) {
433
            // get it again
434
            $this->_bot_id = ($this->getMe())['id'];
435
        }
436
437
        return $this->_bot_id ?? 0;
438
    }
439
440
    /** @} */
441
442
    /**
443
     * \addtogroup Api Api Methods
444
     * \brief Implementations for Telegram Bot API's methods.
445
     * @{
446
     */
447
448
    /**
449
     * \brief Execute any API request using this method.
450
     * \details Use this method for custom api calls using this syntax:
451
     *
452
     *     $param = [
453
     *             'chat_id' => $_chat_id,
454
     *             'text' => 'Hello!'
455
     *     ];
456
     *     apiRequest("sendMessage", $param);
457
     *
458
     * @param string $method The method to call.
459
     * @param array $parameters Parameters to add.
460
     * @return mixed Depends on api method.
461
     */
462
    public function apiRequest(string $method, array $parameters)
463
    {
464
        return $this->execRequest($method . '?' . http_build_query($parameters));
465
    }
466
467
    /** @} */
468
469
    /**
470
     * \@internal
471
     * brief Process an API method by taking method and parameter.
472
     * \details optionally create a object of $class class name with the response as constructor param.
473
     * @param string $method Method to call.
474
     * @param array $param Parameter for the method.
0 ignored issues
show
Bug introduced by
There is no parameter named $param. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
475
     * @param string $class Class name of the object to create using response.
476
     * @return mixed Response or object of $class class name.
477
     */
478 16
    protected function processRequest(string $method, string $class = '', $file = false)
479
    {
480 16
        $url = "$method?" . http_build_query($this->parameters);
481
482
        // If there is a file to upload
483 16
        if ($file === false) {
484 15
            $response = $this->execRequest($url);
485
        } else {
486 3
            $response = $this->execMultipartRequest($url);
487
        }
488
489 16
        if ($response === false) {
490 5
            return false;
491
        }
492
493 11
        if ($class !== '') {
494 11
            $object_class = "PhpBotFramework\Entities\\$class";
495
496 11
            return new $object_class($response);
497
        }
498
499
        return $response;
500
    }
501
502
    /**
503
     * @internal
504
     * \brief Check if the current file is local or not.
505
     * \details If the file file is local, then it has to be uploaded using multipart. If not, then it is a url/file_id so it has to be added in request parameters as a string.
506
     * @return PhpBotFramework\Entities\File|false The file that will be sent using multipart, false otherwise.
507
     */
508 7
    protected function checkCurrentFile()
509
    {
510 7
        if (!$this->_file->isLocal()) {
511 6
            $this->parameters[$this->_file->getFormatName()] = $this->_file->getString();
512 6
            return false;
513
        }
514
515 3
        return $this->_file;
516
    }
517
518
519
    /**
520
     * @internal
521
     * \brief Core function to execute HTTP request.
522
     * @param $url The request's URL.
523
     * @return Array|false Url response decoded from JSON, false on error.
524
     */
525 17
    protected function execRequest(string $url)
526
    {
527 17
        $response = $this->_http->request('POST', $url);
528
529 17
        return $this->checkRequestError($response);
530
    }
531
532
    /**
533
     * @internal
534
     * \brief Core function to execute HTTP request uploading a file.
535
     * \details Using an object of type PhpBotFramework\Entities\File contained in $_file and Guzzle multipart request option, it uploads the file along with api method requested.
536
     * @param $url The request's URL.
537
     * @return Array|false Url response decoded from JSON, false on error.
538
     */
539 3
    protected function execMultipartRequest(string $url)
540
    {
541 3
        $response = $this->_http->request('POST', $url, [
542
            'multipart' => [
543
                [
544 3
                    'name' => $this->_file->getFormatName(),
545 3
                    'contents' => $this->_file->getResource()
546
                ]
547
            ]]);
548
549 3
        return $this->checkRequestError($response);
550
    }
551
552 18
    public function checkRequestError($response)
553
    {
554 18
        $http_code = $response->getStatusCode();
555
556 18
        if ($http_code === 200) {
557 13
            $response = json_decode($response->getBody(), true);
558
559 13
            return $response['result'];
560 5
        } elseif ($http_code >= 500) {
561
            // do not wat to DDOS server if something goes wrong
562
            sleep(10);
563
            return false;
564
        } else {
565 5
            $response = json_decode($response->getBody(), true);
566 5
            error_log("Request has failed with error {$response['error_code']}: {$response['description']}\n");
567 5
            return false;
568
        }
569
    }
570
571
    /** @} */
572
573
    /** @} */
574
}
575