Completed
Push — master ( bd324c...b4485a )
by Armando
13s
created

BotManager::handleGetUpdatesLoop()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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