Telegram::getCommandObject()   B
last analyzed

Complexity

Conditions 11
Paths 13

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 16.3179

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 11
eloc 16
c 2
b 0
f 0
nc 13
nop 2
dl 0
loc 29
ccs 11
cts 17
cp 0.6471
crap 16.3179
rs 7.3166

How to fix   Complexity   

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
/**
4
 * This file is part of the TelegramBot package.
5
 *
6
 * (c) Avtandil Kikabidze aka LONGMAN <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Longman\TelegramBot;
13
14 1
defined('TB_BASE_PATH') || define('TB_BASE_PATH', __DIR__);
15 1
defined('TB_BASE_COMMANDS_PATH') || define('TB_BASE_COMMANDS_PATH', TB_BASE_PATH . '/Commands');
16
17
use Exception;
18
use InvalidArgumentException;
19
use Longman\TelegramBot\Commands\AdminCommand;
20
use Longman\TelegramBot\Commands\Command;
21
use Longman\TelegramBot\Commands\SystemCommand;
22
use Longman\TelegramBot\Commands\UserCommand;
23
use Longman\TelegramBot\Entities\Chat;
24
use Longman\TelegramBot\Entities\ServerResponse;
25
use Longman\TelegramBot\Entities\Update;
26
use Longman\TelegramBot\Entities\User;
27
use Longman\TelegramBot\Exception\TelegramException;
28
use PDO;
29
use RecursiveDirectoryIterator;
30
use RecursiveIteratorIterator;
31
use RegexIterator;
32
33
class Telegram
34
{
35
    /**
36
     * Version
37
     *
38
     * @var string
39
     */
40
    protected $version = '0.83.0';
41
42
    /**
43
     * Telegram API key
44
     *
45
     * @var string
46
     */
47
    protected $api_key = '';
48
49
    /**
50
     * Telegram Bot username
51
     *
52
     * @var string
53
     */
54
    protected $bot_username = '';
55
56
    /**
57
     * Telegram Bot id
58
     *
59
     * @var int
60
     */
61
    protected $bot_id = 0;
62
63
    /**
64
     * Raw request data (json) for webhook methods
65
     *
66
     * @var string
67
     */
68
    protected $input = '';
69
70
    /**
71
     * Custom commands paths
72
     *
73
     * @var array
74
     */
75
    protected $commands_paths = [];
76
77
    /**
78
     * Custom command class names
79
     * ```
80
     * [
81
     *     'User' => [
82
     *         // command_name => command_class
83
     *         'start' => 'name\space\to\StartCommand',
84
     *     ],
85
     *     'Admin' => [], //etc
86
     * ]
87
     * ```
88
     *
89
     * @var array
90
     */
91
    protected $command_classes = [
92
        Command::AUTH_USER   => [],
93
        Command::AUTH_ADMIN  => [],
94
        Command::AUTH_SYSTEM => [],
95
    ];
96
97
    /**
98
     * Custom commands objects
99
     *
100
     * @var array
101
     */
102
    protected $commands_objects = [];
103
104
    /**
105
     * Current Update object
106
     *
107
     * @var Update
108
     */
109
    protected $update;
110
111
    /**
112
     * Upload path
113
     *
114
     * @var string
115
     */
116
    protected $upload_path = '';
117
118
    /**
119
     * Download path
120
     *
121
     * @var string
122
     */
123
    protected $download_path = '';
124
125
    /**
126
     * MySQL integration
127
     *
128
     * @var bool
129
     */
130
    protected $mysql_enabled = false;
131
132
    /**
133
     * PDO object
134
     *
135
     * @var PDO
136
     */
137
    protected $pdo;
138
139
    /**
140
     * Commands config
141
     *
142
     * @var array
143
     */
144
    protected $commands_config = [];
145
146
    /**
147
     * Admins list
148
     *
149
     * @var array
150
     */
151
    protected $admins_list = [];
152
153
    /**
154
     * ServerResponse of the last Command execution
155
     *
156
     * @var ServerResponse
157
     */
158
    protected $last_command_response;
159
160
    /**
161
     * Check if runCommands() is running in this session
162
     *
163
     * @var bool
164
     */
165
    protected $run_commands = false;
166
167
    /**
168
     * Is running getUpdates without DB enabled
169
     *
170
     * @var bool
171
     */
172
    protected $getupdates_without_database = false;
173
174
    /**
175
     * Last update ID
176
     * Only used when running getUpdates without a database
177
     *
178
     * @var int
179
     */
180
    protected $last_update_id;
181
182
    /**
183
     * The command to be executed when there's a new message update and nothing more suitable is found
184
     */
185
    public const GENERIC_MESSAGE_COMMAND = 'genericmessage';
186
187
    /**
188
     * The command to be executed by default (when no other relevant commands are applicable)
189
     */
190
    public const GENERIC_COMMAND = 'generic';
191
192
    /**
193
     * Update filter method
194
     *
195
     * @var callable
196
     */
