Completed
Push — master ( d3f720...c8e85b )
by Armando
03:18
created

BotManager   C

Complexity

Total Complexity 63

Size/Duplication

Total Lines 453
Duplicated Lines 5.52 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
wmc 63
lcom 1
cbo 7
dl 25
loc 453
rs 5.8893
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
A inTest() 0 4 2
A getTelegram() 0 4 1
A getParams() 0 4 1
A getAction() 0 4 1
A run() 0 18 3
B initLogging() 0 14 6
A validateSecret() 0 13 4
B validateAndSetWebhook() 0 30 6
A handleOutput() 0 10 2
C setBotExtras() 0 40 7
A handleRequest() 0 14 3
A getLoopTime() 14 14 4
A getLoopInterval() 11 11 4
A handleGetUpdatesLoop() 0 16 2
B handleGetUpdates() 0 38 6
A handleWebhook() 0 10 2
A getOutput() 0 7 1
B isValidRequest() 0 21 7

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 NPM\TelegramBotManager;
12
13
use Longman\TelegramBot\Entities;
14
use Longman\TelegramBot\Request;
15
use Longman\TelegramBot\Telegram;
16
use Longman\TelegramBot\TelegramLog;
17
18
/**
19
 * Class BotManager.php
20
 *
21
 * Leave all member variables public to allow easy modification.
22
 *
23
 * @package NPM\TelegramBotManager
24
 */
