Passed
Pull Request — develop (#1365)
by Michele
05:30
created

Telegram::getCommandObject()   B

Complexity

Conditions 11
Paths 13

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 14.6926

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 11
eloc 16
c 3
b 0
f 0
nc 13
nop 2
dl 0
loc 29
ccs 11
cts 16
cp 0.6875
crap 14.6926
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.79.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
                    ),
281
                    '/^.+Command.php$/'
282
                );
283
284 1
                foreach ($files as $file) {
285
                    //Remove "Command.php" from filename
286 1
                    $command      = $this->classNameToCommandName(substr($file->getFilename(), 0, -4));
287 1
288
                    if (array_key_exists($command, $commands)) {
289 1
                        continue;
290
                    }
291
292
                    require_once $file->getPathname();
293 1
294
                    $command_obj = $this->getCommandObject($command, $file->getPathname());
295 1
                    if ($command_obj instanceof Command) {
296 1
                        $commands[$command] = $command_obj;
297 1
                    }
298
                }
299
            } catch (Exception $e) {
300
                throw new TelegramException('Error getting commands from path: ' . $path, $e);
0 ignored issues
show
Bug introduced by
$e of type Exception is incompatible with the type integer expected by parameter $code of Longman\TelegramBot\Exce...xception::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

300
                throw new TelegramException('Error getting commands from path: ' . $path, /** @scrutinizer ignore-type */ $e);
Loading history...
301
            }
302
        }
303
304
        return $commands;
305 1
    }
306
307
    /**
308
     * Get classname of predefined commands
309
     *
310
     * @see command_classes
311
     *
312
     * @param string $auth     Auth of command
313
     * @param string $command  Command name
314
     * @param string $filepath Path to the command file
315
     *
316
     * @return string|null
317
     */
318
    public function getCommandClassName(string $auth, string $command, string $filepath = ''): ?string
319 2
    {
320
        $command = mb_strtolower($command);
321 2
        $auth    = $this->ucFirstUnicode($auth);
322 2
323
        // First, check for directly assigned command class.
324
        if ($command_class = $this->command_classes[$auth][$command] ?? null) {
325 2
            return $command_class;
326 1
        }
327
328
        // Start with default namespace.
329
        $command_namespace = __NAMESPACE__ . '\\Commands\\' . $auth . 'Commands';
330 2
331
        // Check if we can get the namespace from the file (if passed).
332
        if ($filepath && !($command_namespace = $this->getFileNamespace($filepath))) {
333 2
            return null;
334
        }
335
336
        $command_class = $command_namespace . '\\' . $this->commandNameToClassName($command);
337 2
338
        if (class_exists($command_class)) {
339 2
            return $command_class;
340 1
        }
341
342
        return null;
343 1
    }
344
345
    /**
346
     * Get an object instance of the passed command
347
     *
348
     * @param string $command
349
     * @param string $filepath
350
     *
351
     * @return Command|null
352
     */
353
    public function getCommandObject(string $command, string $filepath = ''): ?Command
354 1
    {
355
        if (isset($this->commands_objects[$command])) {
356 1
            return $this->commands_objects[$command];
357
        }
358
359
        $which = [Command::AUTH_SYSTEM];
360 1
        $this->isAdmin() && $which[] = Command::AUTH_ADMIN;
361 1
        $which[] = Command::AUTH_USER;
362 1
363
        foreach ($which as $auth) {
364 1
            $command_class = $this->getCommandClassName($auth, $command, $filepath);
365 1
366
            if ($command_class) {
367 1
                $command_obj = new $command_class($this, $this->update);
368 1
369
                if ($auth === Command::AUTH_SYSTEM && $command_obj instanceof SystemCommand) {
370 1
                    return $command_obj;
371 1
                }
372
                if ($auth === Command::AUTH_ADMIN && $command_obj instanceof AdminCommand) {
373
                    return $command_obj;
374
                }
375
                if ($auth === Command::AUTH_USER && $command_obj instanceof UserCommand) {
376
                    return $command_obj;
377
                }
378
            }
379
        }
380
381
        return null;
382
    }
