Completed
Push — master ( 549b3b...080752 )
by Danilo
15:29
created

CoreBot::execRequest()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4.125

Importance

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