Completed
Push — develop ( 8776ed...c3cd3f )
by Armando
02:57
created

BotManager   C

Complexity

Total Complexity 72

Size/Duplication

Total Lines 527
Duplicated Lines 4.74 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
wmc 72
lcom 1
cbo 11
dl 25
loc 527
rs 5.5667
c 0
b 0
f 0

23 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
B handleGetUpdates() 0 40 6
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
     * BotManager constructor.
50
     *
51
     * @param array $params
52
     *
53
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidParamsException
54
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidActionException
55
     * @throws \Longman\TelegramBot\Exception\TelegramException
56
     */
57
    public function __construct(array $params)
58
    {
59
        // Initialise logging before anything else, to allow errors to be logged.
60
        $this->initLogging($params['logging'] ?? []);
61
62
        $this->params = new Params($params);
63
        $this->action = new Action($this->params->getScriptParam('a'));
64
65
        // Set up a new Telegram instance.
66
        $this->telegram = new Telegram(
67
            $this->params->getBotParam('api_key'),
68
            $this->params->getBotParam('bot_username')
69
        );
70
    }
71
72
    /**
73
     * Check if we're busy running the PHPUnit tests.
74
     *
75
     * @return bool
76
     */
77
    public static function inTest(): bool
78
    {
79
        return defined('PHPUNIT_TEST') && PHPUNIT_TEST === true;
80
    }
81
82
    /**
83
     * Return the Telegram object.
84
     *
85
     * @return \Longman\TelegramBot\Telegram
86
     */
87
    public function getTelegram(): Telegram
88
    {
89
        return $this->telegram;
90
    }
91
92
    /**
93
     * Get the Params object.
94
     *
95
     * @return \TelegramBot\TelegramBotManager\Params
96
     */
97
    public function getParams(): Params
98
    {
99
        return $this->params;
100
    }
101
102
    /**
103
     * Get the Action object.
104
     *
105
     * @return \TelegramBot\TelegramBotManager\Action
106
     */
107
    public function getAction(): Action
108
    {
109
        return $this->action;
110
    }
111
112
    /**
113
     * Run this thing in all its glory!
114
     *
115
     * @return \TelegramBot\TelegramBotManager\BotManager
116
     * @throws \Longman\TelegramBot\Exception\TelegramException
117
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
118
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidWebhookException
119
     * @throws \Exception
120
     */
121
    public function run(): self
122
    {
123
        // Make sure this is a valid call.
124
        $this->validateSecret();
125
        $this->validateRequest();
126
127
        if ($this->action->isAction('webhookinfo')) {
128
            $webhookinfo = Request::getWebhookInfo();
129
            print_r($webhookinfo->getResult() ?: $webhookinfo->printError());
130
            return $this;
131
        }
132
        if ($this->action->isAction(['set', 'unset', 'reset'])) {
133
            return $this->validateAndSetWebhook();
134
        }
135
136
        $this->setBotExtras();
137
138
        if ($this->action->isAction('handle')) {
139
            $this->handleRequest();
140
        } elseif ($this->action->isAction('cron')) {
141
            $this->handleCron();
142
        }
143
144
        return $this;
145
    }
146
147
    /**
148
     * Initialise all loggers.
149
     *
150
     * @param array $log_paths
151
     *
152
     * @return \TelegramBot\TelegramBotManager\BotManager
153
     * @throws \Exception
154
     */
155
    public function initLogging(array $log_paths): self
156
    {
157
        foreach ($log_paths as $logger => $logfile) {
158
            ('debug' === $logger) && TelegramLog::initDebugLog($logfile);
159
            ('error' === $logger) && TelegramLog::initErrorLog($logfile);
160
            ('update' === $logger) && TelegramLog::initUpdateLog($logfile);
161
        }
162
163
        return $this;
164
    }
165
166
    /**
167
     * Make sure the passed secret is valid.
168
     *
169
     * @param bool $force Force validation, even on CLI.
170
     *
171
     * @return \TelegramBot\TelegramBotManager\BotManager
172
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
173
     */
174
    public function validateSecret(bool $force = false): self
