Passed
Pull Request — develop (#1491)
by Rabie
14:52
created

Telegram::commandNameToClassName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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