Completed
Push — master ( ad08f4...213d65 )
by Danilo
04:24
created

CoreBot::execMultipartRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
470
471
        // If there is a file to upload
472 8
        if ($file === false) {
473 7
            $response = $this->execRequest($url);
474
        } else {
475 1
            $response = $this->execMultipartRequest($url);
476
        }
477
478 8
        if ($response === false) {
479
            return false;
480
        }
481
482 8
        if ($class !== '') {
483 8
            $object_class = "PhpBotFramework\Entities\\$class";
484
485 8
            return new $object_class($response);
486
        }
487
488
        return $response;
489
    }
490
491
    /**
492
     * \brief Check if the current file is local or not.
493
     * \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.
494
     * @return PhpBotFramework\Entities\File|false The file that will be sent using multipart, false otherwise.
495
     */
496 4
    protected function checkCurrentFile()
497
    {
498 4
        if (!$this->_file->is_local()) {
499 3
            $this->parameters[$this->_file->getFormatName()] = $this->_file->getString();
500 3
            return false;
501
        }
502
503 1
        return $this->_file;
504
    }
505
506
507
    /** \brief Core function to execute HTTP request.
508
     * @param $url The request's URL.
509
     * @return Array|false Url response decoded from JSON, false on error.
510
     */
511 9
    protected function execRequest(string $url)
512
    {
513 9
        $response = $this->_http->request('POST', $url);
514
515 9
        return $this->checkRequestError($response);
516
    }
517
518
    /** \brief Core function to execute HTTP request uploading a file.
519
     * \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.
520
     * @param $url The request's URL.
521
     * @return Array|false Url response decoded from JSON, false on error.
522
     */
523 1
    protected function execMultipartRequest(string $url)
524
    {
525 1
        $response = $this->_http->request('POST', $url, [
526
            'multipart' => [
527
                [
528 1
                    'name' => $this->_file->getFormatName(),
529 1
                    'contents' => $this->_file->getResource()
530
                ]
531
            ]]);
532
533 1
        return $this->checkRequestError($response);
534
    }
535
536 10
    public function checkRequestError($response)
537
    {
538 10
        $http_code = $response->getStatusCode();
539
540 10
        if ($http_code === 200) {
541 9
            $response = json_decode($response->getBody(), true);
542
543 9
            return $response['result'];
544 1
        } elseif ($http_code >= 500) {
545
            // do not wat to DDOS server if something goes wrong
546
            sleep(10);
547
            return false;
548
        } else {
549 1
            $response = json_decode($response->getBody(), true);
550 1
            error_log("Request has failed with error {$response['error_code']}: {$response['description']}\n");
551 1
            return false;
552
        }
553
    }
554
555
    /** @} */
556
557
    /** @} */
558
}
559