Completed
Push — master ( 4c8026...0a2b1f )
by Danilo
02:04
created

Bot::getUpdatesDatabase()   C

Complexity

Conditions 8
Paths 17

Size

Total Lines 67
Code Lines 27

Duplication

Lines 13
Ratio 19.4 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 13
loc 67
rs 6.6523
cc 8
eloc 27
nc 17
nop 4

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
// Exception declration
4
namespace DanySpin97\PhpBotFramework;
5
6
/**
7
 * \class Bot Bot
8
 * \brief Bot class to handle updates and commandes.
9
 * \details Class Bot to handle task like api request, or more specific api function(sendMessage, editMessageText, etc).
10
 * Usage example in webhook.php
11
 *
12
 */
13
class Bot extends CoreBot {
14
15
    /**
16
     * \addtogroup Bot Bot
17
     * \brief Properties and methods to handle the TelegramBot.
18
     * \details Here are listed all the properties and methods that will help the developer create the basic bot functions.
19
     * @{
20
     */
21
22
    /** \brief Text received in messages */
23
    protected $_text;
24
25
    /** \brief Data received in callback query */
26
    protected $_data;
27
28
    /** \brief Query sent by the user in the inline query */
29
    protected $_query;
30
31
    /** \brief Store the inline keyboard */
32
    public $keyboard;
33
34
    /** \brief Pdo reference */
35
    public $pdo;
36
37
    /** \brief Redis connection */
38
    public $redis;
39
40
    /** @} */
41
42
    /**
43
     * \addtogroup Core Core(internal)
44
     * @{
45
     */
46
47
    /** \brief Store the command triggered on message. */
48
    private $message_commands;
49
50
    /** \brief Does the bot has message commands? Set by initBot. */
51
    private $message_commands_set;
52
53
    /** \brief Store the command triggered on callback query. */
54
    private $callback_commands;
55
56
    /** \brief Does the bot has message commands? Set by initBot. */
57
    private $callback_commands_set;
58
59
    /** @} */
60
61
    /**
62
     * \addtogroup Multilanguage Multilanguage
63
     * \brief Methods to create a localized bot.
64
     * @{
65
     */
66
67
    /** \brief Store the language for a multi-language bot */
68
    public $language;
69
70
    /** \brief Store localization data */
71
    public $local;
72
73
    /** \brief Table contaning bot users data in the sql database. */
74
    public $user_table = '"User"';
75
76
    /** \brief Name of the column that represents the user id in the sql database */
77
    public $id_column = 'chat_id';
78
79
    /** @} */
80
81
    /** \addtogroup State
82
     * @{
83
     */
84
85
    /** \brief Status of the bot to handle data inserting and menu-like bot. */
86
    public $status;
87
88
    /** @} */
89
90
    /**
91
     * \addtogroup Bot
92
     * @{
93
     */
94
95
    /**
96
     * \brief Construct an empy bot.
97
     * \details Construct a bot with commands, multilanguage and status.
98
     */
99
    public function __construct(string $token) {
100
101
        // Parent constructor
102
        parent::__construct($token);
103
104
        // Initialize to an empty array
105
        $this->message_commands = [];
106
        $this->callback_commands = [];
107
108
        $this->keyboard = new InlineKeyboard($this);
109
110
    }
111
112
    /** \brief Descruct the class. */
113
    public function __destruct() {
114
115
        parent::__destruct();
116
117
        // Close redis connection if it is open
118
        if (isset($this->redis)) {
119
120
            $this->redis->close();
121
122
        }
123
124
    }
125
126
    /**
127
     * \brief Get the text of the message, if set (for updates of type "message").
128
     * @return Text of the message, empty string if not set.
129
     */
130
    public function getMessageText() : string {
131
132
        if (isset($this->_text)) {
133
134
            return $this->_text;
135
136
        }
137
138
        return '';
139
140
    }
141
142
    /**
143
     * \brief Get the data of callback query, if set (for updates of type "callback_query").
144
     * @return Data of the callback query, empty string if not set.
145
     */
146
    public function getCallbackData() : string {
147
148
        if (isset($this->_data)) {
149
150
            return $this->_data;
151
152
        }
153
154
        return '';
155
156
    }
157
158
    /**
159
     * \brief Get the query received from the inline query (for updates of type "inline_query").
160
     * @return The query sent by the user, throw exception if the current update is not an inline query.
161
     */
162
    public function getInlineQuery() : string {
163
164
        if (isset($this->_query)) {
165
166
            return $this->_query;
167
168
        }
169
170
        throw new BotException("Query from inline query is not set: wrong update type");
171
    }
172
173
    /**
174
     * \brief Get update and process it.
175
     * \details Call this method if you are using webhook.
176
     * It will get update from php::\input, check it and then process it using processUpdate.
177
     */
178
    public function processWebhookUpdate() {
179
180
        $this->initBot();
181
182
        $this->processUpdate(json_decode(file_get_contents('php://input'), true));
183
184
    }
185
186
    /** @} */
187
188
    /**
189
     * \addtogroup Core Core(Internal)
190
     * @{
191
     */
192
193
    /**
194
     * \brief Init variables to skip parsing commands if there aren't any.
195
     * \details Called internnaly by
196
     * - <code>getUpdatesLocal</code>
197
     * - <code>getUpdatesRedis</code>
198
     * - <code>getUpdatesDatabase</code>
199
     * - <code>processWebhookUpdate</code>
200
     */
201
    private function initBot() {
202
203
        // Are there message commands?
204
        $this->message_commands_set = !empty($this->message_commands);
205
206
        // Are there callback commands?
207
        $this->callback_commands_set = !empty($this->callback_commands);
208
209
    }
210
211
    /**
212
     * \brief Dispatch each update to the right method (processMessage, processCallbackQuery, etc).
213
     * \details Set $chat_id for each update, $text, $data and $query are set for each update that contains them.
214
     * It also calls commands for each updates, before process methods.
215
     * @param $update Reference to the update received.
216
     * @return The id of the update processed.
217
     */
218
    public function processUpdate(array $update) : int {
219
220
        if (isset($update['message'])) {
221
222
            // Set data from the message
223
            $this->_chat_id = $update['message']['chat']['id'];
224
225
            // If the message contains text
226
            if (isset($update['message']['text'])) {
227
228
                $this->_text = $update['message']['text'];
229
230
            }
231
232
            // If there are commands set by the user
233
            // and there are bot commands in the message, checking message entities
234
            if ($this->message_commands_set && isset($update['message']['entities']) && $update['message']['entities'][0]['type'] === 'bot_command') {
235
236
                // The lenght of the command
237
                $length = $update['message']['entities'][0]['length'];
238
239
                // Offset of the command
240
                $offset = $update['message']['entities'][0]['offset'];
241
242
                // For each command added by the user
243
                foreach ($this->message_commands as $trigger) {
244
245
                    // If the current command is a regex
246
                    if ($trigger['regex_active']) {
247
248
                        // Use preg_match to check if it is true
249
                        $matched = preg_match('/' . $trigger['regex_rule'] . '/', substr($update['message']['text'], $offset + 1, $length));
250
251
                        // else check if the command sent by the user is the same as the one we are expecting
252
                    } else if ($trigger['length'] == $length && mb_strpos($trigger['command'], $update['message']['text'], $offset) !== false) {
253
254
                        // We found a valid command
255
                        $matched = true;
256
257
                    } else {
258
259
                        // We did not
260
                        $matched = false;
261
262
                    }
263
264
                    // Check the results for the current command
265
                    if ($matched) {
266
267
                        // Execute script,
268
                        $trigger['script']($this, $update['message']);
269
270
                        // clear text variable
271
                        unset($this->_text);
272
273
                        // and return the id of the current update to stop processing this update
274
                        return $update['update_id'];
275
276
                    }
277
278
                }
279
280
            }
281
282
            // And process it
283
            $this->processMessage($update['message']);
284
285
            // clear text variable
286
            unset($this->_text);
287
288
            // If the update is a callback query
289
        } elseif (isset($update['callback_query'])) {
290
291
            // Set variables
292
            $this->_chat_id = $update['callback_query']['from']['id'];
293
            $this->_callback_query_id = $update['callback_query']['id'];
294
295
            // If data is set for the current callback query
296
            if (isset($update['callback_query']['data'])) {
297
298
                $this->_data = $update['callback_query']['data'];
299
300
            }
301
302
            // Check for callback commands
303
            if (isset($this->_data) && $this->callback_commands_set) {
304
305
                // Parse all commands
306
                foreach ($this->callback_commands as $trigger) {
307
308
                    // If command is found in callback data
309
                    if (strpos($trigger['data'], $this->_data) !== false) {
310
311
                        // Trigger the script
312
                        $trigger['script']($this, $update['callback_query']);
313
314
                        // Clear data
315
                        unset($this->_data);
316
                        unset($this->_callback_query_id);
317
318
                        // and return the id of the current update
319
                        return $update['update_id'];
320
321
                    }
322
323
                }
324
325
            }
326
327
            // Process the callback query through processCallbackQuery
328
            $this->processCallbackQuery($update['callback_query']);
329
330
            // Unset callback query variables
331
            unset($this->_callback_query_id);
332
            unset($this->_data);
333
334
        } elseif (isset($update['inline_query'])) {
335
336
            $this->_chat_id = $update['inline_query']['from']['id'];
337
            $this->_query = $update['inline_query']['query'];
338
            $this->_inline_query_id = $update['inline_query']['id'];
339
340
            $this->processInlineQuery($update['inline_query']);
341
342
            unset($this->_query);
343
            unset($this->_inline_query_id);
344
345
        } elseif (isset($update['channel_post'])) {
346
347
            // Set data from the post
348
            $this->_chat_id = $update['channel_post']['chat']['id'];
349
350
            $this->processChannelPost($update['channel_post']);
351
352
        } elseif (isset($update['edited_message'])) {
353
354
            $this->_chat_id = $update['edited_message']['chat']['id'];
355
356
            $this->processEditedMessage($update['edited_message']);
357
358
        } elseif (isset($update['edited_channel_post'])) {
359
360
            $this->_chat_id = $update['edited_channel_post']['chat']['id'];
361
362
            $this->processEditedChannelPost($update['edited_channel_post']);
363
364
        } elseif (isset($update['chosen_inline_result'])) {
365
366
            $this->_chat_id = $update['chosen_inline_result']['chat']['id'];
367
368
            $this->processChosenInlineResult($update['chosen_inline_result']);
369
370
        }
371
372
        return $update['update_id'];
373
374
    }
375
376
    /** @} */
377
378
    /**
379
     * \addtogroup Bot Bot
380
     * @{
381
     */
382
383
    /**
384
     * \brief Called every message received by the bot.
385
     * \details Override it to script the bot answer for each message.
386
     * <code>$chat_id</code> and <code>$text</code>, if the message contains text(use getMessageText() to access it), set inside of this function.
387
     * @param $message Reference to the message received.
388
     */
389
    protected function processMessage($message) {}
0 ignored issues
show
Unused Code introduced by
The parameter $message is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
390
391
        /**
392
         * \brief Called every callback query received by the bot.
393
         * \details Override it to script the bot answer for each callback.
394
         * <code>$chat_id</code> and <code>$data</code>, if set in the callback query(use getCallbackData() to access it) set inside of this function.
395
         * @param $callback_query Reference to the callback query received.
396
         */
397
        protected function processCallbackQuery($callback_query) {
0 ignored issues
show
Unused Code introduced by
The parameter $callback_query is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
398
399
        }
400
401
    /**
402
     * \brief Called every inline query received by the bot.
403
     * \details Override it to script the bot answer for each inline query.
404
     * $chat_id and $query(use getInlineQuery() to access it) set inside of this function.
405
     * @param $inline_query Reference to the inline query received.
406
     */
407
    protected function processInlineQuery($inline_query) {
0 ignored issues
show
Unused Code introduced by
The parameter $inline_query is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
408
409
    }
410
411
    /**
412
     * \brief Called every chosen inline result received by the bot.
413
     * \details Override it to script the bot answer for each chosen inline result.
414
     * <code>$chat_id</code> set inside of this function.
415
     * @param $chosen_inline_result Reference to the chosen inline result received.
416
     */
417
    protected function processChosenInlineResult($chosen_inline_result) {
0 ignored issues
show
Unused Code introduced by
The parameter $chosen_inline_result is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
418
419
    }
420
421
    /**
422
     * \brief Called every chosen edited message received by the bot.
423
     * \details Override it to script the bot answer for each edited message.
424
     * <code>$chat_id</code> set inside of this function.
425
     * @param $edited_message The message edited by the user.
426
     */
427
    protected function processEditedMessage($edited_message) {
0 ignored issues
show
Unused Code introduced by
The parameter $edited_message is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
428
429
    }
430
431
    /**
432
     * \brief Called every new post in the channel where the bot is in.
433
     * \details Override it to script the bot answer for each post sent in a channel.
434
     * <code>$chat_id</code> set inside of this function.
435
     * @param $post The message sent in the channel.
436
     */
437
    protected function processChannelPost($post) {
0 ignored issues
show
Unused Code introduced by
The parameter $post is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
438
439
    }
440
441
    /**
442
     * \brief Called every time a post get edited in the channel where the bot is in.
443
     * \details Override it to script the bot answer for each post edited  in a channel.
444
     * <code>$chat_id</code> set inside of this function.
445
     * @param $post The message edited in the channel.
446
     */
447
    protected function processEditedChannelPost($edited_post) {
0 ignored issues
show
Unused Code introduced by
The parameter $edited_post is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
448
449
    }
450
451
    /**
452
     * \brief Get updates received by the bot, using redis to save and get the last offset.
453
     * \details It check if an offset exists on redis, then get it, or call getUpdates to set it.
454
     * Then it start an infinite loop where it process updates and update the offset on redis.
455
     * Each update is surrounded by a try/catch.
456
     * @see getUpdates
457
     * @param $limit <i>Optional</i>. Limits the number of updates to be retrieved. Values between 1—100 are accepted.
458
     * @param $timeout <i>Optional</i>. Timeout in seconds for long polling.
459
     * @param $offset_key <i>Optional</i>. Name of the variable where the offset is saved on Redis
460
     */
461
    public function getUpdatesRedis(int $limit = 100, int $timeout = 60, string $offset_key = 'offset') {
0 ignored issues
show
Unused Code introduced by
The parameter $offset_key is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
462
463
        // Check redis connection
464
        if (!isset($this->redis)) {
465
466
            throw new BotException("Redis connection is not set");
467
468
        }
469
470
        // If offset is already set in redis
471
        if ($this->redis->exists($variable_name)) {
472
473
            // just set $offset as the same value
474
            $offset = $this->redis->get($variable_name);
0 ignored issues
show
Bug introduced by
The variable $variable_name does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
475
476 View Code Duplication
        } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
477
            // Else get the offset from the id from the first update received
478
479
            do {
480
481
                $update = $this->getUpdates(0, 1);
482
483
            } while (empty($update));
484
485
            $offset = $update[0]['update_id'];
486
487
            $this->redis->set($variable_name, $offset);
488
489
            $update = null;
490
491
        }
492
493
        $this->initBot();
494
495
        // Process all updates received
496
        while (true) {
497
498
            $updates = $this->getUpdates($offset, $limit, $timeout);
499
500
            // Parse all updates received
501
            foreach ($updates as $key => $update) {
502
503
                try {
504
505
                    $this->processUpdate($update);
506
507
                } catch (BotException $e) {
508
509
                    echo $e->getMessage();
510
511
                }
512
513
            }
514
515
            // Update the offset in redis
516
            $this->redis->set($variable_name, $offset + count($updates));
517
        }
518
519
    }
