Completed
Push — master ( c9c19d...a94344 )
by Danilo
06:15
created

CoreBot::processRequest()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5.5021

Importance

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