Completed
Branch scrutinizer_fixes (e85bf7)
by Armando
02:47
created

Telegram::addCommandsPaths()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 3
Bugs 1 Features 1
Metric Value
c 3
b 1
f 1
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 9.4285
cc 2
eloc 4
nc 2
nop 2
crap 2
1
<?php
2
/**
3
 * This file is part of the TelegramBot package.
4
 *
5
 * (c) Avtandil Kikabidze aka LONGMAN <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Longman\TelegramBot;
12
13 1
define('BASE_PATH', __DIR__);
14 1
define('BASE_COMMANDS_PATH', BASE_PATH . '/Commands');
15
16
use Exception;
17
use Longman\TelegramBot\Commands\Command;
18
use Longman\TelegramBot\Entities\ServerResponse;
19
use Longman\TelegramBot\Entities\Update;
20
use Longman\TelegramBot\Exception\TelegramException;
21
use PDO;
22
use RecursiveDirectoryIterator;
23
use RecursiveIteratorIterator;
24
use RegexIterator;
25
26
class Telegram
27
{
28
    /**
29
     * Version
30
     *
31
     * @var string
32
     */
33
    protected $version = '0.35.0';
34
35
    /**
36
     * Telegram API key
37
     *
38
     * @var string
39
     */
40
    protected $api_key = '';
41
42
    /**
43
     * Telegram Bot name
44
     *
45
     * @var string
46
     */
47
    protected $bot_name = '';
48
49
    /**
50
     * Raw request data (json) for webhook methods
51
     *
52
     * @var string
53
     */
54
    protected $input;
55
56
    /**
57
     * Custom commands paths
58
     *
59
     * @var array
60
     */
61
    protected $commands_paths = [];
62
63
    /**
64
     * Current Update object
65
     *
66
     * @var \Longman\TelegramBot\Entities\Update
67
     */
68
    protected $update;
69
70
    /**
71
     * Upload path
72
     *
73
     * @var string
74
     */
75
    protected $upload_path;
76
77
    /**
78
     * Download path
79
     *
80
     * @var string
81
     */
82
    protected $download_path;
83
84
    /**
85
     * MySQL integration
86
     *
87
     * @var boolean
88
     */
89
    protected $mysql_enabled = false;
90
91
    /**
92
     * PDO object
93
     *
94
     * @var \PDO
95
     */
96
    protected $pdo;
97
98
    /**
99
     * Commands config
100
     *
101
     * @var array
102
     */
103
    protected $commands_config = [];
104
105
    /**
106
     * Admins list
107
     *
108
     * @var array
109
     */
110
    protected $admins_list = [];
111
112
    /**
113
     * ServerResponse of the last Command execution
114
     *
115
     * @var \Longman\TelegramBot\Entities\ServerResponse
116
     */
117
    protected $last_command_response;
118
119
    /**
120
     * Botan.io integration
121
     *
122
     * @var boolean
123
     */
124
    protected $botan_enabled = false;
125
126
    /**
127
     * Telegram constructor.
128
     *
129
     * @param string $api_key
130
     * @param string $bot_name
131
     *
132
     * @throws \Longman\TelegramBot\Exception\TelegramException
133
     */
134 39
    public function __construct($api_key, $bot_name)
135
    {
136 39
        if (empty($api_key)) {
137 1
            throw new TelegramException('API KEY not defined!');
138
        }
139
140 39
        if (empty($bot_name)) {
141 1
            throw new TelegramException('Bot Username not defined!');
142
        }
143
144 39
        $this->api_key  = $api_key;
145 39
        $this->bot_name = $bot_name;
146
147
        //Set default download and upload path
148 39
        $this->setDownloadPath(BASE_PATH . '/../Download');
149 39
        $this->setUploadPath(BASE_PATH . '/../Upload');
150
151
        //Add default system commands path
152 39
        $this->addCommandsPath(BASE_COMMANDS_PATH . '/SystemCommands');
153
154 39
        Request::initialize($this);
155 39
    }