383
384
    /**
385
     * Get namespace from php file by src path
386
     *
387
     * @param string $src (absolute path to file)
388
     *
389
     * @return string|null ("Longman\TelegramBot\Commands\SystemCommands" for example)
390
     */
391
    protected function getFileNamespace(string $src): ?string
392 1
    {
393
        $content = file_get_contents($src);
394 1
        if (preg_match('#^\s*namespace\s+(.+?);#m', $content, $m)) {
395 1
            return $m[1];
396 1
        }
397
398
        return null;
399
    }
400
401
    /**
402
     * Set custom input string for debug purposes
403
     *
404
     * @param string $input (json format)
405
     *
406
     * @return Telegram
407
     */
408
    public function setCustomInput(string $input): Telegram
409
    {
410
        $this->input = $input;
411
412
        return $this;
413
    }
414
415
    /**
416
     * Get custom input string for debug purposes
417
     *
418
     * @return string
419
     */
420
    public function getCustomInput(): string
421
    {
422
        return $this->input;
423
    }
424
425
    /**
426
     * Get the ServerResponse of the last Command execution
427
     *
428
     * @return ServerResponse
429
     */
430
    public function getLastCommandResponse(): ServerResponse
431
    {
432
        return $this->last_command_response;
433
    }
434
435
    /**
436
     * Handle getUpdates method
437
     *
438
     * @todo Remove backwards compatibility for old signature and force $data to be an array.
439
     *
440
     * @param array|int|null $data
441
     * @param int|null       $timeout
442
     *
443
     * @return ServerResponse
444
     * @throws TelegramException
445
     */
446
    public function handleGetUpdates($data = null, ?int $timeout = null): ServerResponse
447
    {
448
        if (empty($this->bot_username)) {
449
            throw new TelegramException('Bot Username is not defined!');
450
        }
451
452
        if (!DB::isDbConnected() && !$this->getupdates_without_database) {
453
            return new ServerResponse(
454
                [
455
                    'ok'          => false,
456
                    'description' => 'getUpdates needs MySQL connection! (This can be overridden - see documentation)',
457
                ],
458
                $this->bot_username
459
            );
460
        }
461
462
        $offset = 0;
463
        $limit  = null;
464
465
        // By default, get update types sent by Telegram.
466
        $allowed_updates = [];
467
468
        // @todo Backwards compatibility for old signature, remove in next version.
469
        if (!is_array($data)) {
470
            $limit = $data;
471
472
            @trigger_error(
473
                sprintf('Use of $limit and $timeout parameters in %s is deprecated. Use $data array instead.', __METHOD__),
474
                E_USER_DEPRECATED
475
            );
476
        } else {
477
            $offset          = $data['offset'] ?? $offset;
478
            $limit           = $data['limit'] ?? $limit;
479
            $timeout         = $data['timeout'] ?? $timeout;
480
            $allowed_updates = $data['allowed_updates'] ?? $allowed_updates;
481
        }
482
483
        // Take custom input into account.
484
        if ($custom_input = $this->getCustomInput()) {
485
            try {
486
                $input = json_decode($this->input, true, 512, JSON_THROW_ON_ERROR);
487
                if (empty($input)) {
488
                    throw new TelegramException('Custom input is empty');
489
                }
490
                $response = new ServerResponse($input, $this->bot_username);
491
            } catch (\Throwable $e) {
492
                throw new TelegramException('Invalid custom input JSON: ' . $e->getMessage());
493
            }
494
        } else {
495
            if (DB::isDbConnected() && $last_update = DB::selectTelegramUpdate(1)) {
496
                // Get last Update id from the database.
497
                $last_update          = reset($last_update);
498
                $this->last_update_id = $last_update['id'] ?? null;
499
            }
500
501
            if ($this->last_update_id !== null) {
502
                $offset = $this->last_update_id + 1; // As explained in the telegram bot API documentation.
503
            }
504
505
            $response = Request::getUpdates(compact('offset', 'limit', 'timeout', 'allowed_updates'));
506
        }
507
508
        if ($response->isOk()) {
509
            // Log update.
510
            TelegramLog::update($response->toJson());
511
512
            // Process all updates
513
            /** @var Update $update */
514
            foreach ($response->getResult() as $update) {
515
                $this->processUpdate($update);
516
            }
517
518
            if (!DB::isDbConnected() && !$custom_input && $this->last_update_id !== null && $offset === 0) {
519
                // Mark update(s) as read after handling
520
                $offset = $this->last_update_id + 1;
521
                $limit  = 1;
522
523
                Request::getUpdates(compact('offset', 'limit', 'timeout', 'allowed_updates'));
524
            }
525
        }
526
527
        return $response;
528
    }
