Passed
Pull Request — develop (#1365)
by Michele
05:30
created

Telegram::useGetUpdatesWithoutDatabase()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
ccs 0
cts 2
cp 0
crap 2
rs 10
c 1
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.79.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->classNameToCommandName(substr($file->getFilename(), 0, -4));
287 1
288
                    if (array_key_exists($command, $commands)) {
289 1
                        continue;
290
                    }
291
292
                    require_once $file->getPathname();
293 1
294
                    $command_obj = $this->getCommandObject($command, $file->getPathname());
295 1
                    if ($command_obj instanceof Command) {
296 1
                        $commands[$command] = $command_obj;
297 1
                    }
298
                }
299
            } catch (Exception $e) {
300
                throw new TelegramException('Error getting commands from path: ' . $path, $e);
0 ignored issues
show
Bug introduced by
$e of type Exception is incompatible with the type integer expected by parameter $code of Longman\TelegramBot\Exce...xception::__construct(). ( Ignorable by Annotation )

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

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