197
    protected $update_filter;
198
199
    /**
200
     * Telegram constructor.
201
     *
202
     * @param string $api_key
203
     * @param string $bot_username
204
     *
205
     * @throws TelegramException
206
     */
207 33
    public function __construct(string $api_key, string $bot_username = '')
208
    {
209 33
        if (empty($api_key)) {
210 1
            throw new TelegramException('API KEY not defined!');
211
        }
212 33
        preg_match('/(\d+):[\w\-]+/', $api_key, $matches);
213 33
        if (!isset($matches[1])) {
214 1
            throw new TelegramException('Invalid API KEY defined!');
215
        }
216 33
        $this->bot_id  = (int) $matches[1];
217 33
        $this->api_key = $api_key;
218
219 33
        $this->bot_username = $bot_username;
220
221
        //Add default system commands path
222 33
        $this->addCommandsPath(TB_BASE_COMMANDS_PATH . '/SystemCommands');
223
224 33
        Request::initialize($this);
225
    }
226
227
    /**
228
     * Initialize Database connection
229
     *
230
     * @param array  $credentials
231
     * @param string $table_prefix
232
     * @param string $encoding
233
     *
234
     * @return Telegram
235
     * @throws TelegramException
236
     */
237 9
    public function enableMySql(array $credentials, string $table_prefix = '', string $encoding = 'utf8mb4'): Telegram
238
    {
239 9
        $this->pdo = DB::initialize($credentials, $this, $table_prefix, $encoding);
240 9
        ConversationDB::initializeConversation();
241 9
        $this->mysql_enabled = true;
242
243 9
        return $this;
244
    }
245
246
    /**
247
     * Initialize Database external connection
248
     *
249
     * @param PDO    $external_pdo_connection PDO database object
250
     * @param string $table_prefix
251
     *
252
     * @return Telegram
253
     * @throws TelegramException
254
     */
255
    public function enableExternalMySql(PDO $external_pdo_connection, string $table_prefix = ''): Telegram
256
    {
257
        $this->pdo = DB::externalInitialize($external_pdo_connection, $this, $table_prefix);
258
        ConversationDB::initializeConversation();
259
        $this->mysql_enabled = true;
260
261
        return $this;
262
    }
263
264
    /**
265
     * Get commands list
266
     *
267
     * @return array $commands
268
     * @throws TelegramException
269
     */
270 1
    public function getCommandsList(): array
271
    {
272 1
        $commands = [];
273
274 1
        foreach ($this->commands_paths as $path) {
275
            try {
276
                //Get all "*Command.php" files
277 1
                $files = new RegexIterator(
278 1
                    new RecursiveIteratorIterator(
279 1
                        new RecursiveDirectoryIterator($path)
280 1
                    ),
281 1
                    '/^.+Command.php$/'
282 1
                );
283
284 1
                foreach ($files as $file) {
285
                    // Convert filename to command
286 1
                    $command = $this->classNameToCommandName(substr($file->getFilename(), 0, -4));
287
288
                    // Invalid Classname
289 1
                    if (is_null($command)) {
290
                        continue;
291
                    }
292
293
                    // Already registered
294 1
                    if (array_key_exists($command, $commands)) {
295
                        continue;
296
                    }
297
298 1
                    require_once $file->getPathname();
299
300 1
                    $command_obj = $this->getCommandObject($command, $file->getPathname());
301 1
                    if ($command_obj instanceof Command) {
302 1
                        $commands[$command] = $command_obj;
303
                    }
304
                }
305
            } catch (Exception $e) {
306
                throw new TelegramException('Error getting commands from path: ' . $path, 0, $e);
307
            }
308
        }
309
310 1
        return $commands;
311
    }
312
313
    /**
314
     * Get classname of predefined commands
315
     *
316
     * @see command_classes
317
     *
318
     * @param string $auth     Auth of command
319
     * @param string $command  Command name
320
     * @param string $filepath Path to the command file
321
     *
322
     * @return string|null
323
     */
324 2
    public function getCommandClassName(string $auth, string $command, string $filepath = ''): ?string
325
    {
326 2
        $command = mb_strtolower($command);
327
328
        // Invalid command
329 2
        if (trim($command) === '') {
330
            return null;
331
        }
332
333 2
        $auth    = $this->ucFirstUnicode($auth);
334
335
        // First, check for directly assigned command class.
336 2
        if ($command_class = $this->command_classes[$auth][$command] ?? null) {
337 1
            return $command_class;
338
        }
339
340
        // Start with default namespace.
341 2
        $command_namespace = __NAMESPACE__ . '\\Commands\\' . $auth . 'Commands';
342
343
        // Check if we can get the namespace from the file (if passed).
344 2
        if ($filepath && !($command_namespace = $this->getFileNamespace($filepath))) {
345
            return null;
346
        }
347
348 2
        $command_class = $command_namespace . '\\' . $this->commandNameToClassName($command);
349
350 2
        if (class_exists($command_class)) {
351 1
            return $command_class;
352
        }
353
354 1
        return null;
355
    }
