Completed
Push — master ( a56e0e...d75616 )
by Danilo
02:09
created

Bot::getMessageText()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
368
369
        }
370
371
        return $update['update_id'];
372
373
    }
374
375
    /** @} */
376
377
    /**
378
     * \addtogroup Bot Bot
379
     * @{
380
     */
381
382
    /**
383
     * \brief Called every message received by the bot.
384
     * \details Override it to script the bot answer for each message.
385
     * <code>$chat_id</code> and <code>$text</code>, if the message contains text(use getMessageText() to access it), set inside of this function.
386
     * @param $message Reference to the message received.
387
     */
388
    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...
389
390
        /**
391
         * \brief Called every callback query received by the bot.
392
         * \details Override it to script the bot answer for each callback.
393
         * <code>$chat_id</code> and <code>$data</code>, if set in the callback query(use getCallbackData() to access it) set inside of this function.
394
         * @param $callback_query Reference to the callback query received.
395
         */
396
        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...
397
398
        /**
399
         * \brief Called every inline query received by the bot.
400
         * \details Override it to script the bot answer for each inline query.
401
         * $chat_id and $query(use getInlineQuery() to access it) set inside of this function.
402
         * @param $inline_query Reference to the inline query received.
403
         */
404
        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...
405
406
        /**
407
         * \brief Called every chosen inline result received by the bot.
408
         * \details Override it to script the bot answer for each chosen inline result.
409
         * <code>$chat_id</code> set inside of this function.
410
         * @param $chosen_inline_result Reference to the chosen inline result received.
411
         */
412
        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...
413
414
        /**
415
         * \brief Called every chosen edited message received by the bot.
416
         * \details Override it to script the bot answer for each edited message.
417
         * <code>$chat_id</code> set inside of this function.
418
         * @param $edited_message The message edited by the user.
419
         */
420
        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...
421
422
        /**
423
         * \brief Called every new post in the channel where the bot is in.
424
         * \details Override it to script the bot answer for each post sent in a channel.
425
         * <code>$chat_id</code> set inside of this function.
426
         * @param $post The message sent in the channel.
427
         */
428
        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...
429
430
        /**
431
         * \brief Called every time a post get edited in the channel where the bot is in.
432
         * \details Override it to script the bot answer for each post edited  in a channel.
433
         * <code>$chat_id</code> set inside of this function.
434
         * @param $post The message edited in the channel.
435
         */
436
        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...
437
438
        /**
439
         * \brief Get updates received by the bot, using redis to save and get the last offset.
440
         * \details It check if an offset exists on redis, then get it, or call getUpdates to set it.
441
         * Then it start an infinite loop where it process updates and update the offset on redis.
442
         * Each update is surrounded by a try/catch.
443
         * @see getUpdates
444
         * @param $limit <i>Optional</i>. Limits the number of updates to be retrieved. Values between 1—100 are accepted.
445
         * @param $timeout <i>Optional</i>. Timeout in seconds for long polling.
446
         * @param $offset_key <i>Optional</i>. Name of the variable where the offset is saved on Redis
447
         */
448
        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...
449
450
            // Check redis connection
451
            if (!isset($this->redis)) {
452
453
                throw new BotException("Redis connection is not set");
454
455
            }
456
457
            // If offset is already set in redis
458
            if ($this->redis->exists($variable_name)) {
459
460
                // just set $offset as the same value
461
                $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...
462
463 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...
464
                // Else get the offset from the id from the first update received
465
466
                $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...
467
468
                do {
469
                    $update = $this->getUpdates(0, 1);
470
                } while (empty($update));
471
472
                $offset = $update[0]['update_id'];
473
474
                $this->redis->set($variable_name, $offset);
475
476
                $update = null;
477
478
            }
479
480
            $this->initBot();
481
482
            // Process all updates received