156
157
    /**
158
     * Initialize Database connection
159
     *
160
     * @param array  $credential
161
     * @param string $table_prefix
162
     * @param string $encoding
163
     *
164
     * @return \Longman\TelegramBot\Telegram
165
     * @throws \Longman\TelegramBot\Exception\TelegramException
166
     */
167 9
    public function enableMySql(array $credential, $table_prefix = null, $encoding = 'utf8mb4')
168
    {
169 9
        $this->pdo = DB::initialize($credential, $this, $table_prefix, $encoding);
170 9
        ConversationDB::initializeConversation();
171 9
        $this->mysql_enabled = true;
172
173 9
        return $this;
174
    }
175
176
    /**
177
     * Initialize Database external connection
178
     *
179
     * @param PDO    $external_pdo_connection PDO database object
180
     * @param string $table_prefix
181
     *
182
     * @throws \Longman\TelegramBot\Exception\TelegramException
183
     */
184
    public function enableExternalMysql($external_pdo_connection, $table_prefix = null)
185
    {
186
        $this->pdo = DB::externalInitialize($external_pdo_connection, $this, $table_prefix);
187
        ConversationDB::initializeConversation();
188
        $this->mysql_enabled = true;
189
    }
190
191
    /**
192
     * Get commands list
193
     *
194
     * @return array $commands
195
     * @throws \Longman\TelegramBot\Exception\TelegramException
196
     */
197 10
    public function getCommandsList()
198
    {
199 10
        $commands = [];
200
201 10
        foreach ($this->commands_paths as $path) {
202
            try {
203
                //Get all "*Command.php" files
204 10
                $files = new RegexIterator(
205 10
                    new RecursiveIteratorIterator(
206 10
                        new RecursiveDirectoryIterator($path)
207
                    ),
208 10
                    '/^.+Command.php$/'
209
                );
210
211 10
                foreach ($files as $file) {
212
                    //Remove "Command.php" from filename
213 10
                    $command      = $this->sanitizeCommand(substr($file->getFilename(), 0, -11));
214 10
                    $command_name = strtolower($command);
215
216 10
                    if (array_key_exists($command_name, $commands)) {
217
                        continue;
218
                    }
219
220 10
                    require_once $file->getPathname();
221
222 10
                    $command_obj = $this->getCommandObject($command);
223 10
                    if ($command_obj instanceof Command) {
224 10
                        $commands[$command_name] = $command_obj;
225
                    }
226
                }
227
            } catch (Exception $e) {
228 10
                throw new TelegramException('Error getting commands from path: ' . $path);
229
            }
230
        }
231
232 10
        return $commands;
233
    }
234
235
    /**
236
     * Get an object instance of the passed command
237
     *
238
     * @param string $command
239
     *
240
     * @return \Longman\TelegramBot\Commands\Command|null
241
     */
242 11
    public function getCommandObject($command)
243
    {
244 11
        $which = ['System'];
245 11
        $this->isAdmin() && $which[] = 'Admin';
246 11
        $which[] = 'User';
247
248 11
        foreach ($which as $auth) {
249 11
            $command_namespace = __NAMESPACE__ . '\\Commands\\' . $auth . 'Commands\\' . $this->ucfirstUnicode($command) . 'Command';
250 11
            if (class_exists($command_namespace)) {
251 11
                return new $command_namespace($this, $this->update);
252
            }
253
        }
254
255
        return null;
256
    }
257
258
    /**
259
     * Set custom input string for debug purposes
260
     *
261
     * @param string $input (json format)
262
     *
263
     * @return \Longman\TelegramBot\Telegram
264
     */
265
    public function setCustomInput($input)
266
    {
267
        $this->input = $input;
268
269
        return $this;
270
    }
271
272
    /**
273
     * Get custom input string for debug purposes
274
     *
275
     * @return string
276
     */