356
357
    /**
358
     * Get an object instance of the passed command
359
     *
360
     * @param string $command
361
     * @param string $filepath
362
     *
363
     * @return Command|null
364
     */
365 1
    public function getCommandObject(string $command, string $filepath = ''): ?Command
366
    {
367 1
        if (isset($this->commands_objects[$command])) {
368
            return $this->commands_objects[$command];
369
        }
370
371 1
        $which = [Command::AUTH_SYSTEM];
372 1
        $this->isAdmin() && $which[] = Command::AUTH_ADMIN;
373 1
        $which[] = Command::AUTH_USER;
374
375 1
        foreach ($which as $auth) {
376 1
            $command_class = $this->getCommandClassName($auth, $command, $filepath);
377
378 1
            if ($command_class) {
379 1
                $command_obj = new $command_class($this, $this->update);
380
381 1
                if ($auth === Command::AUTH_SYSTEM && $command_obj instanceof SystemCommand) {
382 1
                    return $command_obj;
383
                }
384
                if ($auth === Command::AUTH_ADMIN && $command_obj instanceof AdminCommand) {
385
                    return $command_obj;
386
                }
387
                if ($auth === Command::AUTH_USER && $command_obj instanceof UserCommand) {
388
                    return $command_obj;
389
                }
390
            }
391
        }
392
393
        return null;
394
    }
395
396
    /**
397
     * Get namespace from php file by src path
398
     *
399
     * @param string $src (absolute path to file)
400
     *
401
     * @return string|null ("Longman\TelegramBot\Commands\SystemCommands" for example)
402
     */
403 1
    protected function getFileNamespace(string $src): ?string
404
    {
405 1
        $content = file_get_contents($src);
406 1
        if (preg_match('#^\s*namespace\s+(.+?);#m', $content, $m)) {
407 1
            return $m[1];
408
        }
409
410
        return null;
411
    }
412
413
    /**
414
     * Set custom input string for debug purposes
415
     *
416
     * @param string $input (json format)
417
     *
418
     * @return Telegram
419
     */
420
    public function setCustomInput(string $input): Telegram
421
    {
422
        $this->input = $input;
423
424
        return $this;
425
    }
426
427
    /**
428
     * Get custom input string for debug purposes
429
     *
430
     * @return string
431
     */
432
    public function getCustomInput(): string
433
    {
434
        return $this->input;
435
    }
436
437
    /**
438
     * Get the ServerResponse of the last Command execution
439
     *
440
     * @return ServerResponse
441
     */
442
    public function getLastCommandResponse(): ServerResponse
443
    {
444
        return $this->last_command_response;
445
    }
446
447
    /**
448
     * Handle getUpdates method
449
     *
450
     * @todo Remove backwards compatibility for old signature and force $data to be an array.
451
     *
452
     * @param array|int|null $data
453
     * @param int|null       $timeout
454
     *
455
     * @return ServerResponse
456
     * @throws TelegramException
457
     */
458
    public function handleGetUpdates($data = null, ?int $timeout = null): ServerResponse
459
    {
460
        if (empty($this->bot_username)) {
461
            throw new TelegramException('Bot Username is not defined!');
462
        }
463
464
        if (!DB::isDbConnected() && !$this->getupdates_without_database) {
465
            return new ServerResponse(
466
                [
467
                    'ok'          => false,
468
                    'description' => 'getUpdates needs MySQL connection! (This can be overridden - see documentation)',
469
                ],
470
                $this->bot_username
471
            );
472
        }
473
474
        $offset = 0;
475
        $limit  = null;
476
477
        // By default, get update types sent by Telegram.
478
        $allowed_updates = [];
479
480
        // @todo Backwards compatibility for old signature, remove in next version.
481
        if (!is_array($data)) {
482
            $limit = $data;
483
484
            @trigger_error(
485
                sprintf('Use of $limit and $timeout parameters in %s is deprecated. Use $data array instead.', __METHOD__),
486
                E_USER_DEPRECATED
487
            );
488
        } else {
489
            $offset          = $data['offset'] ?? $offset;
490
            $limit           = $data['limit'] ?? $limit;
491
            $timeout         = $data['timeout'] ?? $timeout;
492
            $allowed_updates = $data['allowed_updates'] ?? $allowed_updates;
493
        }
494
495
        // Take custom input into account.
496
        if ($custom_input = $this->getCustomInput()) {
497
            try {
498
                $input = json_decode($this->input, true, 512, JSON_THROW_ON_ERROR);
499
                if (empty($input)) {
500
                    throw new TelegramException('Custom input is empty');
501
                }
502
                $response = new ServerResponse($input, $this->bot_username);
503
            } catch (\Throwable $e) {
504
                throw new TelegramException('Invalid custom input JSON: ' . $e->getMessage());
505
            }
506
        } else {
507
            if (DB::isDbConnected() && $last_update = DB::selectTelegramUpdate(1)) {
508
                // Get last Update id from the database.
509
                $last_update          = reset($last_update);
510
                $this->last_update_id = $last_update['id'] ?? null;
511
            }
512
513
            if ($this->last_update_id !== null) {
514
                $offset = $this->last_update_id + 1; // As explained in the telegram bot API documentation.
515
            }
516
517
            $response = Request::getUpdates(compact('offset', 'limit', 'timeout', 'allowed_updates'));
518
        }
519
520
        if ($response->isOk()) {
521
            // Log update.
522
            TelegramLog::update($response->toJson());
523
524
            // Process all updates
525
            /** @var Update $update */
526
            foreach ($response->getResult() as $update) {
527
                $this->processUpdate($update);
528
            }
529
530
            if (!DB::isDbConnected() && !$custom_input && $this->last_update_id !== null && $offset === 0) {
531
                // Mark update(s) as read after handling
532
                $offset = $this->last_update_id + 1;
533
                $limit  = 1;
534
535
                Request::getUpdates(compact('offset', 'limit', 'timeout', 'allowed_updates'));
536
            }
537
        }
538
539
        return $response;
540
    }
