Passed
Pull Request — develop (#50)
by Armando
01:20
created

BotManager::getOutput()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 6
c 0
b 0
f 0
rs 10
cc 1
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 array Telegram webhook servers IP ranges
25
     * @link https://core.telegram.org/bots/webhooks#the-short-version
26
     */
27
    public const TELEGRAM_IP_RANGES = ['149.154.160.0/20', '91.108.4.0/22'];
28
29
    /**
30
     * @var string The output for testing, instead of echoing
31
     */
32
    private $output = '';
33
34
    /**
35
     * @var \Longman\TelegramBot\Telegram
36
     */
37
    private $telegram;
38
39
    /**
40
     * @var \TelegramBot\TelegramBotManager\Params Object that manages the parameters.
41
     */
42
    private $params;
43
44
    /**
45
     * @var \TelegramBot\TelegramBotManager\Action Object that contains the current action.
46
     */
47
    private $action;
48
49
    /**
50
     * @var callable
51
     */
52
    private $custom_get_updates_callback;
53
54
    /**
55
     * BotManager constructor.
56
     *
57
     * @param array $params
58
     *
59
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidParamsException
60
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidActionException
61
     * @throws \Longman\TelegramBot\Exception\TelegramException
62
     * @throws \Exception
63
     */
64
    public function __construct(array $params)
65
    {
66
        // Initialise logging before anything else, to allow errors to be logged.
67
        $this->initLogging($params['logging'] ?? []);
68
69
        $this->params = new Params($params);
70
        $this->action = new Action($this->params->getScriptParam('a'));
71
72
        // Set up a new Telegram instance.
73
        $this->telegram = new Telegram(
74
            $this->params->getBotParam('api_key'),
75
            $this->params->getBotParam('bot_username')
76
        );
77
    }
78
79
    /**
80
     * Check if we're busy running the PHPUnit tests.
81
     *
82
     * @return bool
83
     */
84
    public static function inTest(): bool
85
    {
86
        return \defined('PHPUNIT_TESTSUITE') && PHPUNIT_TESTSUITE === true;
87
    }
88
89
    /**
90
     * Return the Telegram object.
91
     *
92
     * @return \Longman\TelegramBot\Telegram
93
     */
94
    public function getTelegram(): Telegram
95
    {
96
        return $this->telegram;
97
    }
98
99
    /**
100
     * Get the Params object.
101
     *
102
     * @return \TelegramBot\TelegramBotManager\Params
103
     */
104
    public function getParams(): Params
105
    {
106
        return $this->params;
107
    }
108
109
    /**
110
     * Get the Action object.
111
     *
112
     * @return \TelegramBot\TelegramBotManager\Action
113
     */
114
    public function getAction(): Action
115
    {
116
        return $this->action;
117
    }
118
119
    /**
120
     * Run this thing in all its glory!
121
     *
122
     * @return \TelegramBot\TelegramBotManager\BotManager
123
     * @throws \Longman\TelegramBot\Exception\TelegramException
124
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
125
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidWebhookException
126
     * @throws \Exception
127
     */
128
    public function run(): self
129
    {
130
        // Make sure this is a valid call.
131
        $this->validateSecret();
132
        $this->validateRequest();
133
134
        if ($this->action->isAction('webhookinfo')) {
135
            $webhookinfo = Request::getWebhookInfo();
136
            /** @noinspection ForgottenDebugOutputInspection */
137
            print_r($webhookinfo->getResult() ?: $webhookinfo->printError(true));
138
            return $this;
139
        }
140
        if ($this->action->isAction(['set', 'unset', 'reset'])) {
141
            return $this->validateAndSetWebhook();
142
        }
143
144
        $this->setBotExtras();
145
146
        if ($this->action->isAction('handle')) {
147
            $this->handleRequest();
148
        } elseif ($this->action->isAction('cron')) {
149
            $this->handleCron();
150
        }
151
152
        return $this;
153
    }
154
155
    /**
156
     * Initialise all loggers.
157
     *
158
     * @param array $log_paths
159
     *
160
     * @return \TelegramBot\TelegramBotManager\BotManager
161
     * @throws \Exception
162
     */
163
    public function initLogging(array $log_paths): self
164
    {
165
        (defined('PHPUNIT_TESTSUITE') && PHPUNIT_TESTSUITE) || trigger_error(__METHOD__ . ' is deprecated and will be removed soon. Initialise with a preconfigured logger instance instead using "TelegramLog::initialize($logger)".', E_USER_DEPRECATED);
166
167
        foreach ($log_paths as $logger => $logfile) {
168
            ('debug' === $logger) && TelegramLog::initDebugLog($logfile);
0 ignored issues
show
Deprecated Code introduced by
The function Longman\TelegramBot\TelegramLog::initDebugLog() has been deprecated: Initialise a preconfigured logger instance instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

168
            ('debug' === $logger) && /** @scrutinizer ignore-deprecated */ TelegramLog::initDebugLog($logfile);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
169
            ('error' === $logger) && TelegramLog::initErrorLog($logfile);
0 ignored issues
show
Deprecated Code introduced by
The function Longman\TelegramBot\TelegramLog::initErrorLog() has been deprecated: Initialise a preconfigured logger instance instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

169
            ('error' === $logger) && /** @scrutinizer ignore-deprecated */ TelegramLog::initErrorLog($logfile);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
170
            ('update' === $logger) && TelegramLog::initUpdateLog($logfile);
0 ignored issues
show
Deprecated Code introduced by
The function Longman\TelegramBot\TelegramLog::initUpdateLog() has been deprecated: Initialise a preconfigured logger instance instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

170
            ('update' === $logger) && /** @scrutinizer ignore-deprecated */ TelegramLog::initUpdateLog($logfile);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
171
        }
172
173
        return $this;
174
    }
175
176
    /**
177
     * Make sure the passed secret is valid.
178
     *
179
     * @param bool $force Force validation, even on CLI.
180
     *
181
     * @return \TelegramBot\TelegramBotManager\BotManager
182
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
183
     */
184
    public function validateSecret(bool $force = false): self
185
    {
186
        // If we're running from CLI, secret isn't necessary.
187
        if ($force || 'cli' !== PHP_SAPI) {
188
            $secret     = $this->params->getBotParam('secret');
189
            $secret_get = $this->params->getScriptParam('s');
190
            if (!isset($secret, $secret_get) || $secret !== $secret_get) {
191
                throw new InvalidAccessException('Invalid access');
192
            }
193
        }
194
195
        return $this;
196
    }
197
198
    /**
199
     * Make sure the webhook is valid and perform the requested webhook operation.
200
     *
201
     * @return \TelegramBot\TelegramBotManager\BotManager
202
     * @throws \Longman\TelegramBot\Exception\TelegramException
203
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidWebhookException
204
     */
205
    public function validateAndSetWebhook(): self
206
    {
207
        $webhook = $this->params->getBotParam('webhook');
208
        if (empty($webhook['url'] ?? null) && $this->action->isAction(['set', 'reset'])) {
209
            throw new InvalidWebhookException('Invalid webhook');
210
        }
211
212
        if ($this->action->isAction(['unset', 'reset'])) {
213
            $this->handleOutput($this->telegram->deleteWebhook()->getDescription() . PHP_EOL);
214
            // When resetting the webhook, sleep for a bit to prevent too many requests.
215
            $this->action->isAction('reset') && sleep(1);
216
        }
217
218
        if ($this->action->isAction(['set', 'reset'])) {
219
            $webhook_params = array_filter([
220
                'certificate'     => $webhook['certificate'] ?? null,
221
                'max_connections' => $webhook['max_connections'] ?? null,
222
                'allowed_updates' => $webhook['allowed_updates'] ?? null,
223
            ], function ($v, $k) {
224
                if ($k === 'allowed_updates') {
225
                    // Special case for allowed_updates, which can be an empty array.
226
                    return \is_array($v);
227
                }
228
                return !empty($v);
229
            }, ARRAY_FILTER_USE_BOTH);
230
231
            $this->handleOutput(
232
                $this->telegram->setWebhook(
233
                    $webhook['url'] . '?a=handle&s=' . $this->params->getBotParam('secret'),
234
                    $webhook_params
235
                )->getDescription() . PHP_EOL
236
            );
237
        }
238
239
        return $this;
240
    }
241
242
    /**
243
     * Save the test output and echo it if we're not in a test.
244
     *
245
     * @param string $output
246
     *
247
     * @return \TelegramBot\TelegramBotManager\BotManager
248
     */
249
    private function handleOutput(string $output): self
250
    {
251
        $this->output .= $output;
252
253
        if (!self::inTest()) {
254
            echo $output;
255
        }
256
257
        return $this;
258
    }
259
260
    /**
261
     * Set any extra bot features that have been assigned on construction.
262
     *
263
     * @return \TelegramBot\TelegramBotManager\BotManager
264
     * @throws \Longman\TelegramBot\Exception\TelegramException
265
     */
266
    public function setBotExtras(): self
267
    {
268
        $this->setBotExtrasTelegram();
269
        $this->setBotExtrasRequest();
270
271
        return $this;
272
    }
273
274
    /**
275
     * Set extra bot parameters for Telegram object.
276
     *
277
     * @return \TelegramBot\TelegramBotManager\BotManager
278
     * @throws \Longman\TelegramBot\Exception\TelegramException
279
     */
280
    protected function setBotExtrasTelegram(): self
281
    {
282
        $simple_extras = [
283
            'admins'         => 'enableAdmins',
284
            'commands.paths' => 'addCommandsPaths',
285
            'custom_input'   => 'setCustomInput',
286
            'paths.download' => 'setDownloadPath',
287
            'paths.upload'   => 'setUploadPath',
288
        ];
289
        // For simple telegram extras, just pass the single param value to the Telegram method.
290
        foreach ($simple_extras as $param_key => $method) {
291
            $param = $this->params->getBotParam($param_key);
292
            if (null !== $param) {
293
                $this->telegram->$method($param);
294
            }
295
        }
296
297
        // Database.
298
        if ($mysql_config = $this->params->getBotParam('mysql', [])) {
299
            $this->telegram->enableMySql(
300
                $mysql_config,
301
                $mysql_config['table_prefix'] ?? null,
302
                $mysql_config['encoding'] ?? 'utf8mb4'
303
            );
304
        }
305
306
        // Custom command configs.
307
        $command_configs = $this->params->getBotParam('commands.configs', []);
308
        foreach ($command_configs as $command => $config) {
309
            $this->telegram->setCommandConfig($command, $config);
310
        }
311
312
        return $this;
313
    }
314
315
    /**
316
     * Set extra bot parameters for Request class.
317
     *
318
     * @return \TelegramBot\TelegramBotManager\BotManager
319
     * @throws \Longman\TelegramBot\Exception\TelegramException
320
     */
321
    protected function setBotExtrasRequest(): self
322
    {
323
        $request_extras = [
324
            // None at the moment...
325
        ];
326
        // For request extras, just pass the single param value to the Request method.
327
        foreach ($request_extras as $param_key => $method) {
328
            $param = $this->params->getBotParam($param_key);
329
            if (null !== $param) {
330
                Request::$method($param);
331
            }
332
        }
333
334
        // Special cases.
335
        $limiter_enabled = $this->params->getBotParam('limiter.enabled');
336
        if ($limiter_enabled !== null) {
337
            $limiter_options = $this->params->getBotParam('limiter.options', []);
338
            Request::setLimiter($limiter_enabled, $limiter_options);
339
        }
340
341
        return $this;
342
    }
343
344
    /**
345
     * Handle the request, which calls either the Webhook or getUpdates method respectively.
346
     *
347
     * @return \TelegramBot\TelegramBotManager\BotManager
348
     * @throws \Longman\TelegramBot\Exception\TelegramException
349
     */
350
    public function handleRequest(): self
351
    {
352
        if ($this->params->getBotParam('webhook.url')) {
353
            return $this->handleWebhook();
354
        }
355
356
        if ($loop_time = $this->getLoopTime()) {
357
            return $this->handleGetUpdatesLoop($loop_time, $this->getLoopInterval());
358
        }
359
360
        return $this->handleGetUpdates();
361
    }
362
363
    /**
364
     * Handle cron.
365
     *
366
     * @return \TelegramBot\TelegramBotManager\BotManager
367
     * @throws \Longman\TelegramBot\Exception\TelegramException
368
     */
369
    public function handleCron(): self
370
    {
371
        $groups = explode(',', $this->params->getScriptParam('g', 'default'));
372
373
        $commands = [];
374
        foreach ($groups as $group) {
375
            $commands[] = $this->params->getBotParam('cron.groups.' . $group, []);
376
        }
377
        $this->telegram->runCommands(array_merge(...$commands));
378
379
        return $this;
380
    }
381
382
    /**
383
     * Get the number of seconds the script should loop.
384
     *
385
     * @return int
386
     */
387
    public function getLoopTime(): int
388
    {
389
        $loop_time = $this->params->getScriptParam('l');
390
391
        if (null === $loop_time) {
392
            return 0;
393
        }
394
395
        if (\is_string($loop_time) && '' === trim($loop_time)) {
396
            return 604800; // Default to 7 days.
397
        }
398
399
        return max(0, (int) $loop_time);
400
    }
401
402
    /**
403
     * Get the number of seconds the script should wait after each getUpdates request.
404
     *
405
     * @return int
406
     */
407
    public function getLoopInterval(): int
408
    {
409
        $interval_time = $this->params->getScriptParam('i');
410
411
        if (null === $interval_time || (\is_string($interval_time) && '' === trim($interval_time))) {
412
            return 2;
413
        }
414
415
        // Minimum interval is 1 second.
416
        return max(1, (int) $interval_time);
417
    }
418
419
    /**
420
     * Loop the getUpdates method for the passed amount of seconds.
421
     *
422
     * @param int $loop_time_in_seconds
423
     * @param int $loop_interval_in_seconds
424
     *
425
     * @return \TelegramBot\TelegramBotManager\BotManager
426
     * @throws \Longman\TelegramBot\Exception\TelegramException
427
     */
428
    public function handleGetUpdatesLoop(int $loop_time_in_seconds, int $loop_interval_in_seconds = 2): self
429
    {
430
        // Remember the time we started this loop.
431
        $now = time();
432
433
        $this->handleOutput('Looping getUpdates until ' . date('Y-m-d H:i:s', $now + $loop_time_in_seconds) . PHP_EOL);
434
435
        while ($now > time() - $loop_time_in_seconds) {
436
            $this->handleGetUpdates();
437
438
            // Chill a bit.
439
            sleep($loop_interval_in_seconds);
440
        }
441
442
        return $this;
443
    }
444
445
    /**
446
     * Set a custom callback for handling the output of the getUpdates results.
447
     *
448
     * @param callable $callback
449
     *
450
     * @return \TelegramBot\TelegramBotManager\BotManager
451
     */
452
    public function setCustomGetUpdatesCallback(callable $callback): BotManager
453
    {
454
        $this->custom_get_updates_callback = $callback;
455
        return $this;
456
    }
457
458
    /**
459
     * Handle the updates using the getUpdates method.
460
     *
461
     * @return \TelegramBot\TelegramBotManager\BotManager
462
     * @throws \Longman\TelegramBot\Exception\TelegramException
463
     */
464
    public function handleGetUpdates(): self
465
    {
466
        $get_updates_response = $this->telegram->handleGetUpdates();
467
468
        // Check if the user has set a custom callback for handling the response.
469
        if ($this->custom_get_updates_callback !== null) {
470
            $this->handleOutput(\call_user_func($this->custom_get_updates_callback, $get_updates_response));
471
        } else {
472
            $this->handleOutput($this->defaultGetUpdatesCallback($get_updates_response));
473
        }
474
475
        return $this;
476
    }
477
478
    /**
479
     * Return the default output for getUpdates handling.
480
     *
481
     * @param Entities\ServerResponse $get_updates_response
482
     *
483
     * @return string
484
     */
485
    protected function defaultGetUpdatesCallback($get_updates_response): string
486
    {
487
        if (!$get_updates_response->isOk()) {
488
            return sprintf(
489
                '%s - Failed to fetch updates' . PHP_EOL . '%s',
490
                date('Y-m-d H:i:s'),
491
                $get_updates_response->printError(true)
492
            );
493
        }
494
495
        /** @var Entities\Update[] $results */
496
        $results = array_filter((array) $get_updates_response->getResult());
497
498
        $output = sprintf(
499
            '%s - Updates processed: %d' . PHP_EOL,
500
            date('Y-m-d H:i:s'),
501
            count($results)
502
        );
503
504
        foreach ($results as $result) {
505
            $chat_id = 0;
506
            $text    = '<n/a>';
507
508
            $update_content = $result->getUpdateContent();
509
            if ($update_content instanceof Entities\Message) {
510
                $chat_id = $update_content->getChat()->getId();
511
                $text    = sprintf('<%s>', $update_content->getType());
512
            } elseif ($update_content instanceof Entities\InlineQuery ||
513
                      $update_content instanceof Entities\ChosenInlineResult
514
            ) {
515
                $chat_id = $update_content->getFrom()->getId();
516
                $text    = sprintf('<query> %s', $update_content->getQuery());
517
            }
518
519
            $output .= sprintf(
520
                '%d: %s' . PHP_EOL,
521
                $chat_id,
522
                preg_replace('/\s+/', ' ', trim($text))
523
            );
524
        }
525
526
        return $output;
527
    }
528
529
    /**
530
     * Handle the updates using the Webhook method.
531
     *
532
     * @return \TelegramBot\TelegramBotManager\BotManager
533
     * @throws \Longman\TelegramBot\Exception\TelegramException
534
     */
535
    public function handleWebhook(): self
536
    {
537
        $this->telegram->handle();
538
539
        return $this;
540
    }
541
542
    /**
543
     * Return the current test output and clear it.
544
     *
545
     * @return string
546
     */
547
    public function getOutput(): string
548
    {
549
        $output       = $this->output;
550
        $this->output = '';
551
552
        return $output;
553
    }
554
555
    /**
556
     * Check if this is a valid request coming from a Telegram API IP address.
557
     *
558
     * @link https://core.telegram.org/bots/webhooks#the-short-version
559
     *
560
     * @return bool
561
     */
562
    public function isValidRequest(): bool
563
    {
564
        // If we're running from CLI, requests are always valid, unless we're running the tests.
565
        if ((!self::inTest() && 'cli' === PHP_SAPI) || false === $this->params->getBotParam('validate_request')) {
566
            return true;
567
        }
568
569
        $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
570
        foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR'] as $key) {
571
            if (filter_var($_SERVER[$key] ?? null, FILTER_VALIDATE_IP)) {
572
                $ip = $_SERVER[$key];
573
                break;
574
            }
575
        }
576
577
        return Ip::match($ip, array_merge(
578
            self::TELEGRAM_IP_RANGES,
579
            (array) $this->params->getBotParam('valid_ips', [])
580
        ));
581
    }
582
583
    /**
584
     * Make sure this is a valid request.
585
     *
586
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
587
     */
588
    private function validateRequest()
589
    {
590
        if (!$this->isValidRequest()) {
591
            throw new InvalidAccessException('Invalid access');
592
        }
593
    }
594
}
595