Passed
Push — master ( 9ca615...b6ea35 )
by Armando
02:47
created

Telegram::isRunCommands()   A

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 0
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
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\ServerResponse;
24
use Longman\TelegramBot\Entities\Update;
25
use Longman\TelegramBot\Exception\TelegramException;
26
use PDO;
27
use RecursiveDirectoryIterator;
28
use RecursiveIteratorIterator;
29
use RegexIterator;
30
31
class Telegram
32
{
33
    /**
34
     * Version
35
     *
36
     * @var string
37
     */
38
    protected $version = '0.72.0';
39
40
    /**
41
     * Telegram API key
42
     *
43
     * @var string
44
     */
45
    protected $api_key = '';
46
47
    /**
48
     * Telegram Bot username
49
     *
50
     * @var string
51
     */
52
    protected $bot_username = '';
53
54
    /**
55
     * Telegram Bot id
56
     *
57
     * @var int
58
     */
59
    protected $bot_id = 0;
60
61
    /**
62
     * Raw request data (json) for webhook methods
63
     *
64
     * @var string
65
     */
66
    protected $input = '';
67
68
    /**
69
     * Custom commands paths
70
     *
71
     * @var array
72
     */
73
    protected $commands_paths = [];
74
75
    /**
76
     * Custom command class names
77
     * ```
78
     * [
79
     *     'User' => [
80
     *         // command_name => command_class
81
     *         'start' => 'name\space\to\StartCommand',
82
     *     ],
83
     *     'Admin' => [], //etc
84
     * ]
85
     * ```
86
     *
87
     * @var array
88
     */
89
    protected $command_classes = [
90
        Command::AUTH_USER   => [],
91
        Command::AUTH_ADMIN  => [],
92
        Command::AUTH_SYSTEM => [],
93
    ];
94
95
    /**
96
     * Custom commands objects
97
     *
98
     * @var array
99
     */
100
    protected $commands_objects = [];
101
102
    /**
103
     * Current Update object
104
     *
105
     * @var Update
106
     */
107
    protected $update;
108
109
    /**
110
     * Upload path
111
     *
112
     * @var string
113
     */
114
    protected $upload_path = '';
115
116
    /**
117
     * Download path
118
     *
119
     * @var string
120
     */
121
    protected $download_path = '';
122
123
    /**
124
     * MySQL integration
125
     *
126
     * @var bool
127
     */
128
    protected $mysql_enabled = false;
129
130
    /**
131
     * PDO object
132
     *
133
     * @var PDO
134
     */
135
    protected $pdo;
136
137
    /**
138
     * Commands config
139
     *
140
     * @var array
141
     */
142
    protected $commands_config = [];
143
144
    /**
145
     * Admins list
146
     *
147
     * @var array
148
     */
149
    protected $admins_list = [];
150
151
    /**
152
     * ServerResponse of the last Command execution
153
     *
154
     * @var ServerResponse
155
     */
156
    protected $last_command_response;
157
158
    /**
159
     * Check if runCommands() is running in this session
160
     *
161
     * @var bool
162
     */
163
    protected $run_commands = false;
164
165
    /**
166
     * Is running getUpdates without DB enabled
167
     *
168
     * @var bool
169
     */
170
    protected $getupdates_without_database = false;
171
172
    /**
173
     * Last update ID
174
     * Only used when running getUpdates without a database
175
     *
176
     * @var int
177
     */
178
    protected $last_update_id;
179
180
    /**
181
     * The command to be executed when there's a new message update and nothing more suitable is found
182
     */
183
    public const GENERIC_MESSAGE_COMMAND = 'genericmessage';
184
185
    /**
186
     * The command to be executed by default (when no other relevant commands are applicable)
187
     */
188
    public const GENERIC_COMMAND = 'generic';
189
190
    /**
191
     * Update filter
192
     * Filter updates
193
     *
194
     * @var callback
195
     */
196
    protected $update_filter;
197
198
    /**
199
     * Telegram constructor.
200
     *
201
     * @param string $api_key
202
     * @param string $bot_username
203
     *
204
     * @throws TelegramException
205
     */
206 33
    public function __construct(string $api_key, string $bot_username = '')
207
    {
208 33
        if (empty($api_key)) {
209 1
            throw new TelegramException('API KEY not defined!');
210
        }
211 33
        preg_match('/(\d+):[\w\-]+/', $api_key, $matches);
212 33
        if (!isset($matches[1])) {
213 1
            throw new TelegramException('Invalid API KEY defined!');
214
        }
215 33
        $this->bot_id  = (int) $matches[1];
216 33
        $this->api_key = $api_key;
217
218 33
        $this->bot_username = $bot_username;
219
220
        //Add default system commands path
221 33
        $this->addCommandsPath(TB_BASE_COMMANDS_PATH . '/SystemCommands');
222
223 33
        Request::initialize($this);
224 33
    }
225
226
    /**
227
     * Initialize Database connection
228
     *
229
     * @param array  $credentials
230
     * @param string $table_prefix
231
     * @param string $encoding
232
     *
233
     * @return Telegram
234
     * @throws TelegramException
235
     */
236 9
    public function enableMySql(array $credentials, string $table_prefix = '', string $encoding = 'utf8mb4'): Telegram
237
    {
238 9
        $this->pdo = DB::initialize($credentials, $this, $table_prefix, $encoding);
239 9
        ConversationDB::initializeConversation();
240 9
        $this->mysql_enabled = true;
241
242 9
        return $this;
243
    }
244
245
    /**
246
     * Initialize Database external connection
247
     *
248
     * @param PDO    $external_pdo_connection PDO database object
249
     * @param string $table_prefix
250
     *
251
     * @return Telegram
252
     * @throws TelegramException
253
     */
254
    public function enableExternalMySql(PDO $external_pdo_connection, string $table_prefix = ''): Telegram
255
    {
256
        $this->pdo = DB::externalInitialize($external_pdo_connection, $this, $table_prefix);
257
        ConversationDB::initializeConversation();
258
        $this->mysql_enabled = true;
259
260
        return $this;
261
    }
262
263
    /**
264
     * Get commands list
265
     *
266
     * @return array $commands
267
     * @throws TelegramException
268
     */
269 1
    public function getCommandsList(): array
270
    {
271 1
        $commands = [];
272
273 1
        foreach ($this->commands_paths as $path) {
274
            try {
275
                //Get all "*Command.php" files
276 1
                $files = new RegexIterator(
277 1
                    new RecursiveIteratorIterator(
278 1
                        new RecursiveDirectoryIterator($path)
279
                    ),
280 1
                    '/^.+Command.php$/'
281
                );
282
283 1
                foreach ($files as $file) {
284
                    //Remove "Command.php" from filename
285 1
                    $command      = $this->sanitizeCommand(substr($file->getFilename(), 0, -11));
286 1
                    $command_name = mb_strtolower($command);
287
288 1
                    if (array_key_exists($command_name, $commands)) {
289
                        continue;
290
                    }
291
292 1
                    require_once $file->getPathname();
293
294 1
                    $command_obj = $this->getCommandObject($command, $file->getPathname());
295 1
                    if ($command_obj instanceof Command) {
296 1
                        $commands[$command_name] = $command_obj;
297
                    }
298
                }
299
            } catch (Exception $e) {
300
                throw new TelegramException('Error getting commands from path: ' . $path, $e);
0 ignored issues
show
Bug introduced by
$e of type Exception is incompatible with the type integer expected by parameter $code of Longman\TelegramBot\Exce...xception::__construct(). ( Ignorable by Annotation )

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

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