175
    {
176
        // If we're running from CLI, secret isn't necessary.
177
        if ($force || 'cli' !== PHP_SAPI) {
178
            $secret     = $this->params->getBotParam('secret');
179
            $secret_get = $this->params->getScriptParam('s');
180
            if (!isset($secret, $secret_get) || $secret !== $secret_get) {
181
                throw new InvalidAccessException('Invalid access');
182
            }
183
        }
184
185
        return $this;
186
    }
187
188
    /**
189
     * Make sure the webhook is valid and perform the requested webhook operation.
190
     *
191
     * @return \TelegramBot\TelegramBotManager\BotManager
192
     * @throws \Longman\TelegramBot\Exception\TelegramException
193
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidWebhookException
194
     */
195
    public function validateAndSetWebhook(): self
196
    {
197
        $webhook = $this->params->getBotParam('webhook');
198
        if (empty($webhook['url'] ?? null) && $this->action->isAction(['set', 'reset'])) {
199
            throw new InvalidWebhookException('Invalid webhook');
200
        }
201
202
        if ($this->action->isAction(['unset', 'reset'])) {
203
            $this->handleOutput($this->telegram->deleteWebhook()->getDescription() . PHP_EOL);
204
            // When resetting the webhook, sleep for a bit to prevent too many requests.
205
            $this->action->isAction('reset') && sleep(1);
206
        }
207
208
        if ($this->action->isAction(['set', 'reset'])) {
209
            $webhook_params = array_filter([
210
                'certificate'     => $webhook['certificate'] ?? null,
211
                'max_connections' => $webhook['max_connections'] ?? null,
212
                'allowed_updates' => $webhook['allowed_updates'] ?? null,
213
            ], function ($v, $k) {
214
                if ($k === 'allowed_updates') {
215
                    // Special case for allowed_updates, which can be an empty array.
216
                    return is_array($v);
217
                }
218
                return !empty($v);
219
            }, ARRAY_FILTER_USE_BOTH);
220
221
            $this->handleOutput(
222
                $this->telegram->setWebhook(
223
                    $webhook['url'] . '?a=handle&s=' . $this->params->getBotParam('secret'),
224
                    $webhook_params
225
                )->getDescription() . PHP_EOL
226
            );
227
        }
228
229
        return $this;
230
    }
231
232
    /**
233
     * Save the test output and echo it if we're not in a test.
234
     *
235
     * @param string $output
236
     *
237
     * @return \TelegramBot\TelegramBotManager\BotManager
238
     */
239
    private function handleOutput(string $output): self
240
    {
241
        $this->output .= $output;
242
243
        if (!self::inTest()) {
244
            echo $output;
245
        }
246
247
        return $this;
248
    }
249
250
    /**
251
     * Set any extra bot features that have been assigned on construction.
252
     *
253
     * @return \TelegramBot\TelegramBotManager\BotManager
254
     * @throws \Longman\TelegramBot\Exception\TelegramException
255
     */
256
    public function setBotExtras(): self
257
    {
258
        $this->setBotExtrasTelegram();
259
        $this->setBotExtrasRequest();
260
261
        return $this;
262
    }
263
264
    /**
265
     * Set extra bot parameters for Telegram object.
266
     *
267
     * @return \TelegramBot\TelegramBotManager\BotManager
268
     * @throws \Longman\TelegramBot\Exception\TelegramException
269
     */
270
    protected function setBotExtrasTelegram(): self
271
    {
272
        $simple_extras = [
273
            'admins'         => 'enableAdmins',
274
            'mysql'          => 'enableMySql',
275
            'commands.paths' => 'addCommandsPaths',
276
            'custom_input'   => 'setCustomInput',
277
            'paths.download' => 'setDownloadPath',
278
            'paths.upload'   => 'setUploadPath',
279
        ];
280
        // For simple telegram extras, just pass the single param value to the Telegram method.
281
        foreach ($simple_extras as $param_key => $method) {
282
            $param = $this->params->getBotParam($param_key);
283
            if (null !== $param) {
284
                $this->telegram->$method($param);
285
            }
286
        }
287
288
        // Custom command configs.
289
        $command_configs = $this->params->getBotParam('commands.configs', []);
290
        foreach ($command_configs as $command => $config) {
291
            $this->telegram->setCommandConfig($command, $config);
292
        }
293
294
        // Botan with options.
295
        if ($botan_token = $this->params->getBotParam('botan.token')) {
296
            $botan_options = $this->params->getBotParam('botan.options', []);
297
            $this->telegram->enableBotan($botan_token, $botan_options);
298
        }
299
300
        return $this;
301
    }
