Telegram   F
last analyzed

Complexity

Total Complexity 146

Size/Duplication

Total Lines 1302
Duplicated Lines 0 %

Test Coverage

Coverage 38.62%

Importance

Changes 39
Bugs 5 Features 4
Metric Value
eloc 359
dl 0
loc 1302
ccs 151
cts 391
cp 0.3862
rs 2
c 39
b 5
f 4
wmc 146

52 Methods

Rating   Name   Duplication   Size   Complexity  
A getAdminList() 0 3 1
A isDbEnabled() 0 3 1
A enableAdmins() 0 7 2
A enableMySql() 0 7 1
A enableExternalMySql() 0 7 1
A __construct() 0 18 3
A getCustomInput() 0 3 1
A getCommandFromType() 0 3 1
A getFileNamespace() 0 8 2
A getCommandClassName() 0 31 6
A handle() 0 24 5
A setCustomInput() 0 5 1
A getLastCommandResponse() 0 3 1
B getCommandObject() 0 29 11
A ucFirstUnicode() 0 4 1
A getCommandClasses() 0 3 1
A deleteWebhook() 0 11 2
A setCommandsPath() 0 7 1
A setUploadPath() 0 5 1
A ucWordsUnicode() 0 3 1
A setDownloadPath() 0 5 1
A addCommandsPath() 0 13 4
B isAdmin() 0 23 7
A addCommandClasses() 0 7 2
A setUpdateFilter() 0 5 1
A getCommandConfig() 0 3 1
A getApiKey() 0 3 1
A getBotId() 0 3 1
A isRunCommands() 0 3 1
A commandNameToClassName() 0 7 2
A addCommandsPaths() 0 7 2
A executeCommand() 0 25 5
A setCommandsPaths() 0 7 1
A getBotUsername() 0 3 1
A enableLimiter() 0 5 1
A setCommandConfig() 0 5 1
A setWebhook() 0 30 5
A getUploadPath() 0 3 1
A getDownloadPath() 0 3 1
A getCommandsPaths() 0 3 1
A enableAdmin() 0 9 3
B getCommandsList() 0 41 7
A getVersion() 0 3 1
A getUpdateFilter() 0 3 1
A sanitizeCommand() 0 3 1
A classNameToCommandName() 0 8 2
A runCommands() 0 71 5
B addCommandClass() 0 29 8
A useGetUpdatesWithoutDatabase() 0 5 1
C handleGetUpdates() 0 82 17
C processUpdate() 0 63 15
A getLastUpdateId() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Telegram often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Telegram, and based on these observations, apply Extract Interface, too.

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