Completed
Push — master ( eb794c...285a64 )
by Armando
11s
created

BotManager::validateAndSetWebhook()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 36
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 36
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 23
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 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
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
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