Passed
Pull Request — develop (#1345)
by
unknown
09:03
created

Telegram::isAdmin()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 39.826

Importance

Changes 0
Metric Value
cc 7
eloc 15
nc 8
nop 1
dl 0
loc 23
ccs 1
cts 8
cp 0.125
crap 39.826
rs 8.8333
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
    /**
141
     * EntityManager object
142
     *
143
     * @var EntityManagerInterface
0 ignored issues
show
Bug introduced by
The type Longman\TelegramBot\EntityManagerInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
144
     */
145
    public $em;
146
147
    /**
148
     * Commands config
149
     *
150
     * @var array
151
     */
152
    protected $commands_config = [];
153
154
    /**
155
     * Admins list
156
     *
157
     * @var array
158
     */
159
    protected $admins_list = [];
160
161
    /**
162
     * ServerResponse of the last Command execution
163
     *
164
     * @var ServerResponse
165
     */
166
    protected $last_command_response;
167
168
    /**
169
     * Check if runCommands() is running in this session
170
     *
171
     * @var bool
172
     */
173
    protected $run_commands = false;
174
175
    /**
176
     * Is running getUpdates without DB enabled
177
     *
178
     * @var bool
179
     */
180
    protected $getupdates_without_database = false;
181
182
    /**
183
     * Last update ID
184
     * Only used when running getUpdates without a database
185
     *
186
     * @var int
187
     */
188
    protected $last_update_id;
189
190
    /**
191
     * The command to be executed when there's a new message update and nothing more suitable is found
192
     */
193
    public const GENERIC_MESSAGE_COMMAND = 'genericmessage';
194
195
    /**
196
     * The command to be executed by default (when no other relevant commands are applicable)
197
     */
198
    public const GENERIC_COMMAND = 'generic';
199
200
    /**
201
     * Update filter method
202
     *
203
     * @var callable
204
     */
205
    protected $update_filter;
206
207 33
    /**
208
     * Telegram constructor.
209 33
     *
210 1
     * @param string $api_key
211
     * @param string $bot_username
212 33
     *
213 33
     * @throws TelegramException
214 1
     */
215
    public function __construct(string $api_key, string $bot_username = '')
216 33
    {
217 33
        if (empty($api_key)) {
218
            throw new TelegramException('API KEY not defined!');
219 33
        }
220
        preg_match('/(\d+):[\w\-]+/', $api_key, $matches);
221
        if (!isset($matches[1])) {
222 33
            throw new TelegramException('Invalid API KEY defined!');
223
        }
224 33
        $this->bot_id  = (int) $matches[1];
225
        $this->api_key = $api_key;
226
227
        $this->bot_username = $bot_username;
228
229
        //Add default system commands path
230
        $this->addCommandsPath(TB_BASE_COMMANDS_PATH . '/SystemCommands');
231
232
        Request::initialize($this);
233
    }
234
235
    /**
236
     * Initialize Database connection
237 9
     *
238
     * @param array  $credentials
239 9
     * @param string $table_prefix
240 9
     * @param string $encoding
241 9
     *
242
     * @return Telegram
243 9
     * @throws TelegramException
244
     */
245
    public function enableMySql(array $credentials, string $table_prefix = '', string $encoding = 'utf8mb4'): Telegram
246
    {
247
        $this->pdo = DB::initialize($credentials, $this, $table_prefix, $encoding);
248
        ConversationDB::initializeConversation();
249
        $this->mysql_enabled = true;
250
251
        return $this;
252
    }
253
254
    /**
255
     * Initialize Database external connection
256
     *
257
     * @param PDO    $external_pdo_connection PDO database object
258
     * @param string $table_prefix
259
     *
260
     * @return Telegram
261
     * @throws TelegramException
262
     */
263
    public function enableExternalMySql(PDO $external_pdo_connection, string $table_prefix = ''): Telegram
264
    {
265
        $this->pdo = DB::externalInitialize($external_pdo_connection, $this, $table_prefix);
266
        ConversationDB::initializeConversation();
267
        $this->mysql_enabled = true;
268
269
        return $this;
270 1
    }
271
    
272 1
    public function enableEntityManager(EntityManagerInterface $em): Telegram
273
    {
274 1
        $this->em = $em;
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return Longman\TelegramBot\Telegram. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
275
    }
276
277 1
    /**
278 1
     * Get commands list
279 1
     *
280
     * @return array $commands
281
     * @throws TelegramException
282
     */
283
    public function getCommandsList(): array
284 1
    {
285
        $commands = [];
286 1
287 1
        foreach ($this->commands_paths as $path) {
288
            try {
289 1
                //Get all "*Command.php" files
290
                $files = new RegexIterator(
291
                    new RecursiveIteratorIterator(
292
                        new RecursiveDirectoryIterator($path)
293 1
                    ),
294
                    '/^.+Command.php$/'
295 1
                );
296 1
297 1
                foreach ($files as $file) {
298
                    //Remove "Command.php" from filename
299
                    $command      = $this->sanitizeCommand(substr($file->getFilename(), 0, -11));
300
                    $command_name = mb_strtolower($command);
301
302
                    if (array_key_exists($command_name, $commands)) {
303
                        continue;
304
                    }
305 1
306
                    require_once $file->getPathname();
307
308
                    $command_obj = $this->getCommandObject($command, $file->getPathname());
309
                    if ($command_obj instanceof Command) {
310
                        $commands[$command_name] = $command_obj;
311
                    }
312
                }
313
            } catch (Exception $e) {
314
                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

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