Completed
Push — develop ( 236d3b...4b6bc8 )
by Armando
19s queued 13s
created

Telegram::processUpdate()   C

Complexity

Conditions 15
Paths 122

Size

Total Lines 63
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 98.5267

Importance

Changes 10
Bugs 2 Features 1
Metric Value
cc 15
eloc 33
c 10
b 2
f 1
nc 122
nop 1
dl 0
loc 63
ccs 9
cts 32
cp 0.2813
crap 98.5267
rs 5.7333

How to fix   Long Method    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.72.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
194
     * Filter updates
195
     *
196
     * @var callback
197
     */
198
    protected $update_filter;
199
200
    /**
201
     * Telegram constructor.
202
     *
203
     * @param string $api_key
204
     * @param string $bot_username
205
     *
206
     * @throws TelegramException
207
     */
208 33
    public function __construct(string $api_key, string $bot_username = '')
209
    {
210 33
        if (empty($api_key)) {
211 1
            throw new TelegramException('API KEY not defined!');
212
        }
213 33
        preg_match('/(\d+):[\w\-]+/', $api_key, $matches);
214 33
        if (!isset($matches[1])) {
215 1
            throw new TelegramException('Invalid API KEY defined!');
216
        }
217 33
        $this->bot_id  = (int) $matches[1];
218 33
        $this->api_key = $api_key;
219
220 33
        $this->bot_username = $bot_username;
221
222
        //Add default system commands path
223 33
        $this->addCommandsPath(TB_BASE_COMMANDS_PATH . '/SystemCommands');
224
225 33
        Request::initialize($this);
226 33
    }
227
228
    /**
229
     * Initialize Database connection
230
     *
231
     * @param array  $credentials
232
     * @param string $table_prefix
233
     * @param string $encoding
234
     *
235
     * @return Telegram
236
     * @throws TelegramException
237
     */
238 9
    public function enableMySql(array $credentials, string $table_prefix = '', string $encoding = 'utf8mb4'): Telegram
239
    {
240 9
        $this->pdo = DB::initialize($credentials, $this, $table_prefix, $encoding);
241 9
        ConversationDB::initializeConversation();
242 9
        $this->mysql_enabled = true;
243
244 9
        return $this;
245
    }
246
247
    /**
248
     * Initialize Database external connection
249
     *
250
     * @param PDO    $external_pdo_connection PDO database object
251
     * @param string $table_prefix
252
     *
253
     * @return Telegram
254
     * @throws TelegramException
255
     */
256
    public function enableExternalMySql(PDO $external_pdo_connection, string $table_prefix = ''): Telegram
257
    {
258
        $this->pdo = DB::externalInitialize($external_pdo_connection, $this, $table_prefix);
259
        ConversationDB::initializeConversation();
260
        $this->mysql_enabled = true;
261
262
        return $this;
263
    }
264
265
    /**
266
     * Get commands list
267
     *
268
     * @return array $commands
269
     * @throws TelegramException
270
     */
271 1
    public function getCommandsList(): array
272
    {
273 1
        $commands = [];
274
275 1
        foreach ($this->commands_paths as $path) {
276
            try {
277
                //Get all "*Command.php" files
278 1
                $files = new RegexIterator(
279 1
                    new RecursiveIteratorIterator(
280 1
                        new RecursiveDirectoryIterator($path)
281
                    ),
282 1
                    '/^.+Command.php$/'
283
                );
284
285 1
                foreach ($files as $file) {
286
                    //Remove "Command.php" from filename
287 1
                    $command      = $this->sanitizeCommand(substr($file->getFilename(), 0, -11));
288 1
                    $command_name = mb_strtolower($command);
289
290 1
                    if (array_key_exists($command_name, $commands)) {
291
                        continue;
292
                    }
293
294 1
                    require_once $file->getPathname();
295
296 1
                    $command_obj = $this->getCommandObject($command, $file->getPathname());
297 1
                    if ($command_obj instanceof Command) {
298 1
                        $commands[$command_name] = $command_obj;
299
                    }
300
                }
301
            } catch (Exception $e) {
302
                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

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