529
530
    /**
531
     * Handle bot request from webhook
532
     *
533
     * @return bool
534
     *
535
     * @throws TelegramException
536
     */
537
    public function handle(): bool
538
    {
539
        if ($this->bot_username === '') {
540
            throw new TelegramException('Bot Username is not defined!');
541
        }
542
543
        $input = Request::getInput();
544
        if (empty($input)) {
545
            throw new TelegramException('Input is empty! The webhook must not be called manually, only by Telegram.');
546
        }
547
548
        // Log update.
549
        TelegramLog::update($input);
550
551
        $post = json_decode($input, true);
552
        if (empty($post)) {
553
            throw new TelegramException('Invalid input JSON! The webhook must not be called manually, only by Telegram.');
554
        }
555
556
        if ($response = $this->processUpdate(new Update($post, $this->bot_username))) {
557
            return $response->isOk();
558
        }
559
560
        return false;
561
    }
562
563
    /**
564
     * Get the command name from the command type
565
     *
566
     * @param string $type
567
     *
568
     * @return string
569
     */
570
    protected function getCommandFromType(string $type): string
571
    {
572
        return $this->ucFirstUnicode(str_replace('_', '', $type));
573
    }
574
575
    /**
576
     * Process bot Update request
577
     *
578
     * @param Update $update
579
     *
580
     * @return ServerResponse
581
     * @throws TelegramException
582
     */
583
    public function processUpdate(Update $update): ServerResponse
584 1
    {
585
        $this->update         = $update;
586 1
        $this->last_update_id = $update->getUpdateId();
587 1
588
        if (is_callable($this->update_filter)) {
589 1
            $reason = 'Update denied by update_filter';
590 1
            try {
591
                $allowed = (bool) call_user_func_array($this->update_filter, [$update, $this, &$reason]);
592 1
            } catch (Exception $e) {
593
                $allowed = false;
594
            }
595
596
            if (!$allowed) {
597 1
                TelegramLog::debug($reason);
598 1
                return new ServerResponse(['ok' => false, 'description' => 'denied']);
599 1
            }
600
        }
601
602
        //Load admin commands
603
        if ($this->isAdmin()) {
604
            $this->addCommandsPath(TB_BASE_COMMANDS_PATH . '/AdminCommands', false);
605
        }
606
607
        //Make sure we have an up-to-date command list
608
        //This is necessary to "require" all the necessary command files!
609
        $this->commands_objects = $this->getCommandsList();
610
611
        //If all else fails, it's a generic message.
612
        $command = self::GENERIC_MESSAGE_COMMAND;
613
614
        $update_type = $this->update->getUpdateType();
615
        if ($update_type === 'message') {
616
            $message = $this->update->getMessage();
617
            $type    = $message->getType();
618
619
            // Let's check if the message object has the type field we're looking for...
620
            $command_tmp = $type === 'command' ? $message->getCommand() : $this->getCommandFromType($type);
621
            // ...and if a fitting command class is available.
622
            $command_obj = $command_tmp ? $this->getCommandObject($command_tmp) : null;
623
624
            // Empty usage string denotes a non-executable command.
625
            // @see https://github.com/php-telegram-bot/core/issues/772#issuecomment-388616072
626
            if (
627
                ($command_obj === null && $type === 'command')
628
                || ($command_obj !== null && $command_obj->getUsage() !== '')
629
            ) {
630
                $command = $command_tmp;
631
            }
632
        } elseif ($update_type !== null) {
633
            $command = $this->getCommandFromType($update_type);
634
        }
635
636
        //Make sure we don't try to process update that was already processed
637
        $last_id = DB::selectTelegramUpdate(1, $this->update->getUpdateId());
638
        if ($last_id && count($last_id) === 1) {
639
            TelegramLog::debug('Duplicate update received, processing aborted!');
640
            return Request::emptyResponse();
641
        }
642
643
        DB::insertRequest($this->update);
644
645
        return $this->executeCommand($command);
646
    }
