Passed
Push — master ( 627c19...f3fe5e )
by Armando
03:09
created

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

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

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