Completed
Push — master ( 285a64...6e54a8 )
by Armando
01:53 queued 53s
created

BotManager   C

Complexity

Total Complexity 74

Size/Duplication

Total Lines 561
Duplicated Lines 4.46 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 0
Metric Value
wmc 74
lcom 1
cbo 10
dl 25
loc 561
rs 5.5244
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 1
A inTest() 0 4 2
A getTelegram() 0 4 1
A getParams() 0 4 1
A getAction() 0 4 1
B run() 0 25 6
B initLogging() 0 10 5
B validateSecret() 0 13 5
C validateAndSetWebhook() 0 36 7
A handleOutput() 0 10 2
A setBotExtras() 0 7 1
B setBotExtrasTelegram() 0 32 5
B setBotExtrasRequest() 0 22 4
A handleRequest() 0 12 3
A handleCron() 0 12 2
A getLoopTime() 14 14 4
A getLoopInterval() 11 11 4
A handleGetUpdatesLoop() 0 16 2
A setCustomGetUpdatesCallback() 0 5 1
A handleGetUpdates() 0 13 2
B defaultGetUpdatesCallback() 0 35 5
A handleWebhook() 0 6 1
A getOutput() 0 7 1
B isValidRequest() 0 20 6
A validateRequest() 0 6 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like BotManager 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 BotManager, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
/**
3
 * This file is part of the TelegramBotManager package.
4
 *
5
 * (c) Armando Lüscher <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace TelegramBot\TelegramBotManager;
12
13
use Longman\IPTools\Ip;
14
use Longman\TelegramBot\Entities;
15
use Longman\TelegramBot\Request;
16
use Longman\TelegramBot\Telegram;
17
use Longman\TelegramBot\TelegramLog;
18
use TelegramBot\TelegramBotManager\Exception\InvalidAccessException;
19
use TelegramBot\TelegramBotManager\Exception\InvalidWebhookException;
20
21
class BotManager
22
{
23
    /**
24
     * @var string Telegram post servers IP range
25
     */
26
    const TELEGRAM_IP_RANGE = '149.154.167.197-149.154.167.233';
27
28
    /**
29
     * @var string The output for testing, instead of echoing
30
     */
31
    private $output = '';
32
33
    /**
34
     * @var \Longman\TelegramBot\Telegram
35
     */
36
    private $telegram;
37
38
    /**
39
     * @var \TelegramBot\TelegramBotManager\Params Object that manages the parameters.
40
     */
41
    private $params;
42
43
    /**
44
     * @var \TelegramBot\TelegramBotManager\Action Object that contains the current action.
45
     */
46
    private $action;
47
48
    /**
49
     * @var callable
50
     */
51
    private $custom_get_updates_callback;
52
53
    /**
54
     * BotManager constructor.
55
     *
56
     * @param array $params
57
     *
58
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidParamsException
59
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidActionException
60
     * @throws \Longman\TelegramBot\Exception\TelegramException
61
     */
62
    public function __construct(array $params)
63
    {
64
        // Initialise logging before anything else, to allow errors to be logged.
65
        $this->initLogging($params['logging'] ?? []);
66
67
        $this->params = new Params($params);
68
        $this->action = new Action($this->params->getScriptParam('a'));
69
70
        // Set up a new Telegram instance.
71
        $this->telegram = new Telegram(
72
            $this->params->getBotParam('api_key'),
73
            $this->params->getBotParam('bot_username')
74
        );
75
    }
76
77
    /**
78
     * Check if we're busy running the PHPUnit tests.
79
     *
80
     * @return bool
81
     */
82
    public static function inTest(): bool
83
    {
84
        return defined('PHPUNIT_TEST') && PHPUNIT_TEST === true;
85
    }
86
87
    /**
88
     * Return the Telegram object.
89
     *
90
     * @return \Longman\TelegramBot\Telegram
91
     */
92
    public function getTelegram(): Telegram
93
    {
94
        return $this->telegram;
95
    }