483
            while (true) {
484
485
                $updates = $this->getUpdates($offset, $limit, $timeout);
486
487
                // Parse all updates received
488
                foreach ($updates as $key => $update) {
489
490
                    try {
491
492
                        $this->processUpdate($update);
493
494
                    } catch (BotException $e) {
495
496
                        echo $e->getMessage();
497
498
                    }
499
500
                }
501
502
                // Update the offset in redis
503
                $this->redis->set($variable_name, $offset + count($updates));
504
            }
505
506
        }
507
508
    /**
509
     * \brief Get updates received by the bot, and hold the offset in $offset.
510
     * \details Get the update_id of the first update to parse, set it in $offset and
511
     * then it start an infinite loop where it processes updates and keep $offset on the update_id of the last update received.
512
     * Each processUpdate() method call is surrounded by a try/catch.
513
     * @see getUpdates
514
     * @param $limit <i>Optional</i>. Limits the number of updates to be retrieved. Values between 1—100 are accepted.
515
     * @param $timeout <i>Optional</i>. Timeout in seconds for long polling.
516
     */
517
    public function getUpdatesLocal(int $limit = 100, int $timeout = 60) {
518
519
        $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...
520
521
        // While there aren't updates to process
522
        do {
523
524
            // Get updates from telegram
525
            $update = $this->getUpdates(0, 1);
526
527
            // While in the array received there aren't updates
528
        } while (empty($update));
529
530
        // Set the offset to the first update recevied
531
        $offset = $update[0]['update_id'];
532
533
        $update = null;
534
535
        $this->initBot();
536
537
        // Process all updates
538
        while (true) {
539
540
            // Set parameter for the url call
541
            $parameters = [
542
                'offset' => $offset,
543
                'limit' => $limit,
544
                'timeout' => $timeout
545
            ];
546
547
            $updates = $this->exec_curl_request($this->_api_url . 'getUpdates?' . http_build_query($parameters));
548
549
            // Parse all update to receive
550
            foreach ($updates as $key => $update) {
551
552
                try {
553
554
                    // Process one at a time
555
                    $this->processUpdate($update);
556
557
                } catch (BotException $e) {
558
559
                    echo $e->getMessage();
560
561
                }
562
563
            }
564
565
            // Update the offset
566
            $offset += sizeof($updates);
567
568
        }
569
570
    }
571
572
    /**
573
     * \brief Get updates received by the bot, using the sql database to store and get the last offset.
574
     * \details It check if an offset exists on redis, then get it, or call getUpdates to set it.
575
     * Then it start an infinite loop where it process updates and update the offset on redis.
576
     * Each update is surrounded by a try/catch.
577
     * @see getUpdates
578
     * @param $limit <i>Optional</i>. Limits the number of updates to be retrieved. Values between 1—100 are accepted.
579
     * @param $timeout <i>Optional</i>. Timeout in seconds for long polling.
580
     * @param $table_name <i>Optional</i>. Name of the table where offset is saved in the database
581
     * @param $column_name <i>Optional</i>. Name of the column where the offset is saved in the database
582
     */
583
    public function getUpdatesDatabase(int $limit = 100, int $timeout = 0, string $table_name = 'telegram', string $column_name = 'bot_offset') {
584
585
        if (!isset($this->_database)) {
586
587
            throw new BotException("Database connection is not set");
588
589
        }
590
591
        // Get the offset from the database
592
        $sth = $this->pdo->prepare('SELECT ' . $column_name . ' FROM ' . $table_name);
593
594
        try {
595
596
            $sth->execute();
597
598
        } 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...
599
600
            echo $e->getMessage();
601
602
        }
603
604
        $offset = $sth->fetchColumn();
605
        $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...
606
607
        // Get the offset from the first update to update
608 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...
609
610
            $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...
611
612
            do {
613
                $update = $this->getUpdates(0, 1);
614
            } while (empty($update));
615
616
            $offset = $update[0]['update_id'];
617
618
            $update = null;
619
620
        }
621
622
        // Prepare the query for updating the offset in the database
623
        $sth = $this->pdo->prepare('UPDATE "' . $table_name . '" SET "' . $column_name . '" = :new_offset');
624
625
        $this->initBot();