277
    public function getCustomInput()
278
    {
279
        return $this->input;
280
    }
281
282
    /**
283
     * Get the ServerResponse of the last Command execution
284
     *
285
     * @return \Longman\TelegramBot\Entities\ServerResponse
286
     */
287
    public function getLastCommandResponse()
288
    {
289
        return $this->last_command_response;
290
    }
291
292
    /**
293
     * Handle getUpdates method
294
     *
295
     * @param int|null $limit
296
     * @param int|null $timeout
297
     *
298
     * @return \Longman\TelegramBot\Entities\ServerResponse
299
     * @throws \Longman\TelegramBot\Exception\TelegramException
300
     */
301
    public function handleGetUpdates($limit = null, $timeout = null)
302
    {
303
        if (!DB::isDbConnected()) {
304
            return new ServerResponse(
305
                [
306
                    'ok'          => false,
307
                    'description' => 'getUpdates needs MySQL connection!',
308
                ],
309
                $this->bot_name
310
            );
311
        }
312
313
        //DB Query
314
        $last_update = DB::selectTelegramUpdate(1);
315
        $last_update = reset($last_update);
316
317
        //As explained in the telegram bot api documentation
318
        $offset = isset($last_update['id']) ? $last_update['id'] + 1 : null;
319
320
        $response = Request::getUpdates(
321
            [
322
                'offset'  => $offset,
323
                'limit'   => $limit,
324
                'timeout' => $timeout,
325
            ]
326
        );
327
328
        if ($response->isOk()) {
329
            //Process all updates
330
            /** @var Update $result */
331
            foreach ((array)$response->getResult() as $result) {
332
                $this->processUpdate($result);
333
            }
334
        }
335
336
        return $response;
337
    }
338
339
    /**
340
     * Handle bot request from webhook
341
     *
342
     * @return bool
343
     *
344
     * @throws \Longman\TelegramBot\Exception\TelegramException
345
     */
346
    public function handle()
347
    {
348
        $this->input = Request::getInput();
349
350
        if (empty($this->input)) {
351
            throw new TelegramException('Input is empty!');
352
        }
353
354
        $post = json_decode($this->input, true);
355
        if (empty($post)) {
356
            throw new TelegramException('Invalid JSON!');
357
        }
358
359
        if ($response = $this->processUpdate(new Update($post, $this->bot_name))) {
360
            return $response->isOk();
361
        }
362
363
        return false;
364
    }
365
366
    /**
367
     * Get the command name from the command type
368
     *
369
     * @param string $type
370
     *
371
     * @return string
372
     */
373
    private function getCommandFromType($type)
374
    {
375
        return $this->ucfirstUnicode(str_replace('_', '', $type));
376
    }
377
378
    /**
379
     * Process bot Update request
380
     *
381
     * @param \Longman\TelegramBot\Entities\Update $update
382
     *
383
     * @return \Longman\TelegramBot\Entities\ServerResponse
384
     * @throws \Longman\TelegramBot\Exception\TelegramException
385
     */
386
    public function processUpdate(Update $update)