647
648
    /**
649
     * Execute /command
650
     *
651
     * @param string $command
652
     *
653
     * @return ServerResponse
654
     * @throws TelegramException
655
     */
656
    public function executeCommand(string $command): ServerResponse
657
    {
658
        $command = mb_strtolower($command);
659
660
        $command_obj = $this->commands_objects[$command] ?? $this->getCommandObject($command);
661
662
        if (!$command_obj || !$command_obj->isEnabled()) {
663
            //Failsafe in case the Generic command can't be found
664
            if ($command === self::GENERIC_COMMAND) {
665
                throw new TelegramException('Generic command missing!');
666
            }
667
668
            //Handle a generic command or non existing one
669
            $this->last_command_response = $this->executeCommand(self::GENERIC_COMMAND);
670
        } else {
671
            //execute() method is executed after preExecute()
672
            //This is to prevent executing a DB query without a valid connection
673
            $this->last_command_response = $command_obj->preExecute();
674
        }
675
676
        return $this->last_command_response;
677
    }
678
679
    /**
680
     * @deprecated
681
     *
682
     * @param string $command
683
     *
684
     * @return string
685
     */
686
    protected function sanitizeCommand(string $command): string
687 1
    {
688
        return str_replace(' ', '', $this->ucWordsUnicode(str_replace('_', ' ', $command)));
689 1
    }
690
691
    /**
692
     * Enable a single Admin account
693
     *
694
     * @param int $admin_id Single admin id
695
     *
696
     * @return Telegram
697
     */
698
    public function enableAdmin(int $admin_id): Telegram
699 1
    {
700
        if ($admin_id <= 0) {
701 1
            TelegramLog::error('Invalid value "' . $admin_id . '" for admin.');
702
        } elseif (!in_array($admin_id, $this->admins_list, true)) {
703 1
            $this->admins_list[] = $admin_id;
704 1
        }
705
706
        return $this;
707 1
    }
708
709
    /**
710
     * Enable a list of Admin Accounts
711
     *
712
     * @param array $admin_ids List of admin ids
713
     *
714
     * @return Telegram
715
     */
716
    public function enableAdmins(array $admin_ids): Telegram
717 1
    {
718
        foreach ($admin_ids as $admin_id) {
719 1
            $this->enableAdmin($admin_id);
720 1
        }
721
722
        return $this;
723 1
    }
724
725
    /**
726
     * Get list of admins
727
     *
728
     * @return array
729
     */
730
    public function getAdminList(): array
731 1
    {
732
        return $this->admins_list;
733 1
    }
734
735
    /**
736
     * Check if the passed user is an admin
737
     *
738
     * If no user id is passed, the current update is checked for a valid message sender.
739
     *
740
     * @param int|null $user_id
741
     *
742
     * @return bool
743
     */
744
    public function isAdmin($user_id = null): bool
745 1
    {
746
        if ($user_id === null && $this->update !== null) {
747 1
            //Try to figure out if the user is an admin
748
            $update_methods = [
749
                'getMessage',
750
                'getEditedMessage',
751
                'getChannelPost',
752
                'getEditedChannelPost',
753
                'getInlineQuery',
754
                'getChosenInlineResult',
755
                'getCallbackQuery',
756
            ];
757
            foreach ($update_methods as $update_method) {
758
                $object = call_user_func([$this->update, $update_method]);
759
                if ($object !== null && $from = $object->getFrom()) {
760
                    $user_id = $from->getId();
761
                    break;
762
                }
763
            }
764
        }
765
766
        return ($user_id === null) ? false : in_array($user_id, $this->admins_list, true);
767 1
    }
