Completed
Push — develop ( 09e93c...da9458 )
by Armando
03:43
created

BotManager::handleOutput()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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