541
542
    /**
543
     * Handle bot request from webhook
544
     *
545
     * @return bool
546
     *
547
     * @throws TelegramException
548
     */
549
    public function handle(): bool
550
    {
551
        if ($this->bot_username === '') {
552
            throw new TelegramException('Bot Username is not defined!');
553
        }
554
555
        $input = Request::getInput();
556
        if (empty($input)) {
557
            throw new TelegramException('Input is empty! The webhook must not be called manually, only by Telegram.');
558
        }
559
560
        // Log update.
561
        TelegramLog::update($input);
562
563
        $post = json_decode($input, true);
564
        if (empty($post)) {
565
            throw new TelegramException('Invalid input JSON! The webhook must not be called manually, only by Telegram.');
566
        }
567
568
        if ($response = $this->processUpdate(new Update($post, $this->bot_username))) {
569
            return $response->isOk();
570
        }
571
572
        return false;
573
    }
574
575
    /**
576
     * Get the command name from the command type
577
     *
578
     * @param string $type
579
     *
580
     * @return string
581
     */
582
    protected function getCommandFromType(string $type): string
583
    {
584
        return $this->ucFirstUnicode(str_replace('_', '', $type));
585
    }
586
587
    /**
588
     * Process bot Update request
589
     *
590
     * @param Update $update
591
     *
592
     * @return ServerResponse
593
     * @throws TelegramException
594
     */
595 1
    public function processUpdate(Update $update): ServerResponse
596
    {
597 1
        $this->update         = $update;
598 1
        $this->last_update_id = $update->getUpdateId();
599
600 1
        if (is_callable($this->update_filter)) {
601 1
            $reason = 'Update denied by update_filter';
602
            try {
603 1
                $allowed = (bool) call_user_func_array($this->update_filter, [$update, $this, &$reason]);
604
            } catch (Exception $e) {
605
                $allowed = false;
606
            }
607
608 1
            if (!$allowed) {
609 1
                TelegramLog::debug($reason);
610 1
                return new ServerResponse(['ok' => false, 'description' => 'denied']);
611
            }
612
        }
613
614
        //Load admin commands
615
        if ($this->isAdmin()) {
616
            $this->addCommandsPath(TB_BASE_COMMANDS_PATH . '/AdminCommands', false);
617
        }
618
619
        //Make sure we have an up-to-date command list
620
        //This is necessary to "require" all the necessary command files!
621
        $this->commands_objects = $this->getCommandsList();
622
623
        //If all else fails, it's a generic message.
624
        $command = self::GENERIC_MESSAGE_COMMAND;
625
626
        $update_type = $this->update->getUpdateType();
627
        if ($update_type === 'message') {
628
            $message = $this->update->getMessage();
629
            $type    = $message->getType();
630
631
            // Let's check if the message object has the type field we're looking for...
632
            $command_tmp = $type === 'command' ? $message->getCommand() : $this->getCommandFromType($type);
633
            // ...and if a fitting command class is available.
634
            $command_obj = $command_tmp ? $this->getCommandObject($command_tmp) : null;
635
636
            // Empty usage string denotes a non-executable command.
637
            // @see https://github.com/php-telegram-bot/core/issues/772#issuecomment-388616072
638
            if (
639
                ($command_obj === null && $type === 'command')
640
                || ($command_obj !== null && $command_obj->getUsage() !== '')
641
            ) {
642
                $command = $command_tmp;
643
            }
644
        } elseif ($update_type !== null) {
645
            $command = $this->getCommandFromType($update_type);
646
        }
647
648
        //Make sure we don't try to process update that was already processed
649
        $last_id = DB::selectTelegramUpdate(1, $this->update->getUpdateId());
650
        if ($last_id && count($last_id) === 1) {
651
            TelegramLog::debug('Duplicate update received, processing aborted!');
652
            return Request::emptyResponse();
653
        }
654
655
        DB::insertRequest($this->update);
656
657
        return $this->executeCommand($command);
658
    }
