Passed
Push — master ( 097177...94d770 )
by Armando
04:40 queued 01:51
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 16.3179

Importance

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

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file is part of the TelegramBot package.
5
 *
6
 * (c) Avtandil Kikabidze aka LONGMAN <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Longman\TelegramBot;
13
14 1
defined('TB_BASE_PATH') || define('TB_BASE_PATH', __DIR__);
15 1
defined('TB_BASE_COMMANDS_PATH') || define('TB_BASE_COMMANDS_PATH', TB_BASE_PATH . '/Commands');
16
17
use Exception;
18
use InvalidArgumentException;
19
use Longman\TelegramBot\Commands\AdminCommand;
20
use Longman\TelegramBot\Commands\Command;
21
use Longman\TelegramBot\Commands\SystemCommand;
22
use Longman\TelegramBot\Commands\UserCommand;
23
use Longman\TelegramBot\Entities\Chat;
24
use Longman\TelegramBot\Entities\ServerResponse;
25
use Longman\TelegramBot\Entities\Update;
26
use Longman\TelegramBot\Entities\User;
27
use Longman\TelegramBot\Exception\TelegramException;
28
use PDO;
29
use RecursiveDirectoryIterator;
30
use RecursiveIteratorIterator;
31
use RegexIterator;
32
33
class Telegram
34
{
35
    /**
36
     * Version
37
     *
38
     * @var string
39
     */
40
    protected $version = '0.80.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
                    // Convert filename to command
286 1
                    $command = $this->classNameToCommandName(substr($file->getFilename(), 0, -4));
287
288
                    // Invalid Classname
289 1
                    if (is_null($command)) {
290
                        continue;
291
                    }
292
293
                    // Already registered
294 1
                    if (array_key_exists($command, $commands)) {
295
                        continue;
296
                    }
297
298 1
                    require_once $file->getPathname();
299
300 1
                    $command_obj = $this->getCommandObject($command, $file->getPathname());
301 1
                    if ($command_obj instanceof Command) {
302 1
                        $commands[$command] = $command_obj;
303
                    }
304
                }
305
            } catch (Exception $e) {
306
                throw new TelegramException('Error getting commands from path: ' . $path, $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

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