520
521
    /**
522
     * \brief Get updates received by the bot, and hold the offset in $offset.
523
     * \details Get the update_id of the first update to parse, set it in $offset and
524
     * then it start an infinite loop where it processes updates and keep $offset on the update_id of the last update received.
525
     * Each processUpdate() method call is surrounded by a try/catch.
526
     * @see getUpdates
527
     * @param $limit <i>Optional</i>. Limits the number of updates to be retrieved. Values between 1—100 are accepted.
528
     * @param $timeout <i>Optional</i>. Timeout in seconds for long polling.
529
     */
530
    public function getUpdatesLocal(int $limit = 100, int $timeout = 60) {
531
532
        $update = [];
0 ignored issues
show
Unused Code introduced by
$update is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
533
534
        // While there aren't updates to process
535
        do {
536
537
            // Get updates from telegram
538
            $update = $this->getUpdates(0, 1);
539
540
            // While in the array received there aren't updates
541
        } while (empty($update));
542
543
        // Set the offset to the first update recevied
544
        $offset = $update[0]['update_id'];
545
546
        $update = null;
547
548
        $this->initBot();
549
550
        // Process all updates
551
        while (true) {
552
553
            // Set parameter for the url call
554
            $parameters = [
555
                'offset' => $offset,
556
                'limit' => $limit,
557
                'timeout' => $timeout
558
            ];
559
560
            $updates = $this->exec_curl_request($this->_api_url . 'getUpdates?' . http_build_query($parameters));
561
562
            // Parse all update to receive
563
            foreach ($updates as $key => $update) {
564
565
                try {
566
567
                    // Process one at a time
568
                    $this->processUpdate($update);
569
570
                } catch (BotException $e) {
571
572
                    echo $e->getMessage();
573
574
                }
575
576
            }
577
578
            // Update the offset
579
            $offset += sizeof($updates);
580
581
        }
582
583
    }