659
660
    /**
661
     * Execute /command
662
     *
663
     * @param string $command
664
     *
665
     * @return ServerResponse
666
     * @throws TelegramException
667
     */
668
    public function executeCommand(string $command): ServerResponse
669
    {
670
        $command = mb_strtolower($command);
671
672
        $command_obj = $this->commands_objects[$command] ?? $this->getCommandObject($command);
673
674
        if (!$command_obj || !$command_obj->isEnabled()) {
675
            //Failsafe in case the Generic command can't be found
676
            if ($command === self::GENERIC_COMMAND) {
677
                throw new TelegramException('Generic command missing!');
678
            }
679
680
            //Handle a generic command or non existing one
681
            $this->last_command_response = $this->executeCommand(self::GENERIC_COMMAND);
682
        } else {
683
            //execute() method is executed after preExecute()
684
            //This is to prevent executing a DB query without a valid connection
685
            if ($this->update) {
686
                $this->last_command_response = $command_obj->setUpdate($this->update)->preExecute();
687
            } else {
688
                $this->last_command_response = $command_obj->preExecute();
689
            }
690
        }
691
692
        return $this->last_command_response;
693
    }
694
695
    /**
696
     * @deprecated
697
     *
698
     * @param string $command
699
     *
700
     * @return string
701
     */
702
    protected function sanitizeCommand(string $command): string
703
    {
704
        return str_replace(' ', '', $this->ucWordsUnicode(str_replace('_', ' ', $command)));
705
    }
706
707
    /**
708
     * Enable a single Admin account
709
     *
710
     * @param int $admin_id Single admin id
711
     *
712
     * @return Telegram
713
     */
714 1
    public function enableAdmin(int $admin_id): Telegram
715
    {
716 1
        if ($admin_id <= 0) {
717
            TelegramLog::error('Invalid value "' . $admin_id . '" for admin.');
718 1
        } elseif (!in_array($admin_id, $this->admins_list, true)) {
719 1
            $this->admins_list[] = $admin_id;
720
        }
721
722 1
        return $this;
723
    }
724
725
    /**
726
     * Enable a list of Admin Accounts
727
     *
728
     * @param array $admin_ids List of admin ids
729
     *
730
     * @return Telegram
731
     */
732 1
    public function enableAdmins(array $admin_ids): Telegram
733
    {
734 1
        foreach ($admin_ids as $admin_id) {
735 1
            $this->enableAdmin($admin_id);
736
        }
737
738 1
        return $this;
739
    }
740
741
    /**
742
     * Get list of admins
743
     *
744
     * @return array
745
     */
746 1
    public function getAdminList(): array
747
    {
748 1
        return $this->admins_list;
749
    }
750
751
    /**
752
     * Check if the passed user is an admin
753
     *
754
     * If no user id is passed, the current update is checked for a valid message sender.
755
     *
756
     * @param int|null $user_id
757
     *
758
     * @return bool
759
     */
760 1
    public function isAdmin($user_id = null): bool
761
    {
762 1
        if ($user_id === null && $this->update !== null) {
763
            //Try to figure out if the user is an admin
764
            $update_methods = [
765
                'getMessage',
766
                'getEditedMessage',
767
                'getChannelPost',
768
                'getEditedChannelPost',
769
                'getInlineQuery',
770
                'getChosenInlineResult',
771
                'getCallbackQuery',
772
            ];
773
            foreach ($update_methods as $update_method) {
774
                $object = call_user_func([$this->update, $update_method]);
775
                if ($object !== null && $from = $object->getFrom()) {
776
                    $user_id = $from->getId();
777
                    break;
778
                }
779
            }
780
        }
781
782 1
        return ($user_id === null) ? false : in_array($user_id, $this->admins_list, true);
783
    }
784
785
    /**
786
     * Check if user required the db connection
787
     *
788
     * @return bool
789
     */
790
    public function isDbEnabled(): bool
791
    {
792
        return $this->mysql_enabled;
793
    }
794
795
    /**
796
     * Add a single custom command class
797
     *
798
     * @param string $command_class Full command class name
799
     *
800
     * @return Telegram
801
     */
802 2
    public function addCommandClass(string $command_class): Telegram