387
    {
388
        $this->update = $update;
389
390
        //If all else fails, it's a generic message.
391
        $command = 'genericmessage';
392
393
        $update_type = $this->update->getUpdateType();
394
        if (in_array($update_type, ['inline_query', 'chosen_inline_result', 'callback_query', 'edited_message'], true)) {
395
            $command = $this->getCommandFromType($update_type);
396
        } elseif ($update_type === 'message') {
397
            $message = $this->update->getMessage();
398
399
            //Load admin commands
400
            if ($this->isAdmin()) {
401
                $this->addCommandsPath(BASE_COMMANDS_PATH . '/AdminCommands', false);
402
            }
403
404
            $this->addCommandsPath(BASE_COMMANDS_PATH . '/UserCommands', false);
405
406
            $type = $message->getType();
407
            if ($type === 'command') {
408
                $command = $message->getCommand();
409
            } elseif (in_array($type, [
410
                'channel_chat_created',
411
                'delete_chat_photo',
412
                'group_chat_created',
413
                'left_chat_member',
414
                'migrate_from_chat_id',
415
                'migrate_to_chat_id',
416
                'new_chat_member',
417
                'new_chat_photo',
418
                'new_chat_title',
419
                'supergroup_chat_created',
420
            ], true)) {
421
                $command = $this->getCommandFromType($type);
422
            }
423
        }
424
425
        //Make sure we have an up-to-date command list
426
        //This is necessary to "require" all the necessary command files!
427
        $this->getCommandsList();
428
429
        DB::insertRequest($this->update);
430
431
        return $this->executeCommand($command);
0 ignored issues
show
Bug introduced by
It seems like $command defined by $message->getCommand() on line 408 can also be of type boolean; however, Longman\TelegramBot\Telegram::executeCommand() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
432
    }
433
434
    /**
435
     * Execute /command
436
     *
437
     * @param string $command
438
     *
439
     * @return mixed
440
     * @throws \Longman\TelegramBot\Exception\TelegramException
441
     */
442
    public function executeCommand($command)
443
    {
444
        $command_obj = $this->getCommandObject($command);
445
446
        if (!$command_obj || !$command_obj->isEnabled()) {
447
            //Failsafe in case the Generic command can't be found
448
            if ($command === 'Generic') {
449
                throw new TelegramException('Generic command missing!');
450
            }
451
452
            //Handle a generic command or non existing one
453
            $this->last_command_response = $this->executeCommand('Generic');
454
        } else {
455
            //Botan.io integration, make sure only the command user executed is reported
456
            if ($this->botan_enabled) {
457
                Botan::lock($command);
458
            }
459
460
            //execute() method is executed after preExecute()
461
            //This is to prevent executing a DB query without a valid connection
462
            $this->last_command_response = $command_obj->preExecute();
463
464
            //Botan.io integration, send report after executing the command
465
            if ($this->botan_enabled) {
466
                Botan::track($this->update, $command);
467
            }
468
        }
469
470
        return $this->last_command_response;
471
    }
472
473
    /**
474
     * Sanitize Command
475
     *
476
     * @param string $command
477
     *
478
     * @return string
479
     */
480 10
    protected function sanitizeCommand($command)
481
    {
482 10
        return str_replace(' ', '', $this->ucwordsUnicode(str_replace('_', ' ', $command)));
483
    }
484
485
    /**
486
     * Enable a single Admin account
487
     *
488
     * @param integer $admin_id Single admin id
489
     *
490
     * @return \Longman\TelegramBot\Telegram
491
     */
492 1
    public function enableAdmin($admin_id)
493
    {
494 1
        if (is_int($admin_id) && $admin_id > 0 && !in_array($admin_id, $this->admins_list, true)) {
495 1
            $this->admins_list[] = $admin_id;
496
        } else {
497 1
            TelegramLog::error('Invalid value "' . $admin_id . '" for admin.');
498
        }
499
500 1
        return $this;
501
    }
502
503
    /**
504
     * Enable a list of Admin Accounts
505
     *
506
     * @param array $admin_ids List of admin ids
507
     *
508
     * @return \Longman\TelegramBot\Telegram
509
     */
510 1
    public function enableAdmins(array $admin_ids)
511
    {
512 1
        foreach ($admin_ids as $admin_id) {
513 1
            $this->enableAdmin($admin_id);
514
        }
515
516 1
        return $this;
517
    }
518
519
    /**
520
     * Get list of admins
521
     *
522
     * @return array
523
     */
524 1
    public function getAdminList()
525
    {
526 1
        return $this->admins_list;
527
    }
528
529
    /**
530
     * Check if the passed user is an admin
531
     *
532
     * If no user id is passed, the current update is checked for a valid message sender.
533
     *
534
     * @param int|null $user_id
535
     *
536
     * @return bool
537
     */
