Passed
Pull Request — develop (#49)
by Armando
02:33
created

BotManager::isValidRequest()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
dl 0
loc 18
c 1
b 0
f 0
rs 9.2222
cc 6
nc 4
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 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 array Telegram webhook servers IP ranges
25
     * @link https://core.telegram.org/bots/webhooks#the-short-version
26
     */
27
    public const TELEGRAM_IP_RANGES = ['149.154.160.0/20', '91.108.4.0/22'];
28
29
    /**
30
     * @var string The output for testing, instead of echoing
31
     */
32
    private $output = '';
33
34
    /**
35
     * @var \Longman\TelegramBot\Telegram
36
     */
37
    private $telegram;
38
39
    /**
40
     * @var \TelegramBot\TelegramBotManager\Params Object that manages the parameters.
41
     */
42
    private $params;
43
44
    /**
45
     * @var \TelegramBot\TelegramBotManager\Action Object that contains the current action.
46
     */
47
    private $action;
48
49
    /**
50
     * @var callable
51
     */
52
    private $custom_get_updates_callback;
53
54
    /**
55
     * BotManager constructor.
56
     *
57
     * @param array $params
58
     *
59
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidParamsException
60
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidActionException
61
     * @throws \Longman\TelegramBot\Exception\TelegramException
62
     * @throws \Exception
63
     */
64
    public function __construct(array $params)
65
    {
66
        // Initialise logging before anything else, to allow errors to be logged.
67
        $this->initLogging($params['logging'] ?? []);
68
69
        $this->params = new Params($params);
70
        $this->action = new Action($this->params->getScriptParam('a'));
71
72
        // Set up a new Telegram instance.
73
        $this->telegram = new Telegram(
74
            $this->params->getBotParam('api_key'),
75
            $this->params->getBotParam('bot_username')
76
        );
77
    }
78
79
    /**
80
     * Check if we're busy running the PHPUnit tests.
81
     *
82
     * @return bool
83
     */
84
    public static function inTest(): bool
85
    {
86
        return \defined('PHPUNIT_TEST') && PHPUNIT_TEST === true;
0 ignored issues
show
Bug introduced by
The constant TelegramBot\TelegramBotManager\PHPUNIT_TEST was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
87
    }
88
89
    /**
90
     * Return the Telegram object.
91
     *
92
     * @return \Longman\TelegramBot\Telegram
93
     */
94
    public function getTelegram(): Telegram
95
    {
96
        return $this->telegram;
97
    }
98
99
    /**
100
     * Get the Params object.
101
     *
102
     * @return \TelegramBot\TelegramBotManager\Params
103
     */
104
    public function getParams(): Params
105
    {
106
        return $this->params;
107
    }
108
109
    /**
110
     * Get the Action object.
111
     *
112
     * @return \TelegramBot\TelegramBotManager\Action
113
     */
114
    public function getAction(): Action
115
    {
116
        return $this->action;
117
    }
118
119
    /**
120
     * Run this thing in all its glory!
121
     *
122
     * @return \TelegramBot\TelegramBotManager\BotManager
123
     * @throws \Longman\TelegramBot\Exception\TelegramException
124
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
125
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidWebhookException
126
     * @throws \Exception
127
     */
128
    public function run(): self
129
    {
130
        // Make sure this is a valid call.
131
        $this->validateSecret();
132
        $this->validateRequest();
133
134
        if ($this->action->isAction('webhookinfo')) {
135
            $webhookinfo = Request::getWebhookInfo();
136
            /** @noinspection ForgottenDebugOutputInspection */
137
            print_r($webhookinfo->getResult() ?: $webhookinfo->printError(true));
138
            return $this;
139
        }
140
        if ($this->action->isAction(['set', 'unset', 'reset'])) {
141
            return $this->validateAndSetWebhook();
142
        }
143
144
        $this->setBotExtras();
145
146
        if ($this->action->isAction('handle')) {
147
            $this->handleRequest();
148
        } elseif ($this->action->isAction('cron')) {
149
            $this->handleCron();
150
        }
151
152
        return $this;
153
    }
154
155
    /**
156
     * Initialise all loggers.
157
     *
158
     * @param array $log_paths
159
     *
160
     * @return \TelegramBot\TelegramBotManager\BotManager
161
     * @throws \Exception
162
     */
163
    public function initLogging(array $log_paths): self
164
    {
165
        foreach ($log_paths as $logger => $logfile) {
166
            ('debug' === $logger) && TelegramLog::initDebugLog($logfile);
167
            ('error' === $logger) && TelegramLog::initErrorLog($logfile);
168
            ('update' === $logger) && TelegramLog::initUpdateLog($logfile);
169
        }
170
171
        return $this;
172
    }
173
174
    /**
175
     * Make sure the passed secret is valid.
176
     *
177
     * @param bool $force Force validation, even on CLI.
178
     *
179
     * @return \TelegramBot\TelegramBotManager\BotManager
180
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
181
     */
182
    public function validateSecret(bool $force = false): self
183
    {
184
        // If we're running from CLI, secret isn't necessary.
185
        if ($force || 'cli' !== PHP_SAPI) {
186
            $secret     = $this->params->getBotParam('secret');
187
            $secret_get = $this->params->getScriptParam('s');
188
            if (!isset($secret, $secret_get) || $secret !== $secret_get) {
189
                throw new InvalidAccessException('Invalid access');
190
            }
191
        }
192
193
        return $this;
194
    }
195
196
    /**
197
     * Make sure the webhook is valid and perform the requested webhook operation.
198
     *
199
     * @return \TelegramBot\TelegramBotManager\BotManager
200
     * @throws \Longman\TelegramBot\Exception\TelegramException
201
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidWebhookException
202
     */
203
    public function validateAndSetWebhook(): self
204
    {
205
        $webhook = $this->params->getBotParam('webhook');
206
        if (empty($webhook['url'] ?? null) && $this->action->isAction(['set', 'reset'])) {
207
            throw new InvalidWebhookException('Invalid webhook');
208
        }
209
210
        if ($this->action->isAction(['unset', 'reset'])) {
211
            $this->handleOutput($this->telegram->deleteWebhook()->getDescription() . PHP_EOL);
212
            // When resetting the webhook, sleep for a bit to prevent too many requests.
213
            $this->action->isAction('reset') && sleep(1);
214
        }
215
216
        if ($this->action->isAction(['set', 'reset'])) {
217
            $webhook_params = array_filter([
218
                'certificate'     => $webhook['certificate'] ?? null,
219
                'max_connections' => $webhook['max_connections'] ?? null,
220
                'allowed_updates' => $webhook['allowed_updates'] ?? null,
221
            ], function ($v, $k) {
222
                if ($k === 'allowed_updates') {
223
                    // Special case for allowed_updates, which can be an empty array.
224
                    return \is_array($v);
225
                }
226
                return !empty($v);
227
            }, ARRAY_FILTER_USE_BOTH);
228
229
            $this->handleOutput(
230
                $this->telegram->setWebhook(
231
                    $webhook['url'] . '?a=handle&s=' . $this->params->getBotParam('secret'),
232
                    $webhook_params
233
                )->getDescription() . PHP_EOL
234
            );
235
        }
236
237
        return $this;
238
    }
239
240
    /**
241
     * Save the test output and echo it if we're not in a test.
242
     *
243
     * @param string $output
244
     *
245
     * @return \TelegramBot\TelegramBotManager\BotManager
246
     */
247
    private function handleOutput(string $output): self
248
    {
249
        $this->output .= $output;
250
251
        if (!self::inTest()) {
252
            echo $output;
253
        }
254
255
        return $this;
256
    }
257
258
    /**
259
     * Set any extra bot features that have been assigned on construction.
260
     *
261
     * @return \TelegramBot\TelegramBotManager\BotManager
262
     * @throws \Longman\TelegramBot\Exception\TelegramException
263
     */
264
    public function setBotExtras(): self
265
    {
266
        $this->setBotExtrasTelegram();
267
        $this->setBotExtrasRequest();
268
269
        return $this;
270
    }
271
272
    /**
273
     * Set extra bot parameters for Telegram object.
274
     *
275
     * @return \TelegramBot\TelegramBotManager\BotManager
276
     * @throws \Longman\TelegramBot\Exception\TelegramException
277
     */
278
    protected function setBotExtrasTelegram(): self
279
    {
280
        $simple_extras = [
281
            'admins'         => 'enableAdmins',
282
            'commands.paths' => 'addCommandsPaths',
283
            'custom_input'   => 'setCustomInput',
284
            'paths.download' => 'setDownloadPath',
285
            'paths.upload'   => 'setUploadPath',
286
        ];
287
        // For simple telegram extras, just pass the single param value to the Telegram method.
288
        foreach ($simple_extras as $param_key => $method) {
289
            $param = $this->params->getBotParam($param_key);
290
            if (null !== $param) {
291
                $this->telegram->$method($param);
292
            }
293
        }
294
295
        // Database.
296
        if ($mysql_config = $this->params->getBotParam('mysql', [])) {
297
            $this->telegram->enableMySql(
298
                $mysql_config,
299
                $mysql_config['table_prefix'] ?? null,
300
                $mysql_config['encoding'] ?? 'utf8mb4'
301
            );
302
        }
303
304
        // Custom command configs.
305
        $command_configs = $this->params->getBotParam('commands.configs', []);
306
        foreach ($command_configs as $command => $config) {
307
            $this->telegram->setCommandConfig($command, $config);
308
        }
309
310
        // Botan with options.
311
        if ($botan_token = $this->params->getBotParam('botan.token')) {
312
            $botan_options = $this->params->getBotParam('botan.options', []);
313
            $this->telegram->enableBotan($botan_token, $botan_options);
0 ignored issues
show
Deprecated Code introduced by
The function Longman\TelegramBot\Telegram::enableBotan() has been deprecated: Botan.io service is no longer working ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

313
            /** @scrutinizer ignore-deprecated */ $this->telegram->enableBotan($botan_token, $botan_options);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
314
        }
315
316
        return $this;
317
    }
318
319
    /**
320
     * Set extra bot parameters for Request class.
321
     *
322
     * @return \TelegramBot\TelegramBotManager\BotManager
323
     * @throws \Longman\TelegramBot\Exception\TelegramException
324
     */
325
    protected function setBotExtrasRequest(): self
326
    {
327
        $request_extras = [
328
            // None at the moment...
329
        ];
330
        // For request extras, just pass the single param value to the Request method.
331
        foreach ($request_extras as $param_key => $method) {
332
            $param = $this->params->getBotParam($param_key);
333
            if (null !== $param) {
334
                Request::$method($param);
335
            }
336
        }
337
338
        // Special cases.
339
        $limiter_enabled = $this->params->getBotParam('limiter.enabled');
340
        if ($limiter_enabled !== null) {
341
            $limiter_options = $this->params->getBotParam('limiter.options', []);
342
            Request::setLimiter($limiter_enabled, $limiter_options);
343
        }
344
345
        return $this;
346
    }
347
348
    /**
349
     * Handle the request, which calls either the Webhook or getUpdates method respectively.
350
     *
351
     * @return \TelegramBot\TelegramBotManager\BotManager
352
     * @throws \Longman\TelegramBot\Exception\TelegramException
353
     */
354
    public function handleRequest(): self
355
    {
356
        if ($this->params->getBotParam('webhook.url')) {
357
            return $this->handleWebhook();
358
        }
359
360
        if ($loop_time = $this->getLoopTime()) {
361
            return $this->handleGetUpdatesLoop($loop_time, $this->getLoopInterval());
362
        }
363
364
        return $this->handleGetUpdates();
365
    }
366
367
    /**
368
     * Handle cron.
369
     *
370
     * @return \TelegramBot\TelegramBotManager\BotManager
371
     * @throws \Longman\TelegramBot\Exception\TelegramException
372
     */
373
    public function handleCron(): self
374
    {
375
        $groups = explode(',', $this->params->getScriptParam('g', 'default'));
376
377
        $commands = [];
378
        foreach ($groups as $group) {
379
            $commands[] = $this->params->getBotParam('cron.groups.' . $group, []);
380
        }
381
        $this->telegram->runCommands(array_merge(...$commands));
382
383
        return $this;
384
    }
385
386
    /**
387
     * Get the number of seconds the script should loop.
388
     *
389
     * @return int
390
     */
391
    public function getLoopTime(): int
392
    {
393
        $loop_time = $this->params->getScriptParam('l');
394
395
        if (null === $loop_time) {
396
            return 0;
397
        }
398
399
        if (\is_string($loop_time) && '' === trim($loop_time)) {
400
            return 604800; // Default to 7 days.
401
        }
402
403
        return max(0, (int) $loop_time);
404
    }
405
406
    /**
407
     * Get the number of seconds the script should wait after each getUpdates request.
408
     *
409
     * @return int
410
     */
411
    public function getLoopInterval(): int
412
    {
413
        $interval_time = $this->params->getScriptParam('i');
414
415
        if (null === $interval_time || (\is_string($interval_time) && '' === trim($interval_time))) {
416
            return 2;
417
        }
418
419
        // Minimum interval is 1 second.
420
        return max(1, (int) $interval_time);
421
    }
422
423
    /**
424
     * Loop the getUpdates method for the passed amount of seconds.
425
     *
426
     * @param int $loop_time_in_seconds
427
     * @param int $loop_interval_in_seconds
428
     *
429
     * @return \TelegramBot\TelegramBotManager\BotManager
430
     * @throws \Longman\TelegramBot\Exception\TelegramException
431
     */
432
    public function handleGetUpdatesLoop(int $loop_time_in_seconds, int $loop_interval_in_seconds = 2): self
433
    {
434
        // Remember the time we started this loop.
435
        $now = time();
436
437
        $this->handleOutput('Looping getUpdates until ' . date('Y-m-d H:i:s', $now + $loop_time_in_seconds) . PHP_EOL);
438
439
        while ($now > time() - $loop_time_in_seconds) {
440
            $this->handleGetUpdates();
441
442
            // Chill a bit.
443
            sleep($loop_interval_in_seconds);
444
        }
445
446
        return $this;
447
    }
448
449
    /**
450
     * Set a custom callback for handling the output of the getUpdates results.
451
     *
452
     * @param callable $callback
453
     *
454
     * @return \TelegramBot\TelegramBotManager\BotManager
455
     */
456
    public function setCustomGetUpdatesCallback(callable $callback): BotManager
457
    {
458
        $this->custom_get_updates_callback = $callback;
459
        return $this;
460
    }
461
462
    /**
463
     * Handle the updates using the getUpdates method.
464
     *
465
     * @return \TelegramBot\TelegramBotManager\BotManager
466
     * @throws \Longman\TelegramBot\Exception\TelegramException
467
     */
468
    public function handleGetUpdates(): self
469
    {
470
        $get_updates_response = $this->telegram->handleGetUpdates();
471
472
        // Check if the user has set a custom callback for handling the response.
473
        if ($this->custom_get_updates_callback !== null) {
474
            $this->handleOutput(\call_user_func($this->custom_get_updates_callback, $get_updates_response));
475
        } else {
476
            $this->handleOutput($this->defaultGetUpdatesCallback($get_updates_response));
477
        }
478
479
        return $this;
480
    }
481
482
    /**
483
     * Return the default output for getUpdates handling.
484
     *
485
     * @param Entities\ServerResponse $get_updates_response
486
     *
487
     * @return string
488
     */
489
    protected function defaultGetUpdatesCallback($get_updates_response): string
490
    {
491
        if (!$get_updates_response->isOk()) {
492
            return sprintf(
493
                '%s - Failed to fetch updates' . PHP_EOL . '%s',
494
                date('Y-m-d H:i:s'),
495
                $get_updates_response->printError(true)
496
            );
497
        }
498
499
        /** @var Entities\Update[] $results */
500
        $results = array_filter((array) $get_updates_response->getResult());
501
502
        $output = sprintf(
503
            '%s - Updates processed: %d' . PHP_EOL,
504
            date('Y-m-d H:i:s'),
505
            count($results)
506
        );
507
508
        foreach ($results as $result) {
509
            $chat_id = 0;
510
            $text    = '<n/a>';
511
512
            $update_content = $result->getUpdateContent();
513
            if ($update_content instanceof Entities\Message) {
514
                $chat_id = $update_content->getChat()->getId();
515
                $text    = sprintf('<%s>', $update_content->getType());
516
            } elseif ($update_content instanceof Entities\InlineQuery ||
517
                      $update_content instanceof Entities\ChosenInlineResult
518
            ) {
519
                $chat_id = $update_content->getFrom()->getId();
520
                $text    = sprintf('<query> %s', $update_content->getQuery());
521
            }
522
523
            $output .= sprintf(
524
                '%d: %s' . PHP_EOL,
525
                $chat_id,
526
                preg_replace('/\s+/', ' ', trim($text))
527
            );
528
        }
529
530
        return $output;
531
    }
532
533
    /**
534
     * Handle the updates using the Webhook method.
535
     *
536
     * @return \TelegramBot\TelegramBotManager\BotManager
537
     * @throws \Longman\TelegramBot\Exception\TelegramException
538
     */
539
    public function handleWebhook(): self
540
    {
541
        $this->telegram->handle();
542
543
        return $this;
544
    }
545
546
    /**
547
     * Return the current test output and clear it.
548
     *
549
     * @return string
550
     */
551
    public function getOutput(): string
552
    {
553
        $output       = $this->output;
554
        $this->output = '';
555
556
        return $output;
557
    }
558
559
    /**
560
     * Check if this is a valid request coming from a Telegram API IP address.
561
     *
562
     * @link https://core.telegram.org/bots/webhooks#the-short-version
563
     *
564
     * @return bool
565
     */
566
    public function isValidRequest(): bool
567
    {
568
        // If we're running from CLI, requests are always valid, unless we're running the tests.
569
        if ((!self::inTest() && 'cli' === PHP_SAPI) || false === $this->params->getBotParam('validate_request')) {
570
            return true;
571
        }
572
573
        $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
574
        foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR'] as $key) {
575
            if (filter_var($_SERVER[$key] ?? null, FILTER_VALIDATE_IP)) {
576
                $ip = $_SERVER[$key];
577
                break;
578
            }
579
        }
580
581
        return Ip::match($ip, array_merge(
582
            self::TELEGRAM_IP_RANGES,
583
            (array) $this->params->getBotParam('valid_ips', [])
584
        ));
585
    }
586
587
    /**
588
     * Make sure this is a valid request.
589
     *
590
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
591
     */
592
    private function validateRequest()
593
    {
594
        if (!$this->isValidRequest()) {
595
            throw new InvalidAccessException('Invalid access');
596
        }
597
    }
598
}
599