803
    {
804 2
        if (!$command_class || !class_exists($command_class)) {
805 1
            $error = sprintf('Command class "%s" does not exist.', $command_class);
806 1
            TelegramLog::error($error);
807 1
            throw new InvalidArgumentException($error);
808
        }
809
810 2
        if (!is_a($command_class, Command::class, true)) {
811
            $error = sprintf('Command class "%s" does not extend "%s".', $command_class, Command::class);
812
            TelegramLog::error($error);
813
            throw new InvalidArgumentException($error);
814
        }
815
816
        // Dummy object to get data from.
817 2
        $command_object = new $command_class($this);
818
819 2
        $auth = null;
820 2
        $command_object->isSystemCommand() && $auth = Command::AUTH_SYSTEM;
821 2
        $command_object->isAdminCommand() && $auth = Command::AUTH_ADMIN;
822 2
        $command_object->isUserCommand() && $auth = Command::AUTH_USER;
823
824 2
        if ($auth) {
825 2
            $command = mb_strtolower($command_object->getName());
826
827 2
            $this->command_classes[$auth][$command] = $command_class;
828
        }
829
830 2
        return $this;
831
    }
832
833
    /**
834
     * Add multiple custom command classes
835
     *
836
     * @param array $command_classes List of full command class names
837
     *
838
     * @return Telegram
839
     */
840 1
    public function addCommandClasses(array $command_classes): Telegram
841
    {
842 1
        foreach ($command_classes as $command_class) {
843 1
            $this->addCommandClass($command_class);
844
        }
845
846 1
        return $this;
847
    }
848
849
    /**
850
     * Set a single custom commands path
851
     *
852
     * @param string $path Custom commands path to set
853
     *
854
     * @return Telegram
855
     */
856
    public function setCommandsPath(string $path): Telegram
857
    {
858
        $this->commands_paths = [];
859
860
        $this->addCommandsPath($path);
861
862
        return $this;
863
    }
864
865
    /**
866
     * Add a single custom commands path
867
     *
868
     * @param string $path   Custom commands path to add
869
     * @param bool   $before If the path should be prepended or appended to the list
870
     *
871
     * @return Telegram
872
     */
873 33
    public function addCommandsPath(string $path, bool $before = true): Telegram
874
    {
875 33
        if (!is_dir($path)) {
876 1
            TelegramLog::error('Commands path "' . $path . '" does not exist.');
877 33
        } elseif (!in_array($path, $this->commands_paths, true)) {
878 33
            if ($before) {
879 33
                array_unshift($this->commands_paths, $path);
880
            } else {
881
                $this->commands_paths[] = $path;
882
            }
883
        }
884
885 33
        return $this;
886
    }
887
888
    /**
889
     * Set multiple custom commands paths
890
     *
891
     * @param array $paths Custom commands paths to add
892
     *
893
     * @return Telegram
894
     */
895
    public function setCommandsPaths(array $paths): Telegram
896
    {
897
        $this->commands_paths = [];
898
899
        $this->addCommandsPaths($paths);
900
901
        return $this;
902
    }
903
904
    /**
905
     * Add multiple custom commands paths
906
     *
907
     * @param array $paths  Custom commands paths to add
908
     * @param bool  $before If the paths should be prepended or appended to the list
909
     *
910
     * @return Telegram
911
     */
912 1
    public function addCommandsPaths(array $paths, bool $before = true): Telegram
913
    {
914 1
        foreach ($paths as $path) {
915 1
            $this->addCommandsPath($path, $before);
916
        }
917
918 1
        return $this;
919
    }
920
921
    /**
922
     * Return the list of commands paths
923
     *
924
     * @return array
925
     */
926 1
    public function getCommandsPaths(): array
927
    {
928 1
        return $this->commands_paths;
929
    }
930
931
    /**
932
     * Return the list of command classes
933
     *
934
     * @return array
935
     */
936 2
    public function getCommandClasses(): array
937
    {
938 2
        return $this->command_classes;
939
    }
940
941
    /**
942
     * Set custom upload path
943
     *
944
     * @param string $path Custom upload path
945
     *
946
     * @return Telegram
947
     */
948 1
    public function setUploadPath(string $path): Telegram
949
    {
950 1
        $this->upload_path = $path;
951
952 1
        return $this;
953
    }
954
955
    /**
956
     * Get custom upload path
957
     *
958
     * @return string
959
     */
960 1
    public function getUploadPath(): string
961
    {
962 1
        return $this->upload_path;
963
    }
964
965
    /**
966
     * Set custom download path
967
     *
968
     * @param string $path Custom download path
969
     *
970
     * @return Telegram
971
     */
972 1
    public function setDownloadPath(string $path): Telegram
973
    {
974 1
        $this->download_path = $path;
975
976 1
        return $this;
977
    }
978
979
    /**
980
     * Get custom download path
981
     *
982
     * @return string
983
     */
984 1
    public function getDownloadPath(): string
985
    {
986 1
        return $this->download_path;
987
    }
988
989
    /**
990
     * Set command config
991
     *
992
     * Provide further variables to a particular commands.
993
     * For example you can add the channel name at the command /sendtochannel
994
     * Or you can add the api key for external service.
995
     *
996
     * @param string $command
997
     * @param array  $config
998
     *
999
     * @return Telegram
1000
     */
1001 13
    public function setCommandConfig(string $command, array $config): Telegram
1002
    {
1003 13
        $this->commands_config[$command] = $config;
1004
1005 13
        return $this;
1006
    }