584
585
    /**
586
     * \brief Get updates received by the bot, using the sql database to store and get the last offset.
587
     * \details It check if an offset exists on redis, then get it, or call getUpdates to set it.
588
     * Then it start an infinite loop where it process updates and update the offset on redis.
589
     * Each update is surrounded by a try/catch.
590
     * @see getUpdates
591
     * @param $limit <i>Optional</i>. Limits the number of updates to be retrieved. Values between 1—100 are accepted.
592
     * @param $timeout <i>Optional</i>. Timeout in seconds for long polling.
593
     * @param $table_name <i>Optional</i>. Name of the table where offset is saved in the database
594
     * @param $column_name <i>Optional</i>. Name of the column where the offset is saved in the database
595
     */
596
    public function getUpdatesDatabase(int $limit = 100, int $timeout = 0, string $table_name = 'telegram', string $column_name = 'bot_offset') {
597
598
        if (!isset($this->_database)) {
599
600
            throw new BotException("Database connection is not set");
601
602
        }
603
604
        // Get the offset from the database
605
        $sth = $this->pdo->prepare('SELECT ' . $column_name . ' FROM ' . $table_name);
606
607
        try {
608
609
            $sth->execute();
610
611
        } catch (PDOException $e) {
0 ignored issues
show
Bug introduced by
The class DanySpin97\PhpBotFramework\PDOException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
612
613
            echo $e->getMessage();
614
615
        }
616
617
        $offset = $sth->fetchColumn();
618
        $sth = null;
0 ignored issues
show
Unused Code introduced by
$sth is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
619
620
        // Get the offset from the first update to update
621 View Code Duplication
        if ($offset === false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
622
623
            do {
624
625
                $update = $this->getUpdates(0, 1);
626
627
            } while (empty($update));
628
629
            $offset = $update[0]['update_id'];
630
631
            $update = null;
632
633
        }
634
635
        // Prepare the query for updating the offset in the database
636
        $sth = $this->pdo->prepare('UPDATE "' . $table_name . '" SET "' . $column_name . '" = :new_offset');
637
638
        $this->initBot();
639
640
        while (true) {
641
642
            $updates = $this->getUpdates($offset, $limit, $timeout);
643
644
            foreach ($updates as $key => $update) {
645
646
                try {
647
648
                    $this->processUpdate($update);
649
650
                } catch (BotException $e) {
651
652
                    echo $e->getMessage();
653
654
                }
655
656
            }
657
658
            // Update the offset on the database
659
            $sth->bindParam(':new_offset', $offset + sizeof($updates));
660
            $sth->execute();
661
        }
662
    }
