Passed
Pull Request — master (#56)
by Marco
02:22
created

BotManager::validateAndSetWebhook()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 35
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 22
dl 0
loc 35
c 0
b 0
f 0
rs 8.6346
cc 7
nc 7
nop 0
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 PhpTelegramBot\PhpTelegramBotManager;
12
13
use Exception;
14
use Longman\IPTools\Ip;
15
use PhpTelegramBot\Core\Entities\CallbackQuery;
0 ignored issues
show
Bug introduced by
The type PhpTelegramBot\Core\Entities\CallbackQuery was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
16
use PhpTelegramBot\Core\Entities\ChosenInlineResult;
0 ignored issues
show
Bug introduced by
The type PhpTelegramBot\Core\Entities\ChosenInlineResult was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
17
use PhpTelegramBot\Core\Entities\InlineQuery;
0 ignored issues
show
Bug introduced by
The type PhpTelegramBot\Core\Entities\InlineQuery was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
18
use PhpTelegramBot\Core\Entities\Message;
0 ignored issues
show
Bug introduced by
The type PhpTelegramBot\Core\Entities\Message was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
19
use PhpTelegramBot\Core\Entities\ServerResponse;
0 ignored issues
show
Bug introduced by
The type PhpTelegramBot\Core\Entities\ServerResponse was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
20
use PhpTelegramBot\Core\Entities\Update;
0 ignored issues
show
Bug introduced by
The type PhpTelegramBot\Core\Entities\Update was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
21
use PhpTelegramBot\Core\Exception\TelegramException;
0 ignored issues
show
Bug introduced by
The type PhpTelegramBot\Core\Exception\TelegramException was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
22
use PhpTelegramBot\Core\Request;
0 ignored issues
show
Bug introduced by
The type PhpTelegramBot\Core\Request was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
23
use PhpTelegramBot\Core\Telegram;
0 ignored issues
show
Bug introduced by
The type PhpTelegramBot\Core\Telegram was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
24
use PhpTelegramBot\Core\TelegramLog;
0 ignored issues
show
Bug introduced by
The type PhpTelegramBot\Core\TelegramLog was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

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