96
97
    /**
98
     * Get the Params object.
99
     *
100
     * @return \TelegramBot\TelegramBotManager\Params
101
     */
102
    public function getParams(): Params
103
    {
104
        return $this->params;
105
    }
106
107
    /**
108
     * Get the Action object.
109
     *
110
     * @return \TelegramBot\TelegramBotManager\Action
111
     */
112
    public function getAction(): Action
113
    {
114
        return $this->action;
115
    }
116
117
    /**
118
     * Run this thing in all its glory!
119
     *
120
     * @return \TelegramBot\TelegramBotManager\BotManager
121
     * @throws \Longman\TelegramBot\Exception\TelegramException
122
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
123
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidWebhookException
124
     * @throws \Exception
125
     */
126
    public function run(): self
127
    {
128
        // Make sure this is a valid call.
129
        $this->validateSecret();
130
        $this->validateRequest();
131
132
        if ($this->action->isAction('webhookinfo')) {
133
            $webhookinfo = Request::getWebhookInfo();
134
            print_r($webhookinfo->getResult() ?: $webhookinfo->printError());
135
            return $this;
136
        }
137
        if ($this->action->isAction(['set', 'unset', 'reset'])) {
138
            return $this->validateAndSetWebhook();
139
        }
140
141
        $this->setBotExtras();
142
143
        if ($this->action->isAction('handle')) {
144
            $this->handleRequest();
145
        } elseif ($this->action->isAction('cron')) {
146
            $this->handleCron();
147
        }
148
149
        return $this;
150
    }
151
152
    /**
153
     * Initialise all loggers.
154
     *
155
     * @param array $log_paths
156
     *
157
     * @return \TelegramBot\TelegramBotManager\BotManager
158
     * @throws \Exception
159
     */
160
    public function initLogging(array $log_paths): self
161
    {
162
        foreach ($log_paths as $logger => $logfile) {
163
            ('debug' === $logger) && TelegramLog::initDebugLog($logfile);
164
            ('error' === $logger) && TelegramLog::initErrorLog($logfile);
165
            ('update' === $logger) && TelegramLog::initUpdateLog($logfile);
166
        }
167
168
        return $this;
169
    }
170
171
    /**
172
     * Make sure the passed secret is valid.
173
     *
174
     * @param bool $force Force validation, even on CLI.
175
     *
176
     * @return \TelegramBot\TelegramBotManager\BotManager
177
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
178
     */
179
    public function validateSecret(bool $force = false): self
180
    {
181
        // If we're running from CLI, secret isn't necessary.
182
        if ($force || 'cli' !== PHP_SAPI) {
183
            $secret     = $this->params->getBotParam('secret');
184
            $secret_get = $this->params->getScriptParam('s');
185
            if (!isset($secret, $secret_get) || $secret !== $secret_get) {
186
                throw new InvalidAccessException('Invalid access');
187
            }
188
        }
189
190
        return $this;
191
    }
192
193
    /**
194
     * Make sure the webhook is valid and perform the requested webhook operation.
195
     *
196
     * @return \TelegramBot\TelegramBotManager\BotManager
197
     * @throws \Longman\TelegramBot\Exception\TelegramException
198
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidWebhookException
199
     */
200
    public function validateAndSetWebhook(): self