768
769
    /**
770
     * Check if user required the db connection
771
     *
772
     * @return bool
773
     */
774
    public function isDbEnabled(): bool
775
    {
776
        return $this->mysql_enabled;
777
    }
778
779
    /**
780
     * Add a single custom command class
781
     *
782
     * @param string $command_class Full command class name
783
     *
784
     * @return Telegram
785
     */
786
    public function addCommandClass(string $command_class): Telegram
787 2
    {
788
        if (!$command_class || !class_exists($command_class)) {
789 2
            $error = sprintf('Command class "%s" does not exist.', $command_class);
790 1
            TelegramLog::error($error);
791 1
            throw new InvalidArgumentException($error);
792 1
        }
793
794
        if (!is_a($command_class, Command::class, true)) {
795 2
            $error = sprintf('Command class "%s" does not extend "%s".', $command_class, Command::class);
796
            TelegramLog::error($error);
797
            throw new InvalidArgumentException($error);
798
        }
799
800
        // Dummy object to get data from.
801
        $command_object = new $command_class($this);
802 2
803
        $auth = null;
804 2
        $command_object->isSystemCommand() && $auth = Command::AUTH_SYSTEM;
805 2
        $command_object->isAdminCommand() && $auth = Command::AUTH_ADMIN;
806 2
        $command_object->isUserCommand() && $auth = Command::AUTH_USER;
807 2
808
        if ($auth) {
809 2
            $command = mb_strtolower($command_object->getName());
810 2
811
            $this->command_classes[$auth][$command] = $command_class;
812 2
        }
813
814
        return $this;
815 2
    }
816
817
    /**
818
     * Add multiple custom command classes
819
     *
820
     * @param array $command_classes List of full command class names
821
     *
822
     * @return Telegram
823
     */
824
    public function addCommandClasses(array $command_classes): Telegram
825 1
    {
826
        foreach ($command_classes as $command_class) {
827 1
            $this->addCommandClass($command_class);
828 1
        }
829
830
        return $this;
831 1
    }
832
833
    /**
834
     * Set a single custom commands path
835
     *
836
     * @param string $path Custom commands path to set
837
     *
838
     * @return Telegram
839
     */
840
    public function setCommandsPath(string $path): Telegram
841
    {
842
        $this->commands_paths = [];
843
844
        $this->addCommandsPath($path);
845
846
        return $this;
847
    }
848
849
    /**
850
     * Add a single custom commands path
851
     *
852
     * @param string $path   Custom commands path to add
853
     * @param bool   $before If the path should be prepended or appended to the list
854
     *
855
     * @return Telegram
856
     */
857
    public function addCommandsPath(string $path, bool $before = true): Telegram
858 33
    {
859
        if (!is_dir($path)) {
860 33
            TelegramLog::error('Commands path "' . $path . '" does not exist.');
861 1
        } elseif (!in_array($path, $this->commands_paths, true)) {
862 33
            if ($before) {
863 33
                array_unshift($this->commands_paths, $path);
864 33
            } else {
865
                $this->commands_paths[] = $path;
866
            }
867
        }
868
869
        return $this;
870 33
    }
871
872
    /**
873
     * Set multiple custom commands paths
874
     *
875
     * @param array $paths Custom commands paths to add
876
     *
877
     * @return Telegram
878
     */
879
    public function setCommandsPaths(array $paths): Telegram
880
    {
881
        $this->commands_paths = [];
882
883
        $this->addCommandsPaths($paths);
884
885
        return $this;
886
    }
887
888
    /**
889
     * Add multiple custom commands paths
890
     *
891
     * @param array $paths  Custom commands paths to add
892
     * @param bool  $before If the paths should be prepended or appended to the list
893
     *
894
     * @return Telegram
895
     */