302
303
    /**
304
     * Set extra bot parameters for Request class.
305
     *
306
     * @return \TelegramBot\TelegramBotManager\BotManager
307
     * @throws \Longman\TelegramBot\Exception\TelegramException
308
     */
309
    protected function setBotExtrasRequest(): self
310
    {
311
        $request_extras = [
312
            // None at the moment...
313
        ];
314
        // For request extras, just pass the single param value to the Request method.
315
        foreach ($request_extras as $param_key => $method) {
316
            $param = $this->params->getBotParam($param_key);
317
            if (null !== $param) {
318
                Request::$method($param);
319
            }
320
        }
321
322
        // Special cases.
323
        $limiter_enabled = $this->params->getBotParam('limiter.enabled');
324
        if ($limiter_enabled !== null) {
325
            $limiter_options = $this->params->getBotParam('limiter.options', []);
326
            Request::setLimiter($limiter_enabled, $limiter_options);
327
        }
328
329
        return $this;
330
    }
331
332
    /**
333
     * Handle the request, which calls either the Webhook or getUpdates method respectively.
334
     *
335
     * @return \TelegramBot\TelegramBotManager\BotManager
336
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
337
     * @throws \Longman\TelegramBot\Exception\TelegramException
338
     */
339
    public function handleRequest(): self
340
    {
341
        if ($this->params->getBotParam('webhook.url')) {
342
            return $this->handleWebhook();
343
        }
344
345
        if ($loop_time = $this->getLoopTime()) {
346
            return $this->handleGetUpdatesLoop($loop_time, $this->getLoopInterval());
347
        }
348
349
        return $this->handleGetUpdates();
350
    }
351
352
    /**
353
     * Handle cron.
354
     *
355
     * @return \TelegramBot\TelegramBotManager\BotManager
356
     * @throws \Longman\TelegramBot\Exception\TelegramException
357
     */
358
    public function handleCron(): self
359
    {
360
        $groups = explode(',', $this->params->getScriptParam('g', 'default'));
361
362
        $commands = [];
363
        foreach ($groups as $group) {
364
            $commands[] = $this->params->getBotParam('cron.groups.' . $group, []);
365
        }
366
        $this->telegram->runCommands(array_merge(...$commands));
367
368
        return $this;
369
    }
370
371
    /**
372
     * Get the number of seconds the script should loop.
373
     *
374
     * @return int
375
     */
376 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...
377
    {
378
        $loop_time = $this->params->getScriptParam('l');
379
380
        if (null === $loop_time) {
381
            return 0;
382
        }
383
384
        if (is_string($loop_time) && '' === trim($loop_time)) {
385
            return 604800; // Default to 7 days.
386
        }
387
388
        return max(0, (int) $loop_time);
389
    }
390
391
    /**
392
     * Get the number of seconds the script should wait after each getUpdates request.
393
     *
394
     * @return int
395
     */
396 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...
397
    {
398
        $interval_time = $this->params->getScriptParam('i');
399
400
        if (null === $interval_time || (is_string($interval_time) && '' === trim($interval_time))) {
401
            return 2;
402
        }
403
404
        // Minimum interval is 1 second.
405
        return max(1, (int) $interval_time);
406
    }
407
408
    /**
409
     * Loop the getUpdates method for the passed amount of seconds.
410
     *
411
     * @param int $loop_time_in_seconds
412
     * @param int $loop_interval_in_seconds
413
     *
414
     * @return \TelegramBot\TelegramBotManager\BotManager
415
     * @throws \Longman\TelegramBot\Exception\TelegramException
416
     */