663
664
    /**
665
     * \brief Add a function that will be executed everytime a message contain the selected command
666
     * \details Use this syntax:
667
     *
668
     *     addMessageCommand("start", function($bot, $message) {
669
     *         $bot->sendMessage("Hi"); });
670
     * @param $command The command that will trigger this function (without slash). Eg: "start", "help", "about"
671
     * @param $script The function that will be triggered by a command. Must take an object(the bot) and an array(the message received).
672
     */
673
    public function addMessageCommand(string $command, callable $script) {
674
675
        $this->message_commands[] = [
676
            'script' => $script,
677
            'command' => '/' . $command,
678
            'length' => strlen($command) + 1,
679
            'regex_active' => false
680
        ];
681
682
    }
683
684
    /**
685
     * \brief Add a function that will be executed everytime a message contain a command that match the regex
686
     * \details Use this syntax:
687
     *
688
     *     addMessageCommandRegex("number\d", function($bot, $message, $result) {
689
     *         $bot->sendMessage("You sent me a number"); });
690
     * @param $regex_rule Regex rule that will called for evalueting the command received.
691
     * @param $script The function that will be triggered by a command. Must take an object(the bot) and an array(the message received).
692
     */
693
    public function addMessageCommandRegex(string $regex_rule, callable $script) {
694
695
        $this->message_commands[] = [
696
            'script' => $script,
697
            'regex_active' => true,
698
            'regex_rule' => $regex_rule
699
        ];
700
701
    }