201
    {
202
        $webhook = $this->params->getBotParam('webhook');
203
        if (empty($webhook['url'] ?? null) && $this->action->isAction(['set', 'reset'])) {
204
            throw new InvalidWebhookException('Invalid webhook');
205
        }
206
207
        if ($this->action->isAction(['unset', 'reset'])) {
208
            $this->handleOutput($this->telegram->deleteWebhook()->getDescription() . PHP_EOL);
209
            // When resetting the webhook, sleep for a bit to prevent too many requests.
210
            $this->action->isAction('reset') && sleep(1);
211
        }
212
213
        if ($this->action->isAction(['set', 'reset'])) {
214
            $webhook_params = array_filter([
215
                'certificate'     => $webhook['certificate'] ?? null,
216
                'max_connections' => $webhook['max_connections'] ?? null,
217
                'allowed_updates' => $webhook['allowed_updates'] ?? null,
218
            ], function ($v, $k) {
219
                if ($k === 'allowed_updates') {
220
                    // Special case for allowed_updates, which can be an empty array.
221
                    return is_array($v);
222
                }
223
                return !empty($v);
224
            }, ARRAY_FILTER_USE_BOTH);
225
226
            $this->handleOutput(
227
                $this->telegram->setWebhook(
228
                    $webhook['url'] . '?a=handle&s=' . $this->params->getBotParam('secret'),
229
                    $webhook_params
230
                )->getDescription() . PHP_EOL
231
            );
232
        }
233
234
        return $this;
235
    }
236
237
    /**
238
     * Save the test output and echo it if we're not in a test.
239
     *
240
     * @param string $output
241
     *
242
     * @return \TelegramBot\TelegramBotManager\BotManager
243
     */
244
    private function handleOutput(string $output): self
245
    {
246
        $this->output .= $output;
247
248
        if (!self::inTest()) {
249
            echo $output;
250
        }
251
252
        return $this;
253
    }
254
255
    /**
256
     * Set any extra bot features that have been assigned on construction.
257
     *
258
     * @return \TelegramBot\TelegramBotManager\BotManager
259
     * @throws \Longman\TelegramBot\Exception\TelegramException
260
     */
261
    public function setBotExtras(): self
262
    {
263
        $this->setBotExtrasTelegram();
264
        $this->setBotExtrasRequest();
265
266
        return $this;
267
    }
268
269
    /**
270
     * Set extra bot parameters for Telegram object.
271
     *
272
     * @return \TelegramBot\TelegramBotManager\BotManager
273
     * @throws \Longman\TelegramBot\Exception\TelegramException
274
     */
275
    protected function setBotExtrasTelegram(): self
276
    {
277
        $simple_extras = [
278
            'admins'         => 'enableAdmins',
279
            'mysql'          => 'enableMySql',
280
            'commands.paths' => 'addCommandsPaths',
281
            'custom_input'   => 'setCustomInput',
282
            'paths.download' => 'setDownloadPath',
283
            'paths.upload'   => 'setUploadPath',
284
        ];
285
        // For simple telegram extras, just pass the single param value to the Telegram method.
286
        foreach ($simple_extras as $param_key => $method) {
287
            $param = $this->params->getBotParam($param_key);
288
            if (null !== $param) {
289
                $this->telegram->$method($param);
290
            }
291
        }
292
293
        // Custom command configs.
294
        $command_configs = $this->params->getBotParam('commands.configs', []);
295
        foreach ($command_configs as $command => $config) {
296
            $this->telegram->setCommandConfig($command, $config);
297
        }
298
299
        // Botan with options.
300
        if ($botan_token = $this->params->getBotParam('botan.token')) {
301
            $botan_options = $this->params->getBotParam('botan.options', []);
302
            $this->telegram->enableBotan($botan_token, $botan_options);
303
        }
304
305
        return $this;
306
    }
307
308
    /**
309
     * Set extra bot parameters for Request class.
310
     *
311
     * @return \TelegramBot\TelegramBotManager\BotManager
312
     * @throws \Longman\TelegramBot\Exception\TelegramException
313
     */
314
    protected function setBotExtrasRequest(): self
315
    {
316
        $request_extras = [
317
            // None at the moment...
318
        ];
319
        // For request extras, just pass the single param value to the Request method.
320
        foreach ($request_extras as $param_key => $method) {
321
            $param = $this->params->getBotParam($param_key);
322
            if (null !== $param) {
323
                Request::$method($param);
324
            }
325
        }
326
327
        // Special cases.
328
        $limiter_enabled = $this->params->getBotParam('limiter.enabled');
329
        if ($limiter_enabled !== null) {
330
            $limiter_options = $this->params->getBotParam('limiter.options', []);
331
            Request::setLimiter($limiter_enabled, $limiter_options);
332
        }
333
334
        return $this;
335
    }
