Completed
Push — develop ( eb794c...9fedc4 )
by Armando
01:41 queued 01:34
created

BotManager::handleWebhook()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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