702
703
    /**
704
     * \brief Add a function that will be executed everytime a callback query contains a string as data
705
     * \details Use this syntax:
706
     *
707
     *     addMessageCommand("menu", function($bot, $callback_query) {
708
     *         $bot->editMessageText($callback_query['message']['message_id'], "This is the menu"); });
709
     * @param $data The string that will trigger this function.
710
     * @param $script The function that will be triggered by the callback query if it contains the $data string. Must take an object(the bot) and an array(the callback query received).
711
     */
712
    public function addCallbackCommand(string $data, callable $script) {
713
714
        $this->callback_commands[] = [
715
            'data' => $data,
716
            'script' => $script,
717
        ];
718
719
    }
720
721
    /** @} */
722
723
    /**
724
     * \addtogroup Multilanguage Multilanguage
725
     * @{
726
     */
727
728
    /**
729
     * \brief Get current user language from the database, and set it in $language.
730
     * @param $default_language <i>Optional</i>. Default language to return in case of errors.
731
     * @return Language set for the current user, $default_language on errors.
732
     */
733
    public function getLanguageDatabase($default_language = 'en') {
734
735
        // If we have no database
736
        if (!isset($this->_database)) {
737
738
            // Set the language to english
739
            $this->language = $default_language;
740
741
            // Return english
742
            return $default_language;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $default_language; (string) is incompatible with the return type documented by DanySpin97\PhpBotFramewo...ot::getLanguageDatabase of type DanySpin97\PhpBotFramework\Language.

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...
743
744
        }
745
746
        // Get the language from the bot
747
        $sth = $this->pdo->prepare('SELECT language FROM ' . $this->user_table . ' WHERE ' . $this->id_column . ' = :chat_id');
748
        $sth->bindParam(':chat_id', $this->_chat_id);
749
750
        try {
751
752
            $sth->execute();
753
754
        } catch (PDOException $e) {
0 ignored issues
show
Bug introduced by
The class DanySpin97\PhpBotFramework\PDOException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
755
756
            echo $e->getMessage();
757
758
        }
759
760
        $row = $sth->fetch();
761
762
        $sth = null;
0 ignored issues
show
Unused Code introduced by
$sth is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
763
764
        // If we got the language
765
        if (isset($row['language'])) {
766
767
            // Set the language in the bot
768
            $this->language = $row['language'];
769
770
            // And return it
771
            return $row['language'];
772
773
        }
774
775
        // If we couldn't get it, set the language to english
776
        $this->language = $default_language;
777
778
        // and return english
779
        return $this->language;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->language; (string) is incompatible with the return type documented by DanySpin97\PhpBotFramewo...ot::getLanguageDatabase of type DanySpin97\PhpBotFramework\Language.

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...
780
781
    }
