Completed
Push — master ( 23095d...54d687 )
by Danilo
02:12
created

CoreBot   A

Complexity

Total Complexity 14

Size/Duplication

Total Lines 183
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 0
Metric Value
dl 0
loc 183
rs 10
c 0
b 0
f 0
wmc 14
lcom 1
cbo 8

6 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 24 3
A getChatID() 0 5 1
A setChatID() 0 5 1
A getBotID() 0 17 3
A apiRequest() 0 5 1
B exec_curl_request() 0 28 5
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 a lightweight framework for Telegram Bot API.
13
 * Designed to be fast and easy to use, it provides all the features a user need.
14
 * Take control of your bot using the command-handler system or the update type based function.
15
 *
16
 * \subsection Example
17
 * A quick example, the bot will send "Hello" every time the user click "/start":
18
 *
19
 *     <?php
20
 *
21
 *     // Include the framework
22
 *     require './vendor/autoload.php';
23
 *
24
 *     // Create the bot
25
 *     $bot = new DanySpin97\PhpBotFramework\Bot("token");
26
 *
27
 *     // Add a command that will be triggered every time the user click /start
28
 *     $bot->addMessageCommand("start",
29
 *         function($bot, $message) {
30
 *             $bot->sendMessage("Hello");
31
 *         }
32
 *     );
33
 *
34
 *     // Receive update from telegram using getUpdates
35
 *     $bot->getUpdatesLocal();
36
 *
37
 * \section Features
38
 * - Designed to be the fast and easy to use
39
 * - Support for getUpdates and webhooks
40
 * - Support for the most important API methods
41
 * - Command-handle system for messages and callback queries
42
 * - Update type based processing
43
 * - Easy inline keyboard creation
44
 * - Inline query results handler
45
 * - Sql database support
46
 * - Redis support
47
 * - Support for multilanguage bot
48
 * - Support for bot state
49
 * - Highly documented
50
 *
51
 * \section Requirements
52
 * - Php 7.0 or greater
53
 * - php-mbstring
54
 * - Composer (to install the framework)
55
 * - SSL certificate (<i>required by webhook</i>)
56
 * - Web server (<i>required by webhook</i>)
57
 *
58
 * \section Installation
59
 * In your project folder:
60
 *
61
 *     composer require danyspin97/php-bot-framework
62
 *     composer install --no-dev
63
 *
64
 * \subsection Web-server
65
 * To use webhook for the bot, a web server and a SSL certificate are required.
66
 * Install one using your package manager (nginx or caddy reccomended).