1007
1008
    /**
1009
     * Get command config
1010
     *
1011
     * @param string $command
1012
     *
1013
     * @return array
1014
     */
1015 16
    public function getCommandConfig(string $command): array
1016
    {
1017 16
        return $this->commands_config[$command] ?? [];
1018
    }
1019
1020
    /**
1021
     * Get API key
1022
     *
1023
     * @return string
1024
     */
1025 1
    public function getApiKey(): string
1026
    {
1027 1
        return $this->api_key;
1028
    }
1029
1030
    /**
1031
     * Get Bot name
1032
     *
1033
     * @return string
1034
     */
1035 2
    public function getBotUsername(): string
1036
    {
1037 2
        return $this->bot_username;
1038
    }
1039
1040
    /**
1041
     * Get Bot Id
1042
     *
1043
     * @return int
1044
     */
1045
    public function getBotId(): int
1046
    {
1047
        return $this->bot_id;
1048
    }
1049
1050
    /**
1051
     * Get Version
1052
     *
1053
     * @return string
1054
     */
1055
    public function getVersion(): string
1056
    {
1057
        return $this->version;
1058
    }
1059
1060
    /**
1061
     * Set Webhook for bot
1062
     *
1063
     * @param string $url
1064
     * @param array  $data Optional parameters.
1065
     *
1066
     * @return ServerResponse
1067
     * @throws TelegramException
1068
     */
1069
    public function setWebhook(string $url, array $data = []): ServerResponse
1070
    {
1071
        if ($url === '') {
1072
            throw new TelegramException('Hook url is empty!');
1073
        }
1074
1075
        $data        = array_intersect_key($data, array_flip([
1076
            'certificate',
1077
            'ip_address',
1078
            'max_connections',
1079
            'allowed_updates',
1080
            'drop_pending_updates',
1081
            'secret_token',
1082
        ]));
1083
        $data['url'] = $url;
1084
1085
        // If the certificate is passed as a path, encode and add the file to the data array.
1086
        if (!empty($data['certificate']) && is_string($data['certificate'])) {
1087
            $data['certificate'] = Request::encodeFile($data['certificate']);
1088
        }
1089
1090
        $result = Request::setWebhook($data);
1091
1092
        if (!$result->isOk()) {
1093
            throw new TelegramException(
1094
                'Webhook was not set! Error: ' . $result->getErrorCode() . ' ' . $result->getDescription()
1095
            );
1096
        }
1097
1098
        return $result;
1099
    }
1100
1101
    /**
1102
     * Delete any assigned webhook
1103
     *
1104
     * @param array $data
1105
     *
1106
     * @return ServerResponse
1107
     * @throws TelegramException
1108
     */
1109
    public function deleteWebhook(array $data = []): ServerResponse
1110
    {
1111
        $result = Request::deleteWebhook($data);
1112
1113
        if (!$result->isOk()) {
1114
            throw new TelegramException(
1115
                'Webhook was not deleted! Error: ' . $result->getErrorCode() . ' ' . $result->getDescription()
1116
            );
1117
        }
1118
1119
        return $result;
1120
    }
1121
1122
    /**
1123
     * Replace function `ucwords` for UTF-8 characters in the class definition and commands
1124
     *
1125
     * @param string $str
1126
     * @param string $encoding (default = 'UTF-8')
1127
     *
1128
     * @return string
1129
     */
1130 2
    protected function ucWordsUnicode(string $str, string $encoding = 'UTF-8'): string
1131
    {
1132 2
        return mb_convert_case($str, MB_CASE_TITLE, $encoding);
1133
    }
1134
1135
    /**
1136
     * Replace function `ucfirst` for UTF-8 characters in the class definition and commands
1137
     *
1138
     * @param string $str
1139
     * @param string $encoding (default = 'UTF-8')
1140
     *
1141
     * @return string
1142
     */
1143 2
    protected function ucFirstUnicode(string $str, string $encoding = 'UTF-8'): string
1144
    {
1145 2
        return mb_strtoupper(mb_substr($str, 0, 1, $encoding), $encoding)
1146 2
            . mb_strtolower(mb_substr($str, 1, mb_strlen($str), $encoding), $encoding);
1147
    }
1148
1149
    /**
1150
     * Enable requests limiter
1151
     *
1152
     * @param array $options
1153
     *
1154
     * @return Telegram
1155
     * @throws TelegramException
1156
     */
1157
    public function enableLimiter(array $options = []): Telegram
1158
    {
1159
        Request::setLimiter(true, $options);
1160
1161
        return $this;
1162
    }
1163
1164
    /**
1165
     * Run provided commands
1166
     *
1167
     * @param array $commands
1168
     *
1169
     * @return ServerResponse[]
1170
     *
1171
     * @throws TelegramException
1172
     */
1173
    public function runCommands(array $commands): array