896
    public function addCommandsPaths(array $paths, bool $before = true): Telegram
897 1
    {
898
        foreach ($paths as $path) {
899 1
            $this->addCommandsPath($path, $before);
900 1
        }
901
902
        return $this;
903 1
    }
904
905
    /**
906
     * Return the list of commands paths
907
     *
908
     * @return array
909
     */
910
    public function getCommandsPaths(): array
911 1
    {
912
        return $this->commands_paths;
913 1
    }
914
915
    /**
916
     * Return the list of command classes
917
     *
918
     * @return array
919
     */
920
    public function getCommandClasses(): array
921 2
    {
922
        return $this->command_classes;
923 2
    }
924
925
    /**
926
     * Set custom upload path
927
     *
928
     * @param string $path Custom upload path
929
     *
930
     * @return Telegram
931
     */
932
    public function setUploadPath(string $path): Telegram
933 1
    {
934
        $this->upload_path = $path;
935 1
936
        return $this;
937 1
    }
938
939
    /**
940
     * Get custom upload path
941
     *
942
     * @return string
943
     */
944
    public function getUploadPath(): string
945 1
    {
946
        return $this->upload_path;
947 1
    }
948
949
    /**
950
     * Set custom download path
951
     *
952
     * @param string $path Custom download path
953
     *
954
     * @return Telegram
955
     */
956
    public function setDownloadPath(string $path): Telegram
957 1
    {
958
        $this->download_path = $path;
959 1
960
        return $this;
961 1
    }
962
963
    /**
964
     * Get custom download path
965
     *
966
     * @return string
967
     */
968
    public function getDownloadPath(): string
969 1
    {
970
        return $this->download_path;
971 1
    }
972
973
    /**
974
     * Set command config
975
     *
976
     * Provide further variables to a particular commands.
977
     * For example you can add the channel name at the command /sendtochannel
978
     * Or you can add the api key for external service.
979
     *
980
     * @param string $command
981
     * @param array  $config
982
     *
983
     * @return Telegram
984
     */
985
    public function setCommandConfig(string $command, array $config): Telegram
986 13
    {
987
        $this->commands_config[$command] = $config;
988 13
989
        return $this;
990 13
    }
991
992
    /**
993
     * Get command config
994
     *
995
     * @param string $command
996
     *
997
     * @return array
998
     */
999
    public function getCommandConfig(string $command): array
1000 16
    {
1001
        return $this->commands_config[$command] ?? [];
1002 16
    }
1003
1004
    /**
1005
     * Get API key
1006
     *
1007
     * @return string
1008
     */
1009
    public function getApiKey(): string
1010 1
    {
1011
        return $this->api_key;
1012 1
    }
1013
1014
    /**
1015
     * Get Bot name
1016
     *
1017
     * @return string
1018
     */
1019
    public function getBotUsername(): string
1020 2
    {
1021
        return $this->bot_username;
1022 2
    }
1023
1024
    /**
1025
     * Get Bot Id
1026
     *
1027
     * @return int
1028
     */
1029
    public function getBotId(): int
1030
    {
1031
        return $this->bot_id;
1032
    }
1033
1034
    /**
1035
     * Get Version
1036
     *
1037
     * @return string
1038
     */
1039
    public function getVersion(): string
1040
    {
1041
        return $this->version;
1042
    }
1043
1044
    /**
1045
     * Set Webhook for bot
1046
     *
1047
     * @param string $url
1048
     * @param array  $data Optional parameters.
1049
     *
1050
     * @return ServerResponse
1051
     * @throws TelegramException
1052
     */
1053
    public function setWebhook(string $url, array $data = []): ServerResponse
