Completed
Push — master ( 285a64...6e54a8 )
by Armando
02:10 queued 01:11
created

BotManager::handleGetUpdates()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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