Passed
Pull Request — develop (#1493)
by Rabie
06:35
created

Telegram::getCommandObject()   C

Complexity

Conditions 13
Paths 29

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 13.4931

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 13
eloc 22
c 2
b 0
f 0
nc 29
nop 2
dl 0
loc 39
ccs 12
cts 14
cp 0.8571
crap 13.4931
rs 6.6166

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