782
783
    /**
784
     * \brief Get current user language from redis, and set it in language.
785
     * \details Using redis database we get language stored and the value does not expires.
786
     * @param $default_language <i>Optional</i>. Default language to return in case of errors.
787
     * @return Language for the current user, $default_language on errors.
788
     */
789
    public function getLanguageRedis($default_language = 'en') : string {
790
791
        // If redis or pdo connection are not set
792
        if (!isset($this->redis)) {
793
794
            // return default language
795
            return $default_language;
796
797
        }
798
799
        // Does it exists on redis?
800 View Code Duplication
        if ($this->redis->exists($this->_chat_id . ':language')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
801
802
            // Get the value
803
            $this->language = $this->redis->get($this->_chat_id . ':language');
804
            return $this->language;
805
806
        }
807
808
        // If it doens't exist, set $language to $default_language
809
        $this->language = $default_language;
810
811
        // and return it
812
        return $this->language;
813
814
    }
815
816
    /**
817
     * \brief Get current user language from redis, as a cache, and set it in language.
818
     * \details Using redis database as cache, seeks the language in it, if there isn't
819
     * then get the language from the sql database and store it (with default expiring of one day) in redis.
820
     * It also change $language parameter of the bot to the language returned.
821
     * @param $default_language <i>Optional</i>. Default language to return in case of errors.
822
     * @param $expiring_time <i>Optional</i>. Set the expiring time for the language on redis each time it is took from the sql database.
823
     * @return Language for the current user, $default_language on errors.
824
     */
825
    public function getLanguageRedisAsCache($default_language = 'en', $expiring_time = '86400') : string {
826
827
        // If redis or pdo connection are not set
828
        if (!isset($this->redis) || !isset($this->pdo)) {
829
830
            // return default language
831
            return $default_language;
832
833
        }
834
835
        // Does it exists on redis?
836 View Code Duplication
        if ($this->redis->exists($this->_chat_id . ':language')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
837
838
            // Get the value
839
            $this->language = $this->redis->get($this->_chat_id . ':language');
840
            return $this->language;
841
842
        }
843
844
        // Set the value from the db
845
        $this->redis->setEx($this->_chat_id . ':language', $expiring_time, $this->getLanguageDatabase($default_language));
846
847
        // and return it
848
        return $this->language;
849
850
    }
851
852
    /**
853
     * \brief Set the current user language in both redis, sql database and $language.
854
     * \details Save it on database first, then create the expiring key on redis.
855
     * @param $language The new language to set.
856
     * @param $expiring_time <i>Optional</i>. Time for the language key in redis to expire.
857
     * @return On sucess, return true, throw exception otherwise.
858
     */
859
    public function setLanguageRedisAsCache($language, $expiring_time = '86400') {
860
861
        // Check database connection
862
        if (!isset($this->_database) && !isset($this->redis)) {
863
            throw new BotException('Database connection not set');
864
        }
865
866
        // Update the language in the database
867
        $sth = $this->pdo->prepare('UPDATE ' . $this->user_table . ' SET language = :language WHERE ' . $this->id_column . ' = :id');
868
        $sth->bindParam(':language', $language);
869
        $sth->bindParam(':id', $this->_chat_id);
870
871
        try {
872
873
            $sth->execute();
874
875
        } catch (PDOException $e) {
0 ignored issues
show
Bug introduced by
The class DanySpin97\PhpBotFramework\PDOException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
876
877
            throw new BotException($e->getMessage());
878
879
        }
880
881
        // Destroy statement
882
        $sth = null;
0 ignored issues
show
Unused Code introduced by
$sth is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
883
884
        // Set the language in redis with expiring
885
        $this->redis->setEx($this->_chat_id . ':language', $expiring_time, $language);
886
887
        // Set language in the bot variable
888
        $this->language = $language;
889
    }
