Completed
Pull Request — master (#31)
by Armando
05:25 queued 02:29
created

BotManager::validateSecret()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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