67
 * To get a SSL certificate you can user [Let's Encrypt](https://letsencrypt.org/).
68
 *
69
 * \section Usage
70
 * Add the scripting by adding command (Bot::addMessageCommand()) or by creating a class that inherits Bot.
71
 * Each api call will have <code>$_chat_id</code> set to the current user, use CoreBot::setChatID() to change it.
72
 *
73
 * \subsection getUpdates
74
 * The bot ask for updates to telegram server.
75
 * If you want to use getUpdates method to receive updates from telegram, add one of these function at the end of your bot:
76
 * - Bot::getUpdatesLocal()
77
 * - Bot::getUpdatesDatabase()
78
 * - Bot::getUpdatesRedis()
79
 *
80
 * The bot will process updates in a row, and will call Bot::processUpdate() for each.
81
 * getUpdates handling is single-threaded so there will be only one object that will process updates. The connection will be opened at the creation and used for the entire life of the bot.
82
 *
83
 * \subsection Webhook
84
 * A web server will create an instance of the bot for every update received.
85
 * If you want to use webhook call Bot::processWebhookUpdate() at the end of your bot. The bot will get data from <code>php://input</code> and process it using Bot::processUpdate().
86
 * Each instance of the bot will open its connection.
87
 *
88
 * \subsection Message-commands Message commands
89
 * Script how the bot will answer to messages containing commands (like <code>/start</code>).
90
 *
91
 *     $bot->addMessageCommand("start", function($bot, $message) {
92
 *             $bot->sendMessage("I am your personal bot, try /help command");
93
 *     });
94
 *
95
 *     $help_function = function($bot, $message) {
96
 *         $bot->sendMessage("This is the help message")
97
 *     };
98
 *
99
 *     $bot->addMessageCommand("/help", $help_function);
100
 *
101
 * Check Bot::addMessageCommand() for more.
102
 *
103
 * You can also use regex to check commands.
104
 *
105
 * The closure will be called if the commands if the expression evaluates to true. Here is an example:
106
 *
107
 *     $bot->addMessageCommandRegex("number\d",
108
 *         $help_function);
109
 *
110
 * The closure will be called when the user send a command that match the regex like, in this example, both <code>/number1</code> or <code>/number135</code>.
111
 *
112
 * \subsection Callback-commands Callback commands
113
 * Script how the bot will answer to callback query containing a particular string as data.
114
 *
115
 *     $bot->addCallbackCommand("back", function($bot, $callback_query) {
116
 *             $bot->editMessageText($callback_query['message']['message_id'], "You pressed back");
117
 *     });
118
 *
119
 * Check Bot::addCallbackCommand() for more.
120
 *
121
 * \subsection Bot-Intherited Inherit Bot Class
122
 * Create a new class that inherits Bot to handle all updates.
123
 *
124
 * <code>EchoBot.php</code>
125
 *
126
 *     // Create the class that will extends Bot class
127
 *     class EchoBot extends DanySpin97\PhpBotFramework\Bot {
128
 *
129
 *         // Add the function for processing messages
130
 *         protected function processMessage($message) {
131
 *
132
 *             // Answer each message with the text received
133
 *             $this->sendMessage($message['text']);
134
 *
135
 *         }
136
 *
137
 *     }
138
 *
139
 *     // Create an object of type EchoBot
140
 *     $bot = new EchoBot("token");
141
 *
142
 *     // Process updates using webhook
143
 *     $bot->processWebhookUpdate();
144
 *
145
 * Override these method to make your bot handle each update type:
146
 * - Bot::processMessage($message)
147
 * - Bot::processCallbackQuery($callback_query)
148
 * - Bot::processInlineQuery($inline_query)
149
 * - Bot::processChosenInlineResult($_chosen_inline_result)
150
 * - Bot::processEditedMessage($edited_message)
151
 * - Bot::processChannelPost($post)
152
 * - Bot::processEditedChannelPost($edited_post)
153
 *
154
 * \subsection InlineKeyboard-Usage InlineKeyboard Usage
155
 *
156
 * How to use the InlineKeyboard class:
157
 *
158
 *     // Create the bot
159
 *     $bot = new DanySpin97\PhpBotFramework\Bot("token");
160
 *
161
 *     $command_function = function($bot, $message) {
162
 *             // Add a button to the inline keyboard
163
 *             $bot->inline_keyboard->addLevelButtons([
164
 *                  // with written "Click me!"
165
 *                  'text' => 'Click me!',
166
 *                  // and that open the telegram site, if pressed
167
 *                  'url' => 'telegram.me'
168
 *                  ]);
169
 *             // Then send a message, with our keyboard in the parameter $reply_markup of sendMessage
170
 *             $bot->sendMessage("This is a test message", $bot->inline_keyboard->get());
171
 *             }
172
 *
173
 *     // Add the command
174
 *     $bot->addMessageCommand("start", $command_function);
175
 *
176
 * \subsection Sql-Database Sql Database
177
 * The sql database is used to save offset from getUpdates and to save user language.
178
 *
179
 * To connect a sql database to the bot, a pdo connection is required.
180
 *
181
 * Here is a simple pdo connection that is passed to the bot:
182
 *
183
 *     $bot->pdo = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
184
 *
185
 * \subsection Redis-database Redis Database
186
 * Redis is used to save offset from getUpdates, to store language (both as cache and persistent) and to save bot state.
187
 *
188
 * To connect redis with the bot, create a redis object.
189
 *
190
 *     $bot->redis = new Redis();
191
 *
192
 * \subsection Multilanguage-section Multilanguage Bot
193
 * This framework offers method to develop a multi language bot.
194
 *
195
 * Here's an example:
196
 *
197
 * <code>en.json</code>:
198
 *
199
 *     {"Greetings_Msg": "Hello"}
200
 *
201
 * <code>it.json</code>:
202
 *
203
 *     {"Greetings_Msg": "Ciao"}
204
 *
205
 * <code>Greetings.php</code>:
206
 *
207
 *     $bot->loadLocalization();
208
 *     $start_function = function($bot, $message) {
209
 *             $bot->sendMessage($this->localization[
210
 *                     $bot->getLanguageDatabase()]['Greetings_Msg'])
211
 *     };
212
 *
213
 *     $bot->addMessageCommand("start", $start_function);
214
 *
215
 * The bot will get the language from the database, then the bot will send the message localizated for the user.
216
 *
217
 * \ref Multilanguage [See here for more]
218
 *
219
 * \section Source
220
 * The source is hosted on github and can be found [here](https://github.com/DanySpin97/PhpBotFramework).
221
 *
222
 * \section Bot-created Bot using this framework
223
 * - [\@MyAddressBookBot](https://telegram.me/myaddressbookbot) ([Source](https://github.com/DanySpin97/MyAddressBookBot))
224
 * - [\@Giveaways_bot](https://telegram.me/giveaways_bot) ([Source](https://github.com/DanySpin97/GiveawaysBot))
225
 *
226
 * \section Authors
227
 * This framework is developed and manteined by Danilo Spinella.
228
 *
229
 * \section License
230
 * PhpBotFramework is released under GNU Lesser General Public License.
231
 * You may copy, distribute and modify the software provided that modifications are described and licensed for free under LGPL-3. Derivatives works (including modifications) can only be redistributed under LGPL-3, but applications that use the wrapper don't have to be.
232
 *
233
 */
234
235
/**
236
 * \class CoreBot
237
 * \brief Core of the framework
238
 * \details Contains data used by the bot to works, curl request handling, and all api methods (sendMessage, editMessageText, etc).
239
 */
240
class CoreBot {
241
242
    use Updates,
243
        Send,
244
        Edit,
245
        Inline,
246
        Chat;
247
248
    /**
249
     * \addtogroup Bot Bot
250
     * @{
251
     */
252
253
    /** \brief Chat_id of the user that interacted with the bot */
254
    protected $_chat_id;
255
256
    /** @} */
257
258
    /**
259
     * \addtogroup Core Core(Internal)
260
     * \brief Core of the framework.
261
     * @{
262
     */
263
264
    /** \brief The bot token (given by @BotFather). */
265
    private $token;
266
267
    /** \brief Url request (containing $token). */
268
    protected $_api_url;
269
270
    /** \brief Implements interface for execute HTTP requests. */
271
    protected $_http;
272
273
    /**
274
     * \brief Contrusct an empty bot.
275
     * \details Construct a bot passing the token.
276
     * @param $token Token given by @botfather.
277
     */
278
    public function __construct(string $token) {
279
280
        // Check token is valid
281
        if (is_numeric($token) || $token === '') {
282
283
            throw new BotException('Token is not valid or empty');
284
285
        }
286
287
        // Init variables
288
        $this->token = $token;
289
        $this->_api_url = 'https://api.telegram.org/bot' . $token . '/';
290
291
        // Init connection and config it
292
        $this->_http = new \GuzzleHttp\Client([
293
            'base_uri' => $this->_api_url,
294
            'connect_timeout' => 5,
295
            'verify' => false,
296
            'timeout' => 60,
297
            'http_errors' => false
298
        ]);
299
300
        return;
301
    }
302
303
    /** @} */
304
305
    /**
306
     * \addtogroup Bot Bot
307
     * @{
308
     */
309
310
    /**
311
     * \brief Get chat id of the current user.
312
     * @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...
313
     */
314
    public function getChatID() {
315
316
        return $this->_chat_id;
317
318
    }
319
320
    /**
321
     * \brief Set current chat id.
322
     * \details Change the chat id which the bot execute api methods.
323
     * @param $_chat_id The new chat id to set.
324
     */
325
    public function setChatID($_chat_id) {
326
327
        $this->_chat_id = $_chat_id;
328
329
    }
330
331
    /**
332
     * \brief Get bot ID using getMe API method.
333
     */
334
    public function getBotID() : int {
335
336
        // Get the id of the bot
337
        static $bot_id;
338
        $bot_id = ($this->getMe())['id'];
339
340
        // If it is not valid
341
        if (!isset($bot_id) || $bot_id == 0) {
342
343
            // get it again
344
            $bot_id = ($this->getMe())['id'];
345
346
        }
347
348
        return $bot_id ?? 0;
349
350
    }
351
352
    /** @} */
353
354
    /**
355
     * \addtogroup Api Api Methods
356
     * \brief All api methods to interface the bot with Telegram.
357
     * @{
358
     */
359
360
    /**
361
     * \brief Exec any api request using this method.
362
     * \details Use this method for custom api calls using this syntax:
363
     *
364
     *     $param = [
365
     *             'chat_id' => $_chat_id,
366
     *             'text' => 'Hello!'
367
     *     ];
368
     *     apiRequest("sendMessage", $param);
369
     *
370
     * @param $method The method to call.
371
     * @param $parameters Parameters to add.
372
     * @return Depends on api method.
373
     */
374
    public function apiRequest(string $method, array $parameters) {
375
376
        return $this->exec_curl_request($method . '?' . http_build_query($parameters));
377
378
    }
379
380
    /** @} */
381
382
    /**
383
     * \addtogroup Core Core(internal)
384
     * @{
385
     */
386
387
    /** \brief Core function to execute url request.
388
     * @param $url The url to call using the curl session.
389
     * @return Url response, false on error.
390
     */
391
    protected function exec_curl_request($url, $method = 'POST') {
392
393
        $response = $this->_http->request($method, $url);
394
        $http_code = $response->getStatusCode();
395
396
        if ($http_code === 200) {
397
            $response = json_decode($response->getBody(), true);
398
399
            if (isset($response['desc'])) {
400
                error_log("Request was successfull: {$response['description']}\n");
401
            }
402
403
            return $response['result'];
404
        } elseif ($http_code >= 500) {
405
            // do not wat to DDOS server if something goes wrong
406
            sleep(10);
407
            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::exec_curl_request 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...
408
        } else {
409
            $response = json_decode($response->getBody(), true);
410
            error_log("Request has failed with error {$response['error_code']}: {$response['description']}\n");
411
            if ($http_code === 401) {
412
                throw new BotException('Invalid access token provided');
413
            }
414
            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::exec_curl_request 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...
415
        }
416
417
        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...
418
    }
419
420
    /** @} */
421
422
}
423
424