890
891
    /**
892
     * \brief Load localization files (JSON-serialized) from a folder and set them in $local variable.
893
     * \details Save all localization files, saved as json format, from a directory and put the contents in $local variable.
894
     * Each file will be saved into $local with the first two letters of the filename as the index.
895
     * Access the english data as $this->local["en"]["Your key"].
896
     * File <code>./localization/en.json</code>:
897
     *
898
     *     {"Hello_Msg": "Hello"}
899
     *
900
     * File <code>./localization/it.json</code>:
901
     *
902
     *     {"Hello_Msg": "Ciao"}
903
     *
904
     * Usage in <code>processMessage()</code>:
905
     *
906
     *     $sendMessage($this->local[$this->language]["Hello_Msg"]);
907
     *
908
     * @param $dir Directory where the localization files are saved.
909
     */
910
    public function loadLocalization($dir = './localization') {
911
912
        // Open directory
913
        if ($handle = opendir($dir)) {
914
915
            // Iterate over all files
916
            while (false !== ($file = readdir($handle))) {
917
918
                // If the file is a JSON data file
919
                if (strlen($file) > 6 && substr($file, -5) === '.json') {
920
921
                    try {
922
923
                        // Add the contents of the file to the $local variable, after deserializng it from JSON format
924
                        // The contents will be added with the 2 letter of the file as the index
925
                        $this->local[substr($file, 0, 2)] = json_decode(file_get_contents("$dir/$file"), true);
926
927
                    } catch (BotException $e) {
928
929
                        echo $e->getMessage();
930
931
                    }
932
933
                }
934
935
            }
936
937
        }
938
939
    }
940
941
    /** @} */
942
943
    /**
944
     * \addtogroup State
945
     * \brief Create a state based bot using these methods.
946
     * \details Bot will answer in different way based on the state.
947
     * Here is an example where we use save user credential using bot states:
948
     *
949
     *     <?php
950
     *
951
     *     // Include the framework
952
     *     require './vendor/autoload.php';
953
     *
954
     *     // Define bot state
955
     *     define("SEND_USERNAME", 1);
956
     *     define("SEND_PASSWORD", 2);
957
     *
958
     *     // Create the class for the bot that will handle login
959
     *     class LoginBot extends DanySpin97\PhpBotFramework\Bot {
960
     *
961
     *         // Add the function for processing messages
962
     *         protected function processMessage($message) {
963
     *
964
     *             switch($this->getStatus()) {
965
     *
966
     *                 // If we are expecting a username from the user
967
     *                 case SEND_USERNAME:
968
     *
969
     *                     // Save the username
970
     *
971
     *                     // Say the user to insert the password
972
     *                     $this->sendMessage("Please, send your password.");
973
     *
974
     *                     // Update the bot state
975
     *                     $this->setStatus(SEND_PASSWORD);
976
     *
977
     *                     break;
978
     *
979
     *                 // Or if we are expecting a password from the user
980
     *                 case SEND_PASSWORD:
981
     *
982
     *                     // Save the password
983
     *
984
     *                     // Say the user he completed the process
985
     *                     $this->sendMessage("The registration is complete");
986
     *
987
     *                     break;
988
     *                 }
989
     *
990
     *         }
991
     *
992
     *     }
993
     *
994
     *     // Create the bot
995
     *     $bot = new LoginBot("token");
996
     *
997
     *     // Create redis object
998
     *     $bot->redis = new Redis();
999
     *
1000
     *     // Connect to redis database
1001
     *     $bot->redis->connect('127.0.0.1');
1002
     *
1003
     *     // Create the awnser to the <code>/start</code> command
1004
     *     $start_closure = function($bot, $message) {
1005
     *
1006
     *         // saying the user to enter a username
1007
     *         $bot->sendMessage("Please, send your username.");
1008
     *
1009
     *         // and update the status
1010
     *         $bot->setStatus(SEND_USERNAME);
1011
     *     };
1012
     *
1013
     *     // Add the answer
1014
     *     $bot->addMessageCommand("start", $start_closure);
1015
     *
1016
     *     $bot->getUpdatesLocal();
1017
     * @{
1018
     */
1019
1020
    /**
1021
     * \brief Get current user status from redis and set it in status variable.
1022
     * \details Throw exception if redis connection is missing.
1023
     * @param $default_status <i>Optional</i>. The default status to return in case there is no status for the current user.
1024
     * @return The status for the current user, $default_status if missing.
1025
     */