626
627
        while (true) {
628
629
            $updates = $this->getUpdates($offset, $limit, $timeout);
630
631
            foreach ($updates as $key => $update) {
632
633
                try {
634
635
                    $this->processUpdate($update);
636
637
                } catch (BotException $e) {
638
639
                    echo $e->getMessage();
640
641
                }
642
643
            }
644
645
            // Update the offset on the database
646
            $sth->bindParam(':new_offset', $offset + sizeof($updates));
647
            $sth->execute();
648
        }
649
    }
650
651
    /**
652
     * \brief Add a function that will be executed everytime a message contain the selected command
653
     * \details Use this syntax:
654
     *
655
     *     addMessageCommand("start", function($bot, $message) {
656
     *         $bot->sendMessage("Hi"); });
657
     * @param $command The command that will trigger this function (without slash). Eg: "start", "help", "about"
658
     * @param $script The function that will be triggered by a command. Must take an object(the bot) and an array(the message received).
659
     */
660
    public function addMessageCommand(string $command, callable $script) {
661
662
        $this->message_commands[] = [
663
            'script' => $script,
664
            'command' => '/' . $command,
665
            'length' => strlen($command) + 1,
666
            'regex_active' => false
667
        ];
668
669
    }
670
671
    /**
672
     * \brief Add a function that will be executed everytime a message contain a command that match the regex
673
     * \details Use this syntax:
674
     *
675
     *     addMessageCommandRegex("number\d", function($bot, $message, $result) {
676
     *         $bot->sendMessage("You sent me a number"); });
677
     * @param $regex_rule Regex rule that will called for evalueting the command received.
678
     * @param $script The function that will be triggered by a command. Must take an object(the bot) and an array(the message received).
679
     */
680
    public function addMessageCommandRegex(string $regex_rule, callable $script) {
681
682
        $this->message_commands[] = [
683
            'script' => $script,
684
            'regex_active' => true,
685
            'regex_rule' => $regex_rule
686
        ];
687
688
    }
689
690
    /**
691
     * \brief Add a function that will be executed everytime a callback query contains a string as data
692
     * \details Use this syntax:
693
     *
694
     *     addMessageCommand("menu", function($bot, $callback_query) {
695
     *         $bot->editMessageText($callback_query['message']['message_id'], "This is the menu"); });
696
     * @param $data The string that will trigger this function.
697
     * @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).
698
     */
699
    public function addCallbackCommand(string $data, callable $script) {
700
701
        $this->callback_commands[] = [
702
            'data' => $data,
703
            'script' => $script,
704
        ];
705
706
    }
707
708
    /** @} */
709
710
    /**
711
     * \addtogroup Multilanguage Multilanguage
712
     * @{
713
     */
714
715
    /**
716
     * \brief Get current user language from the database, and set it in $language.
717
     * @param $default_language <i>Optional</i>. Default language to return in case of errors.
718
     * @return Language set for the current user, $default_language on errors.
719
     */
720
    public function getLanguageDatabase($default_language = 'en') {
721
722
        // If we have no database
723
        if (!isset($this->_database)) {
724
725
            // Set the language to english
726
            $this->language = $default_language;
727
728
            // Return english
729
            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...
730
731
        }
732
733
        // Get the language from the bot
734
        $sth = $this->pdo->prepare('SELECT language FROM ' . $this->user_table . ' WHERE ' . $this->id_column . ' = :chat_id');
735
        $sth->bindParam(':chat_id', $this->_chat_id);
736
737
        try {
738
739
            $sth->execute();
740
741
        } 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...
742
743
            echo $e->getMessage();
744
745
        }
746
747
        $row = $sth->fetch();
748
749
        $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...
750
751
        // If we got the language
752
        if (isset($row['language'])) {
753
754
            // Set the language in the bot
755
            $this->language = $row['language'];
756
757
            // And return it
758
            return $row['language'];
759
760
        }
761
762
        // If we couldn't get it, set the language to english
763
        $this->language = $default_language;
764
765
        // and return english
766
        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...
767
768
    }