1054
    {
1055
        if ($url === '') {
1056
            throw new TelegramException('Hook url is empty!');
1057
        }
1058
1059
        $data        = array_intersect_key($data, array_flip([
1060
            'certificate',
1061
            'ip_address',
1062
            'max_connections',
1063
            'allowed_updates',
1064
            'drop_pending_updates',
1065
            'secret_token',
1066
        ]));
1067
        $data['url'] = $url;
1068
1069
        // If the certificate is passed as a path, encode and add the file to the data array.
1070
        if (!empty($data['certificate']) && is_string($data['certificate'])) {
1071
            $data['certificate'] = Request::encodeFile($data['certificate']);
1072
        }
1073
1074
        $result = Request::setWebhook($data);
1075
1076
        if (!$result->isOk()) {
1077
            throw new TelegramException(
1078
                'Webhook was not set! Error: ' . $result->getErrorCode() . ' ' . $result->getDescription()
1079
            );
1080
        }
1081
1082
        return $result;
1083
    }
1084
1085
    /**
1086
     * Delete any assigned webhook
1087
     *
1088
     * @param array $data
1089
     *
1090
     * @return ServerResponse
1091
     * @throws TelegramException
1092
     */
1093
    public function deleteWebhook(array $data = []): ServerResponse
1094
    {
1095
        $result = Request::deleteWebhook($data);
1096
1097
        if (!$result->isOk()) {
1098
            throw new TelegramException(
1099
                'Webhook was not deleted! Error: ' . $result->getErrorCode() . ' ' . $result->getDescription()
1100
            );
1101
        }
1102
1103
        return $result;
1104
    }
1105
1106
    /**
1107
     * Replace function `ucwords` for UTF-8 characters in the class definition and commands
1108
     *
1109
     * @param string $str
1110
     * @param string $encoding (default = 'UTF-8')
1111
     *
1112
     * @return string
1113
     */
1114
    protected function ucWordsUnicode(string $str, string $encoding = 'UTF-8'): string
1115 1
    {
1116
        return mb_convert_case($str, MB_CASE_TITLE, $encoding);
1117 1
    }
1118
1119
    /**
1120
     * Replace function `ucfirst` for UTF-8 characters in the class definition and commands
1121
     *
1122
     * @param string $str
1123
     * @param string $encoding (default = 'UTF-8')
1124
     *
1125
     * @return string
1126
     */
1127
    protected function ucFirstUnicode(string $str, string $encoding = 'UTF-8'): string
1128 2
    {
1129
        return mb_strtoupper(mb_substr($str, 0, 1, $encoding), $encoding)
1130 2
            . mb_strtolower(mb_substr($str, 1, mb_strlen($str), $encoding), $encoding);
1131 2
    }
1132
1133
    /**
1134
     * Enable requests limiter
1135
     *
1136
     * @param array $options
1137
     *
1138
     * @return Telegram
1139
     * @throws TelegramException
1140
     */
1141
    public function enableLimiter(array $options = []): Telegram
1142
    {
1143
        Request::setLimiter(true, $options);
1144
1145
        return $this;
1146
    }
1147
1148
    /**
1149
     * Run provided commands
1150
     *
1151
     * @param array $commands
1152
     *
1153
     * @return ServerResponse[]
1154
     *
1155
     * @throws TelegramException
1156
     */
1157
    public function runCommands(array $commands): array
