Passed
Pull Request — develop (#1493)
by Rabie
06:35
created

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