25
class BotManager
26
{
27
    /**
28
     * @var string Telegram post servers lower IP limit
29
     */
30
    const TELEGRAM_IP_LOWER = '149.154.167.197';
31
32
    /**
33
     * @var string Telegram post servers upper IP limit
34
     */
35
    const TELEGRAM_IP_UPPER = '149.154.167.233';
36
37
    /**
38
     * @var string The output for testing, instead of echoing
39
     */
40
    private $output = '';
41
42
    /**
43
     * @var \Longman\TelegramBot\Telegram
44
     */
45
    private $telegram;
46
47
    /**
48
     * @var \NPM\TelegramBotManager\Params Object that manages the parameters.
49
     */
50
    private $params;
51
52
    /**
53
     * @var \NPM\TelegramBotManager\Action Object that contains the current action.
54
     */
55
    private $action;
56
57
    /**
58
     * BotManager constructor.
59
     *
60
     * @param array $params
61
     *
62
     * @throws \InvalidArgumentException
63
     */
64
    public function __construct(array $params)
65
    {
66
        $this->params = new Params($params);
67
        $this->action = new Action($this->params->getScriptParam('a'));
68
69
        // Set up a new Telegram instance.
70
        $this->telegram = new Telegram(
71
            $this->params->getBotParam('api_key'),
72
            $this->params->getBotParam('botname')
73
        );
74
    }
75
76
    /**
77
     * Check if we're busy running the PHPUnit tests.
78
     *
79
     * @return bool
80
     */
81
    public static function inTest(): bool
82
    {
83
        return defined('PHPUNIT_TEST') && PHPUNIT_TEST === true;
84
    }
85
86
    /**
87
     * Return the Telegram object.
88
     *
89
     * @return \Longman\TelegramBot\Telegram
90
     */
91
    public function getTelegram(): Telegram
92
    {
93
        return $this->telegram;
94
    }
95
96
    /**
97
     * Get the Params object.
98
     *
99
     * @return \NPM\TelegramBotManager\Params
100
     */
101
    public function getParams(): Params
102
    {
103
        return $this->params;
104
    }
105
106
    /**
107
     * Get the Action object.
108
     *
109
     * @return \NPM\TelegramBotManager\Action
110
     */
111
    public function getAction(): Action
112
    {
113
        return $this->action;
114
    }
115
116
    /**
117
     * Run this thing in all its glory!
118
     *
119
     * @return \NPM\TelegramBotManager\BotManager
120
     * @throws \Longman\TelegramBot\Exception\TelegramException
121
     * @throws \InvalidArgumentException
122
     * @throws \Exception
123
     */
124
    public function run(): self
125
    {
126
        // Initialise logging.
127
        $this->initLogging();
128
129
        // Make sure this is a valid call.
130
        $this->validateSecret();
131
132
        if ($this->action->isAction(['set', 'unset', 'reset'])) {
133
            $this->validateAndSetWebhook();
134
        } elseif ($this->action->isAction('handle')) {
135
            // Set any extras.
136
            $this->setBotExtras();
137
            $this->handleRequest();
138
        }
139
140
        return $this;
141
    }
142
143
    /**
144
     * Initialise all loggers.
145
     *
146
     * @return \NPM\TelegramBotManager\BotManager
147
     * @throws \Exception
148
     */
149
    public function initLogging(): self
150
    {
151
        $logging = $this->params->getBotParam('logging');
152
        if (is_array($logging)) {
153
            /** @var array $logging */
154
            foreach ($logging as $logger => $logfile) {
155
                ('debug' === $logger) && TelegramLog::initDebugLog($logfile);
156
                ('error' === $logger) && TelegramLog::initErrorLog($logfile);
157
                ('update' === $logger) && TelegramLog::initUpdateLog($logfile);
158
            }
159
        }
160
161
        return $this;
162
    }
163
164
    /**
165
     * Make sure the passed secret is valid.
166
     *
167
     * @param bool $force Force validation, even on CLI.
168
     *
169
     * @return \NPM\TelegramBotManager\BotManager
170
     * @throws \InvalidArgumentException
171
     */
172
    public function validateSecret(bool $force = false): self
173
    {
174
        // If we're running from CLI, secret isn't necessary.
175
        if ($force || 'cli' !== PHP_SAPI) {
176
            $secret    = $this->params->getBotParam('secret');
177
            $secretGet = $this->params->getScriptParam('s');
178
            if ($secretGet !== $secret) {
179
                throw new \InvalidArgumentException('Invalid access');
180
            }
181
        }
182
183
        return $this;
184
    }
185
186
    /**
187
     * Make sure the webhook is valid and perform the requested webhook operation.
188
     *
189
     * @return \NPM\TelegramBotManager\BotManager
190
     * @throws \Longman\TelegramBot\Exception\TelegramException
191
     * @throws \InvalidArgumentException
192
     */
193
    public function validateAndSetWebhook(): self
194
    {
195
        $webhook = $this->params->getBotParam('webhook');
196
        if (empty($webhook) && $this->action->isAction(['set', 'reset'])) {
197
            throw new \InvalidArgumentException('Invalid webhook');
198
        }
199
200
        if ($this->action->isAction(['unset', 'reset'])) {
201
            $this->handleOutput($this->telegram->deleteWebhook()->getDescription() . PHP_EOL);
202
            // When resetting the webhook, sleep for a bit to prevent too many requests.
203
            $this->action->isAction('reset') && sleep(1);
204
        }
205
206
        if ($this->action->isAction(['set', 'reset'])) {
207
            $webhook_params = array_filter([
208
                'certificate'     => $this->params->getBotParam('certificate'),
209
                'max_connections' => $this->params->getBotParam('max_connections'),
210
                'allowed_updates' => $this->params->getBotParam('allowed_updates'),
211
            ]);
212
213
            $this->handleOutput(
214
                $this->telegram->setWebhook(
215
                    $webhook . '?a=handle&s=' . $this->params->getBotParam('secret'),
216
                    $webhook_params
217
                )->getDescription() . PHP_EOL
218
            );
219
        }
220
221
        return $this;
222
    }
223
224
    /**
225
     * Save the test output and echo it if we're not in a test.
226
     *
227
     * @param string $output
228
     *
229
     * @return \NPM\TelegramBotManager\BotManager
230
     */
231
    private function handleOutput($output): self
232
    {
233
        $this->output .= $output;
234
235
        if (!self::inTest()) {
236
            echo $output;
237
        }
238
239
        return $this;
240
    }
241
242
    /**
243
     * Set any extra bot features that have been assigned on construction.
244
     *
245
     * @return \NPM\TelegramBotManager\BotManager
246
     */
247
    public function setBotExtras(): self
248
    {
249
        $telegram_extras = [
250
            'admins'         => 'enableAdmins',
251
            'mysql'          => 'enableMySql',
252
            'botan_token'    => 'enableBotan',
253
            'commands_paths' => 'addCommandsPaths',
254
            'custom_input'   => 'setCustomInput',
255
            'download_path'  => 'setDownloadPath',
256
            'upload_path'    => 'setUploadPath',
257
        ];
258
        // For telegram extras, just pass the single param value to the Telegram method.
259
        foreach ($telegram_extras as $param_key => $method) {
260
            $param = $this->params->getBotParam($param_key);
261
            if (null !== $param) {
262
                $this->telegram->$method($param);
263
            }
264
        }
265
266
        $request_extras = [
267
            'limiter' => 'setLimiter',
268
        ];
269
        // For request extras, just pass the single param value to the Request method.
270
        foreach ($request_extras as $param_key => $method) {
271
            $param = $this->params->getBotParam($param_key);
272
            if (null !== $param) {
273
                Request::$method($param);
274
            }
275
        }
276
277
        $command_configs = $this->params->getBotParam('command_configs');
278
        if (is_array($command_configs)) {
279
            /** @var array $command_configs */
280
            foreach ($command_configs as $command => $config) {
281
                $this->telegram->setCommandConfig($command, $config);
282
            }
283
        }
284
285
        return $this;
286
    }
287
288
    /**
289
     * Handle the request, which calls either the Webhook or getUpdates method respectively.
290
     *
291
     * @return \NPM\TelegramBotManager\BotManager
292
     * @throws \Longman\TelegramBot\Exception\TelegramException
293
     * @throws \Exception
294
     */
295
    public function handleRequest(): self
296
    {
297
        if (empty($this->params->getBotParam('webhook'))) {
298
            if ($loop_time = $this->getLoopTime()) {
299
                $this->handleGetUpdatesLoop($loop_time, $this->getLoopInterval());
300
            } else {
301
                $this->handleGetUpdates();
302
            }
303
        } else {
304
            $this->handleWebhook();
305
        }
306
307
        return $this;
308
    }
309
310
    /**
311
     * Get the number of seconds the script should loop.
312
     *
313
     * @return int
314
     */
315 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...
316
    {
317
        $loop_time = $this->params->getScriptParam('l');
318
319
        if (null === $loop_time) {
320
            return 0;
321
        }
322
323
        if (is_string($loop_time) && '' === trim($loop_time)) {
324
            return 604800; // Default to 7 days.
325
        }
326
327
        return max(0, (int)$loop_time);
328
    }
329
330
    /**
331
     * Get the number of seconds the script should wait after each getUpdates request.
332
     *
333
     * @return int
334
     */
335 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...
336
    {
337
        $interval_time = $this->params->getScriptParam('i');
338
339
        if (null === $interval_time || (is_string($interval_time) && '' === trim($interval_time))) {
340
            return 2;
341
        }
342
343
        // Minimum interval is 1 second.
344
        return max(1, (int)$interval_time);
345
    }
346
347
    /**
348
     * Loop the getUpdates method for the passed amount of seconds.
349
     *
350
     * @param int $loop_time_in_seconds
351
     * @param int $loop_interval_in_seconds
352
     *
353
     * @return \NPM\TelegramBotManager\BotManager
354
     * @throws \Longman\TelegramBot\Exception\TelegramException
355
     */
356
    public function handleGetUpdatesLoop(int $loop_time_in_seconds, int $loop_interval_in_seconds = 2): self
357
    {
358
        // Remember the time we started this loop.
359
        $now = time();
360
361
        $this->handleOutput('Looping getUpdates until ' . date('Y-m-d H:i:s', $now + $loop_time_in_seconds) . PHP_EOL);
362
363
        while ($now > time() - $loop_time_in_seconds) {
364
            $this->handleGetUpdates();
365
366
            // Chill a bit.
367
            sleep($loop_interval_in_seconds);
368
        }
369
370
        return $this;
371
    }
372
373
    /**
374
     * Handle the updates using the getUpdates method.
375
     *
376
     * @return \NPM\TelegramBotManager\BotManager
377
     * @throws \Longman\TelegramBot\Exception\TelegramException
378
     */
379
    public function handleGetUpdates(): self
380
    {
381
        $output = date('Y-m-d H:i:s', time()) . ' - ';
382
383
        $response = $this->telegram->handleGetUpdates();
384
        if ($response->isOk()) {
385
            $results = array_filter((array)$response->getResult());
386
387
            $output .= sprintf('Updates processed: %d' . PHP_EOL, count($results));
388
389
            /** @var Entities\Update $result */
390
            foreach ($results as $result) {
391
                $chat_id = 0;
392
                $text    = 'Nothing';
393
394
                $update_content = $result->getUpdateContent();
395
                if ($update_content instanceof Entities\Message) {
396
                    $chat_id = $update_content->getFrom()->getId();
397
                    $text    = $update_content->getText();
398
                } elseif ($update_content instanceof Entities\InlineQuery || $update_content instanceof Entities\ChosenInlineResult) {
399
                    $chat_id = $update_content->getFrom()->getId();
400
                    $text    = $update_content->getQuery();
401
                }
402
403
                $output .= sprintf(
404
                    '%d: %s' . PHP_EOL,
405
                    $chat_id,
406
                    preg_replace('/\s+/', ' ', trim($text))
407
                );
408
            }
409
        } else {
410
            $output .= sprintf('Failed to fetch updates: %s' . PHP_EOL, $response->printError());
411
        }
412
413
        $this->handleOutput($output);
414
415
        return $this;
416
    }
417
418
    /**
419
     * Handle the updates using the Webhook method.
420
     *
421
     * @return \NPM\TelegramBotManager\BotManager
422
     * @throws \Longman\TelegramBot\Exception\TelegramException
423
     * @throws \Exception
424
     */
425
    public function handleWebhook(): self
426
    {
427
        if (!$this->isValidRequest()) {
428
            throw new \Exception('Invalid access');
429
        }
430
431
        $this->telegram->handle();
432
433
        return $this;
434
    }
435
436
    /**
437
     * Return the current test output and clear it.
438
     *
439
     * @return string
440
     */
441
    public function getOutput(): string
442
    {
443
        $output       = $this->output;
444
        $this->output = '';
445
446
        return $output;
447
    }
448
449
    /**
450
     * Check if this is a valid request coming from a Telegram API IP address.
451
     *
452
     * @link https://core.telegram.org/bots/webhooks#the-short-version
453
     *
454
     * @return bool
455
     */
456
    public function isValidRequest(): bool
457
    {
458
        if ((!self::inTest() && 'cli' === PHP_SAPI) || false === $this->params->getBotParam('validate_request')) {
459
            return true;
460
        }
461
462
        $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
463
        foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR'] as $key) {
464
            $addr = $_SERVER[$key] ?? null;
465
            if (filter_var($addr, FILTER_VALIDATE_IP)) {
466
                $ip = $addr;
467
                break;
468
            }
469
        }
470
471
        $lower_dec = (float)sprintf('%u', ip2long(self::TELEGRAM_IP_LOWER));
472
        $upper_dec = (float)sprintf('%u', ip2long(self::TELEGRAM_IP_UPPER));
473
        $ip_dec    = (float)sprintf('%u', ip2long($ip));
474
475
        return $ip_dec >= $lower_dec && $ip_dec <= $upper_dec;
476
    }
477
}
478