769
770
    /**
771
     * \brief Get current user language from redis, and set it in language.
772
     * \details Using redis database we get language stored and the value does not expires.
773
     * @param $default_language <i>Optional</i>. Default language to return in case of errors.
774
     * @return Language for the current user, $default_language on errors.
775
     */
776
    public function getLanguageRedis($default_language = 'en') : string {
777
778
        // If redis or pdo connection are not set
779
        if (!isset($this->redis)) {
780
781
            // return default language
782
            return $default_language;
783
784
        }
785
786
        // Does it exists on redis?
787 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...
788
789
            // Get the value
790
            $this->language = $this->redis->get($this->_chat_id . ':language');
791
            return $this->language;
792
793
        }
794
795
        // If it doens't exist, set $language to $default_language
796
        $this->language = $default_language;
797
798
        // and return it
799
        return $this->language;
800
801
    }
802
803
    /**
804
     * \brief Get current user language from redis, as a cache, and set it in language.
805
     * \details Using redis database as cache, seeks the language in it, if there isn't
806
     * then get the language from the sql database and store it (with default expiring of one day) in redis.
807
     * It also change $language parameter of the bot to the language returned.
808
     * @param $default_language <i>Optional</i>. Default language to return in case of errors.
809
     * @param $expiring_time <i>Optional</i>. Set the expiring time for the language on redis each time it is took from the sql database.
810
     * @return Language for the current user, $default_language on errors.
811
     */
812
    public function getLanguageRedisAsCache($default_language = 'en', $expiring_time = '86400') : string {
813
814
        // If redis or pdo connection are not set
815
        if (!isset($this->redis) || !isset($this->pdo)) {
816
817
            // return default language
818
            return $default_language;
819
820
        }
821
822
        // Does it exists on redis?
823 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...
824
825
            // Get the value
826
            $this->language = $this->redis->get($this->_chat_id . ':language');
827
            return $this->language;
828
829
        }
830
831
        // Set the value from the db
832
        $this->redis->setEx($this->_chat_id . ':language', $expiring_time, $this->getLanguageDatabase($default_language));
833
834
        // and return it
835
        return $this->language;
836
837
    }
838
839
    /**
840
     * \brief Set the current user language in both redis, sql database and $language.
841
     * \details Save it on database first, then create the expiring key on redis.
842
     * @param $language The new language to set.
843
     * @param $expiring_time <i>Optional</i>. Time for the language key in redis to expire.
844
     * @return On sucess, return true, throw exception otherwise.
845
     */
846
    public function setLanguageRedisAsCache($language, $expiring_time = '86400') {
847
848
        // Check database connection
849
        if (!isset($this->_database) && !isset($this->redis)) {
850
            throw new BotException('Database connection not set');
851
        }
852
853
        // Update the language in the database
854
        $sth = $this->pdo->prepare('UPDATE ' . $this->user_table . ' SET language = :language WHERE ' . $this->id_column . ' = :id');
855
        $sth->bindParam(':language', $language);
856
        $sth->bindParam(':id', $this->_chat_id);
857
858
        try {
859
860
            $sth->execute();
861
862
        } 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...
863
864
            throw new BotException($e->getMessage());
865
866
        }
867
868
        // Destroy statement
869
        $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...
870
871
        // Set the language in redis with expiring
872
        $this->redis->setEx($this->_chat_id . ':language', $expiring_time, $language);
873
874
        // Set language in the bot variable
875
        $this->language = $language;
876
    }
877
878
    /**
879
     * \brief Load localization files (JSON-serialized) from a folder and set them in $local variable.
880
     * \details Save all localization files, saved as json format, from a directory and put the contents in $local variable.
881
     * Each file will be saved into $local with the first two letters of the filename as the index.
882
     * Access the english data as $this->local["en"]["Your key"].
883
     * File <code>./localization/en.json</code>:
884
     *
885
     *     {"Hello_Msg": "Hello"}
886
     *
887
     * File <code>./localization/it.json</code>:
888
     *
889
     *     {"Hello_Msg": "Ciao"}
890
     *
891
     * Usage in <code>processMessage()</code>:
892
     *
893
     *     $sendMessage($this->local[$this->language]["Hello_Msg"]);
894
     *
895
     * @param $dir Directory where the localization files are saved.
896
     */