1158
    {
1159
        if (empty($commands)) {
1160
            throw new TelegramException('No command(s) provided!');
1161
        }
1162
1163
        $this->run_commands = true;
1164
1165
        // Check if this request has a user Update / comes from Telegram.
1166
        if ($userUpdate = $this->update) {
1167
            $from = $this->update->getMessage()->getFrom();
1168
            $chat = $this->update->getMessage()->getChat();
1169
        } else {
1170
            // Fall back to the Bot user.
1171
            $from = new User([
1172
                'id'         => $this->getBotId(),
1173
                'first_name' => $this->getBotUsername(),
1174
                'username'   => $this->getBotUsername(),
1175
            ]);
1176
1177
            // Try to get "live" Bot info.
1178
            $response = Request::getMe();
1179
            if ($response->isOk()) {
1180
                /** @var User $result */
1181
                $result = $response->getResult();
1182
1183
                $from = new User([
1184
                    'id'         => $result->getId(),
1185
                    'first_name' => $result->getFirstName(),
1186
                    'username'   => $result->getUsername(),
1187
                ]);
1188
            }
1189
1190
            // Give Bot access to admin commands.
1191
            $this->enableAdmin($from->getId());
1192
1193
            // Lock the bot to a private chat context.
1194
            $chat = new Chat([
1195
                'id'   => $from->getId(),
1196
                'type' => 'private',
1197
            ]);
1198
        }
1199
1200
        $newUpdate = static function ($text = '') use ($from, $chat) {
1201
            return new Update([
1202
                'update_id' => -1,
1203
                'message'   => [
1204
                    'message_id' => -1,
1205
                    'date'       => time(),
1206
                    'from'       => json_decode($from->toJson(), true),
1207
                    'chat'       => json_decode($chat->toJson(), true),
1208
                    'text'       => $text,
1209
                ],
1210
            ]);
1211
        };
1212
1213
        $responses = [];
1214
1215
        foreach ($commands as $command) {
1216
            $this->update = $newUpdate($command);
1217
1218
            // Refresh commands list for new Update object.
1219
            $this->commands_objects = $this->getCommandsList();
1220
1221
            $responses[] = $this->executeCommand($this->update->getMessage()->getCommand());
1222
        }
1223
1224
        // Reset Update to initial context.
1225
        $this->update = $userUpdate;
1226
1227
        return $responses;
1228
    }
1229
1230
    /**
1231
     * Is this session initiated by runCommands()
1232
     *
1233
     * @return bool
1234
     */
1235
    public function isRunCommands(): bool
1236
    {
1237
        return $this->run_commands;
1238
    }
1239
1240
    /**
1241
     * Switch to enable running getUpdates without a database
1242
     *
1243
     * @param bool $enable
1244
     *
1245
     * @return Telegram
1246
     */
1247
    public function useGetUpdatesWithoutDatabase(bool $enable = true): Telegram
1248
    {
1249
        $this->getupdates_without_database = $enable;
1250
1251
        return $this;
1252
    }
1253
1254
    /**
1255
     * Return last update id
1256
     *
1257
     * @return int|null
1258
     */
1259
    public function getLastUpdateId(): ?int
1260
    {
1261
        return $this->last_update_id;
1262
    }
1263
1264
    /**
1265
     * Set an update filter callback
1266
     *
1267
     * @param callable $callback
1268
     *
1269
     * @return Telegram
1270
     */
1271
    public function setUpdateFilter(callable $callback): Telegram
1272 1
    {
1273
        $this->update_filter = $callback;
1274 1
1275
        return $this;
1276 1
    }
1277
1278
    /**
1279
     * Return update filter callback
1280
     *
1281
     * @return callable|null
1282
     */
1283
    public function getUpdateFilter(): ?callable
1284
    {
1285
        return $this->update_filter;
1286
    }
1287
1288
    /**
1289
     * Converts the name of a class into the name of a command.
1290
     *
1291
     * @param string $class For example FooBarCommand
1292
     *
1293
     * @return string for example foo_bar. In case of errors, returns an empty string
1294
     */
1295
    protected function classNameToCommandName(string $class): string
1296
    {
1297
        // 7 is the length of 'Command'
1298
        if (substr($class, -7) !== 'Command') {
1299
            return '';
1300
        }
1301
        return mb_strtolower(preg_replace('/(.)(?=[\p{Lu}])/u', '$1_', substr($class, 0, -7)));
1302
    }
1303
1304
    /**
1305
     * Converts a command name into the name of a class.
1306
     *
1307
     * @param string $command For example foo_bar
1308
     *
1309
     * @return string for example FooBarCommand. In case of errors, returns an empty string
1310
     */
1311
    protected function commandNameToClassName(string $command): string
1312
    {
1313
        if ($command === '') {
1314
            return '';
1315
        }
1316
        return str_replace(' ', '', $this->ucWordsUnicode(str_replace('_', ' ', $command))) . 'Command';
1317
    }
1318
}
1319