1174
    {
1175
        if (empty($commands)) {
1176
            throw new TelegramException('No command(s) provided!');
1177
        }
1178
1179
        $this->run_commands = true;
1180
1181
        // Check if this request has a user Update / comes from Telegram.
1182
        if ($userUpdate = $this->update) {
1183
            $from = $this->update->getMessage()->getFrom();
1184
            $chat = $this->update->getMessage()->getChat();
1185
        } else {
1186
            // Fall back to the Bot user.
1187
            $from = new User([
1188
                'id'         => $this->getBotId(),
1189
                'first_name' => $this->getBotUsername(),
1190
                'username'   => $this->getBotUsername(),
1191
            ]);
1192
1193
            // Try to get "live" Bot info.
1194
            $response = Request::getMe();
1195
            if ($response->isOk()) {
1196
                /** @var User $result */
1197
                $result = $response->getResult();
1198
1199
                $from = new User([
1200
                    'id'         => $result->getId(),
1201
                    'first_name' => $result->getFirstName(),
1202
                    'username'   => $result->getUsername(),
1203
                ]);
1204
            }
1205
1206
            // Give Bot access to admin commands.
1207
            $this->enableAdmin($from->getId());
1208
1209
            // Lock the bot to a private chat context.
1210
            $chat = new Chat([
1211
                'id'   => $from->getId(),
1212
                'type' => 'private',
1213
            ]);
1214
        }
1215
1216
        $newUpdate = static function ($text = '') use ($from, $chat) {
1217
            return new Update([
1218
                'update_id' => -1,
1219
                'message'   => [
1220
                    'message_id' => -1,
1221
                    'date'       => time(),
1222
                    'from'       => json_decode($from->toJson(), true),
1223
                    'chat'       => json_decode($chat->toJson(), true),
1224
                    'text'       => $text,
1225
                ],
1226
            ]);
1227
        };
1228
1229
        $responses = [];
1230
1231
        foreach ($commands as $command) {
1232
            $this->update = $newUpdate($command);
1233
1234
            // Refresh commands list for new Update object.
1235
            $this->commands_objects = $this->getCommandsList();
1236
1237
            $responses[] = $this->executeCommand($this->update->getMessage()->getCommand());
1238
        }
1239
1240
        // Reset Update to initial context.
1241
        $this->update = $userUpdate;
1242
1243
        return $responses;
1244
    }
1245
1246
    /**
1247
     * Is this session initiated by runCommands()
1248
     *
1249
     * @return bool
1250
     */
1251
    public function isRunCommands(): bool
1252
    {
1253
        return $this->run_commands;
1254
    }
1255
1256
    /**
1257
     * Switch to enable running getUpdates without a database
1258
     *
1259
     * @param bool $enable
1260
     *
1261
     * @return Telegram
1262
     */
1263
    public function useGetUpdatesWithoutDatabase(bool $enable = true): Telegram
1264
    {
1265
        $this->getupdates_without_database = $enable;
1266
1267
        return $this;
1268
    }
1269
1270
    /**
1271
     * Return last update id
1272
     *
1273
     * @return int|null
1274
     */
1275
    public function getLastUpdateId(): ?int
1276
    {
1277
        return $this->last_update_id;
1278
    }
1279
1280
    /**
1281
     * Set an update filter callback
1282
     *
1283
     * @param callable $callback
1284
     *
1285
     * @return Telegram
1286
     */
1287 1
    public function setUpdateFilter(callable $callback): Telegram
1288
    {
1289 1
        $this->update_filter = $callback;
1290
1291 1
        return $this;
1292
    }
1293
1294
    /**
1295
     * Return update filter callback
1296
     *
1297
     * @return callable|null
1298
     */
1299
    public function getUpdateFilter(): ?callable
1300
    {
1301
        return $this->update_filter;
1302
    }
1303
1304
    /**
1305
     * Converts the name of a class into the name of a command.
1306
     *
1307
     * @param string $class For example FooBarCommand
1308
     *
1309
     * @return string|null for example foo_bar. In case of errors, returns null.
1310
     */
1311 1
    protected function classNameToCommandName(string $class): ?string
1312
    {
1313
        // If $class doesn't end with 'Command'
1314 1
        if (substr($class, -7) !== 'Command') {
1315
            return null;
1316
        }
1317
1318 1
        return mb_strtolower(preg_replace('/(.)(?=[\p{Lu}])/u', '$1_', substr($class, 0, -7)));
1319
    }
1320
1321
    /**
1322
     * Converts a command name into the name of a class.
1323
     *
1324
     * @param string $command For example foo_bar
1325
     *
1326
     * @return string|null for example FooBarCommand. In case of errors, returns null.
1327
     */
1328 2
    protected function commandNameToClassName(string $command): ?string
1329
    {
1330 2
        if (trim($command) === '') {
1331
            return null;
1332
        }
1333
1334 2
        return str_replace(' ', '', $this->ucWordsUnicode(str_replace('_', ' ', $command))) . 'Command';
1335
    }
1336
}
1337