897
    public function loadLocalization($dir = './localization') {
898
899
        // Open directory
900
        if ($handle = opendir($dir)) {
901
902
            // Iterate over all files
903
            while (false !== ($file = readdir($handle))) {
904
905
                // If the file is a JSON data file
906
                if (strlen($file) > 6 && substr($file, -5) === '.json') {
907
908
                    try {
909
910
                        // Add the contents of the file to the $local variable, after deserializng it from JSON format
911
                        // The contents will be added with the 2 letter of the file as the index
912
                        $this->local[substr($file, 0, 2)] = json_decode(file_get_contents("$dir/$file"), true);
913
914
                    } catch (BotException $e) {
915
916
                        echo $e->getMessage();
917
918
                    }
919
920
                }
921
922
            }
923
924
        }
925
926
    }
927
928
    /** @} */
929
930
    /**
931
     * \addtogroup State
932
     * \brief Create a state based bot using these methods.
933
     * \details Bot will answer in different way based on the state.
934
     * Here is an example where we use save user credential using bot states:
935
     *
936
     *     <?php
937
     *
938
     *     // Include the framework
939
     *     require './vendor/autoload.php';
940
     *
941
     *     // Define bot state
942
     *     define("SEND_USERNAME", 1);
943
     *     define("SEND_PASSWORD", 2);
944
     *
945
     *     // Create the class for the bot that will handle login
946
     *     class LoginBot extends DanySpin97\PhpBotFramework\Bot {
947
     *
948
     *         // Add the function for processing messages
949
     *         protected function processMessage($message) {
950
     *
951
     *             switch($this->getStatus()) {
952
     *
953
     *                 // If we are expecting a username from the user
954
     *                 case SEND_USERNAME:
955
     *
956
     *                     // Save the username
957
     *
958
     *                     // Say the user to insert the password
959
     *                     $this->sendMessage("Please, send your password.");
960
     *
961
     *                     // Update the bot state
962
     *                     $this->setStatus(SEND_PASSWORD);
963
     *
964
     *                     break;
965
     *
966
     *                 // Or if we are expecting a password from the user
967
     *                 case SEND_PASSWORD:
968
     *
969
     *                     // Save the password
970
     *
971
     *                     // Say the user he completed the process
972
     *                     $this->sendMessage("The registration is complete");
973
     *
974
     *                     break;
975
     *                 }
976
     *
977
     *         }
978
     *
979
     *     }
980
     *
981
     *     // Create the bot
982
     *     $bot = new LoginBot("token");
983
     *
984
     *     // Create redis object
985
     *     $bot->redis = new Redis();
986
     *
987
     *     // Connect to redis database
988
     *     $bot->redis->connect('127.0.0.1');
989
     *
990
     *     // Create the awnser to the <code>/start</code> command
991
     *     $start_closure = function($bot, $message) {
992
     *
993
     *         // saying the user to enter a username
994
     *         $bot->sendMessage("Please, send your username.");
995
     *
996
     *         // and update the status
997
     *         $bot->setStatus(SEND_USERNAME);
998
     *     };
999
     *
1000
     *     // Add the answer
1001
     *     $bot->addMessageCommand("start", $start_closure);
1002
     *
1003
     *     $bot->getUpdatesLocal();
1004
     * @{
1005
     */
1006
1007
    /**
1008
     * \brief Get current user status from redis and set it in status variable.
1009
     * \details Throw exception if redis connection is missing.
1010
     * @param $default_status <i>Optional</i>. The default status to return in case there is no status for the current user.
1011
     * @return The status for the current user, $default_status if missing.
1012
     */
1013
    public function getStatus(int $default_status = -1) : int {
1014
1015
        if (!isset($this->redis)) {
1016
1017
            throw new BotException('Redis connection not set');
1018
1019
        }
1020
1021 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...
1022
1023
            $this->status = $this->redis->get($this->_chat_id . ':status');
1024
1025
            return $this->status;
1026
1027
        }
