GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Telegram::getCommandConfig()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
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 1
14 1
defined('TB_BASE_PATH') || define('TB_BASE_PATH', __DIR__);
15
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.76.1';
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 30
     * Is running getUpdates without DB enabled
169
     *
170 30
     * @var bool
171 1
     */
172
    protected $getupdates_without_database = false;
173 30
174 30
    /**
175 1
     * Last update ID
176
     * Only used when running getUpdates without a database
177 30
     *
178 30
     * @var int
179
     */
180 30
    protected $last_update_id;
181 30
182
    /**
183
     * The command to be executed when there's a new message update and nothing more suitable is found
184
     */
185 30
    public const GENERIC_MESSAGE_COMMAND = 'genericmessage';
186
187 30
    /**
188 30
     * 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 9
     * Telegram constructor.
201
     *
202 9
     * @param string $api_key
203 9
     * @param string $bot_username
204 9
     *
205
     * @throws TelegramException
206 9
     */
207
    public function __construct(string $api_key, string $bot_username = '')
208
    {
209
        if (empty($api_key)) {
210
            throw new TelegramException('API KEY not defined!');
211
        }
212
        preg_match('/(\d+):[\w\-]+/', $api_key, $matches);
213
        if (!isset($matches[1])) {
214
            throw new TelegramException('Invalid API KEY defined!');
215
        }
216
        $this->bot_id  = (int) $matches[1];
217
        $this->api_key = $api_key;
218
219
        $this->bot_username = $bot_username;
220
221
        //Add default system commands path
222
        $this->addCommandsPath(TB_BASE_COMMANDS_PATH . '/SystemCommands');
223
224
        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 1
     *
234
     * @return Telegram
235 1
     * @throws TelegramException
236
     */
237 1
    public function enableMySql(array $credentials, string $table_prefix = '', string $encoding = 'utf8mb4'): Telegram
238
    {
239
        $this->pdo = DB::initialize($credentials, $this, $table_prefix, $encoding);
240 1
        ConversationDB::initializeConversation();
241 1
        $this->mysql_enabled = true;
242 1
243
        return $this;
244 1
    }
245
246
    /**
247 1
     * Initialize Database external connection
248
     *
249 1
     * @param PDO    $external_pdo_connection PDO database object
250 1
     * @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 1
        $this->pdo = DB::externalInitialize($external_pdo_connection, $this, $table_prefix);
258
        ConversationDB::initializeConversation();
259 1
        $this->mysql_enabled = true;
260 1
261 1
        return $this;
262
    }
263
264
    /**
265 1
     * Get commands list
266
     *
267
     * @return array $commands
268
     * @throws TelegramException
269 1
     */
270
    public function getCommandsList(): array
271
    {
272
        $commands = [];
273
274
        foreach ($this->commands_paths as $path) {
275
            try {
276
                //Get all "*Command.php" files
277
                $files = new RegexIterator(
278
                    new RecursiveIteratorIterator(
279 1
                        new RecursiveDirectoryIterator($path)
280
                    ),
281 1
                    '/^.+Command.php$/'
282 1
                );
283
284
                foreach ($files as $file) {
285 1
                    //Remove "Command.php" from filename
286 1
                    $command      = $this->sanitizeCommand(substr($file->getFilename(), 0, -11));
287 1
                    $command_name = mb_strtolower($command);
288 1
289
                    if (array_key_exists($command_name, $commands)) {
290
                        continue;
291 1
                    }
292 1
293 1
                    require_once $file->getPathname();
294 1
295
                    $command_obj = $this->getCommandObject($command, $file->getPathname());
296
                    if ($command_obj instanceof Command) {
297
                        $commands[$command_name] = $command_obj;
298
                    }
299
                }
300
            } catch (Exception $e) {
301
                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

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