538 11
    public function isAdmin($user_id = null)
539
    {
540 11
        if ($user_id === null && $this->update !== null) {
541
            if (($message = $this->update->getMessage()) && ($from = $message->getFrom())) {
542
                $user_id = $from->getId();
543
            } elseif (($inline_query = $this->update->getInlineQuery()) && ($from = $inline_query->getFrom())) {
544
                $user_id = $from->getId();
545
            } elseif (($chosen_inline_result = $this->update->getChosenInlineResult()) && ($from = $chosen_inline_result->getFrom())) {
546
                $user_id = $from->getId();
547
            } elseif (($callback_query = $this->update->getCallbackQuery()) && ($from = $callback_query->getFrom())) {
548
                $user_id = $from->getId();
549
            } elseif (($edited_message = $this->update->getEditedMessage()) && ($from = $edited_message->getFrom())) {
550
                $user_id = $from->getId();
551
            }
552
        }
553
554 11
        return ($user_id === null) ? false : in_array($user_id, $this->admins_list, true);
555
    }
556
557
    /**
558
     * Check if user required the db connection
559
     *
560
     * @return bool
561
     */
562
    public function isDbEnabled()
563
    {
564
        if ($this->mysql_enabled) {
565
            return true;
566
        } else {
567
            return false;
568
        }
569
    }
570
571
    /**
572
     * Add a single custom commands path
573
     *
574
     * @param string $path   Custom commands path to add
575
     * @param bool   $before If the path should be prepended or appended to the list
576
     *
577
     * @return \Longman\TelegramBot\Telegram
578
     */
579 39
    public function addCommandsPath($path, $before = true)
580
    {
581 39
        if (!is_dir($path)) {
582 1
            TelegramLog::error('Commands path "' . $path . '" does not exist.');
583 39
        } elseif (!in_array($path, $this->commands_paths, true)) {
584 39
            if ($before) {
585 39
                array_unshift($this->commands_paths, $path);
586
            } else {
587
                $this->commands_paths[] = $path;
588
            }
589
        }
590
591 39
        return $this;
592
    }
593
594
    /**
595
     * Add multiple custom commands paths
596
     *
597
     * @param array $paths  Custom commands paths to add
598
     * @param bool  $before If the paths should be prepended or appended to the list
599
     *
600
     * @return \Longman\TelegramBot\Telegram
601
     */
602 1
    public function addCommandsPaths(array $paths, $before = true)
603
    {
604 1
        foreach ($paths as $path) {
605 1
            $this->addCommandsPath($path, $before);
606
        }
607
608 1
        return $this;
609
    }
610
611
    /**
612
     * Set custom upload path
613
     *
614
     * @param string $path Custom upload path
615
     *
616
     * @return \Longman\TelegramBot\Telegram
617
     */
618 39
    public function setUploadPath($path)
619
    {
620 39
        $this->upload_path = $path;
621
622 39
        return $this;
623
    }
624
625
    /**
626
     * Get custom upload path
627
     *
628
     * @return string
629
     */
630
    public function getUploadPath()
631
    {
632
        return $this->upload_path;
633
    }
634
635
    /**
636
     * Set custom download path
637
     *
638
     * @param string $path Custom download path
639
     *
640
     * @return \Longman\TelegramBot\Telegram
641
     */
642 39
    public function setDownloadPath($path)
643
    {
644 39
        $this->download_path = $path;
645
646 39
        return $this;
647
    }
648
649
    /**
650
     * Get custom download path
651
     *
652
     * @return string
653
     */
654
    public function getDownloadPath()
655
    {
656
        return $this->download_path;
657
    }
658
659
    /**
660
     * Set command config
661
     *
662
     * Provide further variables to a particular commands.
663
     * For example you can add the channel name at the command /sendtochannel
664
     * Or you can add the api key for external service.
665
     *
666
     * @param string $command
667
     * @param array  $config
668
     *
669
     * @return \Longman\TelegramBot\Telegram
670
     */