336
337
    /**
338
     * Handle the request, which calls either the Webhook or getUpdates method respectively.
339
     *
340
     * @return \TelegramBot\TelegramBotManager\BotManager
341
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
342
     * @throws \Longman\TelegramBot\Exception\TelegramException
343
     */
344
    public function handleRequest(): self
345
    {
346
        if ($this->params->getBotParam('webhook.url')) {
347
            return $this->handleWebhook();
348
        }
349
350
        if ($loop_time = $this->getLoopTime()) {
351
            return $this->handleGetUpdatesLoop($loop_time, $this->getLoopInterval());
352
        }
353
354
        return $this->handleGetUpdates();
355
    }
356
357
    /**
358
     * Handle cron.
359
     *
360
     * @return \TelegramBot\TelegramBotManager\BotManager
361
     * @throws \Longman\TelegramBot\Exception\TelegramException
362
     */
363
    public function handleCron(): self
364
    {
365
        $groups = explode(',', $this->params->getScriptParam('g', 'default'));
366
367
        $commands = [];
368
        foreach ($groups as $group) {
369
            $commands[] = $this->params->getBotParam('cron.groups.' . $group, []);
370
        }
371
        $this->telegram->runCommands(array_merge(...$commands));
372
373
        return $this;
374
    }
375
376
    /**
377
     * Get the number of seconds the script should loop.
378
     *
379
     * @return int
380
     */
381 View Code Duplication
    public function getLoopTime(): int
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
382
    {
383
        $loop_time = $this->params->getScriptParam('l');
384
385
        if (null === $loop_time) {
386
            return 0;
387
        }
388
389
        if (is_string($loop_time) && '' === trim($loop_time)) {
390
            return 604800; // Default to 7 days.
391
        }
392
393
        return max(0, (int) $loop_time);
394
    }
395
396
    /**
397
     * Get the number of seconds the script should wait after each getUpdates request.
398
     *
399
     * @return int
400
     */
401 View Code Duplication
    public function getLoopInterval(): int
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
402
    {
403
        $interval_time = $this->params->getScriptParam('i');
404
405
        if (null === $interval_time || (is_string($interval_time) && '' === trim($interval_time))) {
406
            return 2;
407
        }
408
409
        // Minimum interval is 1 second.
410
        return max(1, (int) $interval_time);
411
    }
412
413
    /**
414
     * Loop the getUpdates method for the passed amount of seconds.
415
     *
416
     * @param int $loop_time_in_seconds
417
     * @param int $loop_interval_in_seconds
418
     *
419
     * @return \TelegramBot\TelegramBotManager\BotManager
420
     * @throws \Longman\TelegramBot\Exception\TelegramException
421
     */
422
    public function handleGetUpdatesLoop(int $loop_time_in_seconds, int $loop_interval_in_seconds = 2): self
423
    {
424
        // Remember the time we started this loop.
425
        $now = time();
426
427
        $this->handleOutput('Looping getUpdates until ' . date('Y-m-d H:i:s', $now + $loop_time_in_seconds) . PHP_EOL);
428
429
        while ($now > time() - $loop_time_in_seconds) {
430
            $this->handleGetUpdates();
431
432
            // Chill a bit.
433
            sleep($loop_interval_in_seconds);
434
        }
435
436
        return $this;
437
    }
438
439
    /**
440
     * Set a custom callback for handling the output of the getUpdates results.
441
     *
442
     * @param callable $callback
443
     *
444
     * @return \TelegramBot\TelegramBotManager\BotManager
445
     */
446
    public function setCustomGetUpdatesCallback(callable $callback): BotManager
447
    {
448
        $this->custom_get_updates_callback = $callback;
449
        return $this;
450
    }