1028
1029
        $this->redis->set($this->_chat_id . ':status', $default_status);
1030
        $this->status = $default_status;
1031
        return $default_status;
1032
1033
    }
1034
1035
    /** \brief Set the status of the bot in both redis and $status.
1036
     * \details Throw exception if redis connection is missing.
1037
     * @param $status The new status of the bot.
1038
     */
1039
    public function setStatus(int $status) {
1040
1041
        $this->redis->set($this->_chat_id . ':status', $status);
1042
1043
        $this->status = $status;
1044
1045
    }
1046
1047
    /** @} */
1048
1049
    /**
1050
     * \addtogroup Users-handle Users handling
1051
     * \brief Handle bot users on the database.
1052
     * @{
1053
     */
1054
1055
    /** \brief Add a user to the database.
1056
     * \details Add a user to the database in Bot::$user_table table and Bot::$id_column column using Bot::$pdo connection.
1057
     * @param $chat_id chat_id of the user to add.
1058
     * @return True on success.
1059
     */
1060
    public function addUser($chat_id) : bool {
1061
1062
        // Is there database connection?
1063
        if (!isset($this->pdo)) {
1064
1065
            throw new BotException("Database connection not set");
1066
1067
        }
1068
1069
        // Create insertion query and initialize variable
1070
        $query = "INSERT INTO $this->user_table ($this->id_column) VALUES (:chat_id)";
1071
1072
        // Prepare the query
1073
        $sth = $this->pdo->prepare($query);
1074
1075
        // Add the chat_id to the query
1076
        $sth->bindParam(':chat_id', $chat_id);
1077
1078
        try {
1079
1080
            $sth->execute();
1081
            $success = true;
1082
1083
        } 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...
1084
1085
            echo $e->getMessage();
1086
1087
            $success = false;
1088
1089
        }
1090
1091
        // Close statement
1092
        $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...
1093
1094
        // Return result
1095
        return $success;
1096
1097
    }
1098
1099
    /**
1100
     * \brief Broadcast a message to all user registred on the database.
1101
     * \details Send a message to all users subscribed, change Bot::$user_table and Bot::$id_column to match your database structure is.
1102
     * This method requires Bot::$pdo connection set.
1103
     * All parameters are the same as CoreBot::sendMessage.
1104
     * Because a limitation of Telegram Bot API the bot will have a delay after 20 messages sent in different chats.
1105
     * @see CoreBot::sendMessage
1106
     */
1107
    public function broadcastMessage($text, string $reply_markup = null, string $parse_mode = 'HTML', bool $disable_web_preview = true, bool $disable_notification = false) {
0 ignored issues
show
Unused Code introduced by
The parameter $disable_notification 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...
1108
1109
        // Is there database connection?
1110
        if (!isset($this->pdo)) {
1111
1112
            throw new BotException("Database connection not set");
1113
1114
        }
1115
1116
        // Prepare the query to get all chat_id from the database
1117
        $sth = $this->pdo->prepare("SELECT $this->id_column FROM $this->user_table");
1118
1119
        try {
1120
1121
            $sth->execute();
1122
1123
        } 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...
1124
1125
            echo $e->getMessage();
1126
1127
        }
1128
1129
        // Iterate over all the row got
1130
        while($user = $sth->fetch()) {
1131
1132
            // Call getChat to know that this users haven't blocked the bot
1133
            $user_data = $this->getChat($user[$this->id_column]);
1134
1135
            // Did they block it?
1136
            if ($user_data !== false) {
1137
1138
                // Change the chat_id for the next API method
1139
                $this->setChatID($user[$this->id_column]);
1140
1141
                // Send the message
1142
                $this->sendMessage($text, $reply_markup, null, $parse_mode, $disable_web_preview);
1143
1144
            }
1145
1146
        }
1147
1148
        // Close statement
1149
        $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...
1150
1151
    }
1152
1153
    /** @} */
1154
1155
}
1156