671 13
    public function setCommandConfig($command, array $config)
672
    {
673 13
        $this->commands_config[$command] = $config;
674
675 13
        return $this;
676
    }
677
678
    /**
679
     * Get command config
680
     *
681
     * @param string $command
682
     *
683
     * @return array
684
     */
685 24
    public function getCommandConfig($command)
686
    {
687 24
        return isset($this->commands_config[$command]) ? $this->commands_config[$command] : [];
688
    }
689
690
    /**
691
     * Get API key
692
     *
693
     * @return string
694
     */
695 1
    public function getApiKey()
696
    {
697 1
        return $this->api_key;
698
    }
699
700
    /**
701
     * Get Bot name
702
     *
703
     * @return string
704
     */
705 6
    public function getBotName()
706
    {
707 6
        return $this->bot_name;
708
    }
709
710
    /**
711
     * Get Version
712
     *
713
     * @return string
714
     */
715 1
    public function getVersion()
716
    {
717 1
        return $this->version;
718
    }
719
720
    /**
721
     * Set Webhook for bot
722
     *
723
     * @param string      $url
724
     * @param string|null $path_certificate
725
     *
726
     * @return \Longman\TelegramBot\Entities\ServerResponse
727
     * @throws \Longman\TelegramBot\Exception\TelegramException
728
     */
729
    public function setWebHook($url, $path_certificate = null)
730
    {
731
        if (empty($url)) {
732
            throw new TelegramException('Hook url is empty!');
733
        }
734
735
        $result = Request::setWebhook($url, $path_certificate);
736
737 View Code Duplication
        if (!$result->isOk()) {
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...
738
            throw new TelegramException(
739
                'Webhook was not set! Error: ' . $result->getErrorCode() . ' ' . $result->getDescription()
740
            );
741
        }
742
743
        return $result;
744
    }
745
746
    /**
747
     * Unset Webhook for bot
748
     *
749
     * @return mixed
750
     * @throws \Longman\TelegramBot\Exception\TelegramException
751
     */
752
    public function unsetWebHook()
753
    {
754
        $result = Request::setWebhook();
755
756 View Code Duplication
        if (!$result->isOk()) {
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...
757
            throw new TelegramException(
758
                'Webhook was not unset! Error: ' . $result->getErrorCode() . ' ' . $result->getDescription()
759
            );
760
        }
761
762
        return $result;
763
    }
764
765
    /**
766
     * Replace function `ucwords` for UTF-8 characters in the class definition and commands
767
     *
768
     * @param string $str
769
     * @param string $encoding (default = 'UTF-8')
770
     *
771
     * @return string
772
     */
773 10
    protected function ucwordsUnicode($str, $encoding = 'UTF-8')
774
    {
775 10
        return mb_convert_case($str, MB_CASE_TITLE, $encoding);
776
    }
777
778
    /**
779
     * Replace function `ucfirst` for UTF-8 characters in the class definition and commands
780
     *
781
     * @param string $str
782
     * @param string $encoding (default = 'UTF-8')
783
     *
784
     * @return string
785
     */
786 11
    protected function ucfirstUnicode($str, $encoding = 'UTF-8')
787
    {
788
        return
789 11
            mb_strtoupper(mb_substr($str, 0, 1, $encoding), $encoding)
790 11
            . mb_strtolower(mb_substr($str, 1, mb_strlen($str), $encoding), $encoding);
791
    }
792
793
    /**
794
     * Enable Botan.io integration
795
     *
796
     * @param  $token
797
     *
798
     * @return \Longman\TelegramBot\Telegram
799
     * @throws \Longman\TelegramBot\Exception\TelegramException
800
     */
801
    public function enableBotan($token)
802
    {
803
        Botan::initializeBotan($token);
804
        $this->botan_enabled = true;
805
806
        return $this;
807
    }
808
}
809