451
452
    /**
453
     * Handle the updates using the getUpdates method.
454
     *
455
     * @return \TelegramBot\TelegramBotManager\BotManager
456
     * @throws \Longman\TelegramBot\Exception\TelegramException
457
     */
458
    public function handleGetUpdates(): self
459
    {
460
        $get_updates_response = $this->telegram->handleGetUpdates();
461
462
        // Check if the user has set a custom callback for handling the response.
463
        if ($this->custom_get_updates_callback !== null) {
464
            $this->handleOutput(call_user_func($this->custom_get_updates_callback, $get_updates_response));
465
        } else {
466
            $this->handleOutput($this->defaultGetUpdatesCallback($get_updates_response));
467
        }
468
469
        return $this;
470
    }
471
472
    /**
473
     * Return the default output for getUpdates handling.
474
     *
475
     * @param Entities\ServerResponse $get_updates_response
476
     *
477
     * @return string
478
     */
479
    protected function defaultGetUpdatesCallback($get_updates_response): string
480
    {
481
        /** @var Entities\Update[] $results */
482
        $results = array_filter((array) $get_updates_response->getResult());
483
484
        $output = sprintf(
485
            '%s - Updates processed: %d' . PHP_EOL,
486
            date('Y-m-d H:i:s'),
487
            count($results)
488
        );
489
490
        foreach ($results as $result) {
491
            $chat_id = 0;
492
            $text    = '<n/a>';
493
494
            $update_content = $result->getUpdateContent();
495
            if ($update_content instanceof Entities\Message) {
496
                $chat_id = $update_content->getFrom()->getId();
497
                $text    = sprintf('<%s>', $update_content->getType());
498
            } elseif ($update_content instanceof Entities\InlineQuery ||
499
                      $update_content instanceof Entities\ChosenInlineResult
500
            ) {
501
                $chat_id = $update_content->getFrom()->getId();
502
                $text    = sprintf('<query> %s', $update_content->getQuery());
503
            }
504
505
            $output .= sprintf(
506
                '%d: %s' . PHP_EOL,
507
                $chat_id,
508
                preg_replace('/\s+/', ' ', trim($text))
509
            );
510
        }
511
512
        return $output;
513
    }
514
515
    /**
516
     * Handle the updates using the Webhook method.
517
     *
518
     * @return \TelegramBot\TelegramBotManager\BotManager
519
     * @throws \Longman\TelegramBot\Exception\TelegramException
520
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
521
     */
522
    public function handleWebhook(): self
523
    {
524
        $this->telegram->handle();
525
526
        return $this;
527
    }
528
529
    /**
530
     * Return the current test output and clear it.
531
     *
532
     * @return string
533
     */
534
    public function getOutput(): string
535
    {
536
        $output       = $this->output;
537
        $this->output = '';
538
539
        return $output;
540
    }
541
542
    /**
543
     * Check if this is a valid request coming from a Telegram API IP address.
544
     *
545
     * @link https://core.telegram.org/bots/webhooks#the-short-version
546
     *
547
     * @return bool
548
     */
549
    public function isValidRequest(): bool
550
    {
551
        // If we're running from CLI, requests are always valid, unless we're running the tests.
552
        if ((!self::inTest() && 'cli' === PHP_SAPI) || false === $this->params->getBotParam('validate_request')) {
553
            return true;
554
        }
555
556
        $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
557
        foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR'] as $key) {
558
            if (filter_var($_SERVER[$key] ?? null, FILTER_VALIDATE_IP)) {
559
                $ip = $_SERVER[$key];
560
                break;
561
            }
562
        }
563
564
        return Ip::match($ip, array_merge(
565
            [self::TELEGRAM_IP_RANGE],
566
            (array) $this->params->getBotParam('valid_ips', [])
567
        ));
568
    }
569
570
    /**
571
     * Make sure this is a valid request.
572
     *
573
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
574
     */
575
    private function validateRequest()
576
    {
577
        if (!$this->isValidRequest()) {
578
            throw new InvalidAccessException('Invalid access');
579
        }
580
    }
581
}
582