1026
    public function getStatus(int $default_status = -1) : int {
1027
1028
        if (!isset($this->redis)) {
1029
1030
            throw new BotException('Redis connection not set');
1031
1032
        }
1033
1034 View Code Duplication
        if ($this->redis->exists($this->_chat_id . ':status')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1035
1036
            $this->status = $this->redis->get($this->_chat_id . ':status');
1037
1038
            return $this->status;
1039
1040
        }
1041
1042
        $this->redis->set($this->_chat_id . ':status', $default_status);
1043
        $this->status = $default_status;
1044
        return $default_status;
1045
1046
    }
1047
1048
    /** \brief Set the status of the bot in both redis and $status.
1049
     * \details Throw exception if redis connection is missing.
1050
     * @param $status The new status of the bot.
1051
     */
1052
    public function setStatus(int $status) {
1053
1054
        $this->redis->set($this->_chat_id . ':status', $status);
1055
1056
        $this->status = $status;
1057
1058
    }
1059
1060
    /** @} */
1061
1062
    /**
1063
     * \addtogroup Users-handle Users handling
1064
     * \brief Handle bot users on the database.
1065
     * @{
1066
     */
1067
1068
    /** \brief Add a user to the database.
1069
     * \details Add a user to the database in Bot::$user_table table and Bot::$id_column column using Bot::$pdo connection.
1070
     * @param $chat_id chat_id of the user to add.
1071
     * @return True on success.
1072
     */
1073
    public function addUser($chat_id) : bool {
1074
1075
        // Is there database connection?
1076
        if (!isset($this->pdo)) {
1077
1078
            throw new BotException("Database connection not set");
1079
1080
        }
1081
1082
        // Create insertion query and initialize variable
1083
        $query = "INSERT INTO $this->user_table ($this->id_column) VALUES (:chat_id)";
1084
1085
        // Prepare the query
1086
        $sth = $this->pdo->prepare($query);
1087
1088
        // Add the chat_id to the query
1089
        $sth->bindParam(':chat_id', $chat_id);
1090
1091
        try {
1092
1093
            $sth->execute();
1094
            $success = true;
1095
1096
        } catch (PDOException $e) {
0 ignored issues
show
Bug introduced by
The class DanySpin97\PhpBotFramework\PDOException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
1097
1098
            echo $e->getMessage();
1099
1100
            $success = false;
1101
1102
        }
1103
1104
        // Close statement
1105
        $sth = null;
0 ignored issues
show
Unused Code introduced by
$sth is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1106
1107
        // Return result
1108
        return $success;
1109
1110
    }
1111
1112
    /**
1113
     * \brief Broadcast a message to all user registred on the database.
1114
     * \details Send a message to all users subscribed, change Bot::$user_table and Bot::$id_column to match your database structure is.
1115
     * This method requires Bot::$pdo connection set.
1116
     * All parameters are the same as CoreBot::sendMessage.
1117
     * Because a limitation of Telegram Bot API the bot will have a delay after 20 messages sent in different chats.
1118
     * @see CoreBot::sendMessage
1119
     */
1120
    public function broadcastMessage($text, string $reply_markup = null, string $parse_mode = 'HTML', bool $disable_web_preview = true, bool $disable_notification = false) {
1121
1122
        // Is there database connection?
1123
        if (!isset($this->pdo)) {
1124
1125
            throw new BotException("Database connection not set");
1126
1127
        }
1128
1129
        // Prepare the query to get all chat_id from the database
1130
        $sth = $this->pdo->prepare("SELECT $this->id_column FROM $this->user_table");
1131
1132
        try {
1133
1134
            $sth->execute();
1135
1136
        } catch (PDOException $e) {
0 ignored issues
show
Bug introduced by
The class DanySpin97\PhpBotFramework\PDOException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
1137
1138
            echo $e->getMessage();
1139
1140
        }
1141
1142
        // Iterate over all the row got
1143
        while ($user = $sth->fetch()) {
1144
1145
            // Call getChat to know that this users haven't blocked the bot
1146
            $user_data = $this->getChat($user[$this->id_column]);
1147
1148
            // Did they block it?
1149
            if ($user_data !== false) {
1150
1151
                // Change the chat_id for the next API method
1152
                $this->setChatID($user[$this->id_column]);
1153
1154
                // Send the message
1155
                $this->sendMessage($text, $reply_markup, null, $parse_mode, $disable_web_preview, $disable_notification);
1156
1157
            }
1158
1159
        }
1160
1161
        // Close statement
1162
        $sth = null;
0 ignored issues
show
Unused Code introduced by
$sth is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1163
1164
    }
1165
1166
    /** @} */
1167
1168
}
1169