417
    public function handleGetUpdatesLoop(int $loop_time_in_seconds, int $loop_interval_in_seconds = 2): self
418
    {
419
        // Remember the time we started this loop.
420
        $now = time();
421
422
        $this->handleOutput('Looping getUpdates until ' . date('Y-m-d H:i:s', $now + $loop_time_in_seconds) . PHP_EOL);
423
424
        while ($now > time() - $loop_time_in_seconds) {
425
            $this->handleGetUpdates();
426
427
            // Chill a bit.
428
            sleep($loop_interval_in_seconds);
429
        }
430
431
        return $this;
432
    }
433
434
    /**
435
     * Handle the updates using the getUpdates method.
436
     *
437
     * @return \TelegramBot\TelegramBotManager\BotManager
438
     * @throws \Longman\TelegramBot\Exception\TelegramException
439
     */
440
    public function handleGetUpdates(): self
441
    {
442
        $output = date('Y-m-d H:i:s', time()) . ' - ';
443
444
        $response = $this->telegram->handleGetUpdates();
445
        if ($response->isOk()) {
446
            $results = array_filter((array) $response->getResult());
447
448
            $output .= sprintf('Updates processed: %d' . PHP_EOL, count($results));
449
450
            /** @var Entities\Update $result */
451
            foreach ($results as $result) {
452
                $chat_id = 0;
453
                $text    = 'Nothing';
454
455
                $update_content = $result->getUpdateContent();
456
                if ($update_content instanceof Entities\Message) {
457
                    $chat_id = $update_content->getFrom()->getId();
458
                    $text    = $update_content->getText();
459
                } elseif ($update_content instanceof Entities\InlineQuery ||
460
                          $update_content instanceof Entities\ChosenInlineResult
461
                ) {
462
                    $chat_id = $update_content->getFrom()->getId();
463
                    $text    = $update_content->getQuery();
464
                }
465
466
                $output .= sprintf(
467
                    '%d: %s' . PHP_EOL,
468
                    $chat_id,
469
                    preg_replace('/\s+/', ' ', trim($text))
470
                );
471
            }
472
        } else {
473
            $output .= sprintf('Failed to fetch updates: %s' . PHP_EOL, $response->printError());
474
        }
475
476
        $this->handleOutput($output);
477
478
        return $this;
479
    }
480
481
    /**
482
     * Handle the updates using the Webhook method.
483
     *
484
     * @return \TelegramBot\TelegramBotManager\BotManager
485
     * @throws \Longman\TelegramBot\Exception\TelegramException
486
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
487
     */
488
    public function handleWebhook(): self
489
    {
490
        $this->telegram->handle();
491
492
        return $this;
493
    }
494
495
    /**
496
     * Return the current test output and clear it.
497
     *
498
     * @return string
499
     */
500
    public function getOutput(): string
501
    {
502
        $output       = $this->output;
503
        $this->output = '';
504
505
        return $output;
506
    }
507
508
    /**
509
     * Check if this is a valid request coming from a Telegram API IP address.
510
     *
511
     * @link https://core.telegram.org/bots/webhooks#the-short-version
512
     *
513
     * @return bool
514
     */
515
    public function isValidRequest(): bool
516
    {
517
        // If we're running from CLI, requests are always valid, unless we're running the tests.
518
        if ((!self::inTest() && 'cli' === PHP_SAPI) || false === $this->params->getBotParam('validate_request')) {
519
            return true;
520
        }
521
522
        $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
523
        foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR'] as $key) {
524
            if (filter_var($_SERVER[$key] ?? null, FILTER_VALIDATE_IP)) {
525
                $ip = $_SERVER[$key];
526
                break;
527
            }
528
        }
529
530
        return Ip::match($ip, array_merge(
531
            [self::TELEGRAM_IP_RANGE],
532
            (array) $this->params->getBotParam('valid_ips', [])
533
        ));
534
    }
535
536
    /**
537
     * Make sure this is a valid request.
538
     *
539
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
540
     */
541
    private function validateRequest()
542
    {
543
        if (!$this->isValidRequest()) {
544
            throw new InvalidAccessException('Invalid access');
545
        }
546
    }
547
}
548