Completed
Pull Request — develop (#48)
by Armando
01:40
created

BotManager::handleOutput()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
cc 2
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 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 method Longman\TelegramBot\TelegramLog::initDebugLog() has been deprecated with message: Initialise a preconfigured logger instance instead.

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

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

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

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

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

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

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

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class 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
        // Botan with options.
313
        if ($botan_token = $this->params->getBotParam('botan.token')) {
314
            $botan_options = $this->params->getBotParam('botan.options', []);
315
            $this->telegram->enableBotan($botan_token, $botan_options);
0 ignored issues
show
Bug introduced by
The method enableBotan() does not seem to exist on object<Longman\TelegramBot\Telegram>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
316
        }
317
318
        return $this;
319
    }
320
321
    /**
322
     * Set extra bot parameters for Request class.
323
     *
324
     * @return \TelegramBot\TelegramBotManager\BotManager
325
     * @throws \Longman\TelegramBot\Exception\TelegramException
326
     */
327
    protected function setBotExtrasRequest(): self
328
    {
329
        $request_extras = [
330
            // None at the moment...
331
        ];
332
        // For request extras, just pass the single param value to the Request method.
333
        foreach ($request_extras as $param_key => $method) {
334
            $param = $this->params->getBotParam($param_key);
335
            if (null !== $param) {
336
                Request::$method($param);
337
            }
338
        }
339
340
        // Special cases.
341
        $limiter_enabled = $this->params->getBotParam('limiter.enabled');
342
        if ($limiter_enabled !== null) {
343
            $limiter_options = $this->params->getBotParam('limiter.options', []);
344
            Request::setLimiter($limiter_enabled, $limiter_options);
345
        }
346
347
        return $this;
348
    }
349
350
    /**
351
     * Handle the request, which calls either the Webhook or getUpdates method respectively.
352
     *
353
     * @return \TelegramBot\TelegramBotManager\BotManager
354
     * @throws \Longman\TelegramBot\Exception\TelegramException
355
     */
356
    public function handleRequest(): self
357
    {
358
        if ($this->params->getBotParam('webhook.url')) {
359
            return $this->handleWebhook();
360
        }
361
362
        if ($loop_time = $this->getLoopTime()) {
363
            return $this->handleGetUpdatesLoop($loop_time, $this->getLoopInterval());
364
        }
365
366
        return $this->handleGetUpdates();
367
    }
368
369
    /**
370
     * Handle cron.
371
     *
372
     * @return \TelegramBot\TelegramBotManager\BotManager
373
     * @throws \Longman\TelegramBot\Exception\TelegramException
374
     */
375
    public function handleCron(): self
376
    {
377
        $groups = explode(',', $this->params->getScriptParam('g', 'default'));
378
379
        $commands = [];
380
        foreach ($groups as $group) {
381
            $commands[] = $this->params->getBotParam('cron.groups.' . $group, []);
382
        }
383
        $this->telegram->runCommands(array_merge(...$commands));
384
385
        return $this;
386
    }
387
388
    /**
389
     * Get the number of seconds the script should loop.
390
     *
391
     * @return int
392
     */
393
    public function getLoopTime(): int
394
    {
395
        $loop_time = $this->params->getScriptParam('l');
396
397
        if (null === $loop_time) {
398
            return 0;
399
        }
400
401
        if (\is_string($loop_time) && '' === trim($loop_time)) {
402
            return 604800; // Default to 7 days.
403
        }
404
405
        return max(0, (int) $loop_time);
406
    }
407
408
    /**
409
     * Get the number of seconds the script should wait after each getUpdates request.
410
     *
411
     * @return int
412
     */
413
    public function getLoopInterval(): int
414
    {
415
        $interval_time = $this->params->getScriptParam('i');
416
417
        if (null === $interval_time || (\is_string($interval_time) && '' === trim($interval_time))) {
418
            return 2;
419
        }
420
421
        // Minimum interval is 1 second.
422
        return max(1, (int) $interval_time);
423
    }
424
425
    /**
426
     * Loop the getUpdates method for the passed amount of seconds.
427
     *
428
     * @param int $loop_time_in_seconds
429
     * @param int $loop_interval_in_seconds
430
     *
431
     * @return \TelegramBot\TelegramBotManager\BotManager
432
     * @throws \Longman\TelegramBot\Exception\TelegramException
433
     */
434
    public function handleGetUpdatesLoop(int $loop_time_in_seconds, int $loop_interval_in_seconds = 2): self
435
    {
436
        // Remember the time we started this loop.
437
        $now = time();
438
439
        $this->handleOutput('Looping getUpdates until ' . date('Y-m-d H:i:s', $now + $loop_time_in_seconds) . PHP_EOL);
440
441
        while ($now > time() - $loop_time_in_seconds) {
442
            $this->handleGetUpdates();
443
444
            // Chill a bit.
445
            sleep($loop_interval_in_seconds);
446
        }
447
448
        return $this;
449
    }
450
451
    /**
452
     * Set a custom callback for handling the output of the getUpdates results.
453
     *
454
     * @param callable $callback
455
     *
456
     * @return \TelegramBot\TelegramBotManager\BotManager
457
     */
458
    public function setCustomGetUpdatesCallback(callable $callback): BotManager
459
    {
460
        $this->custom_get_updates_callback = $callback;
461
        return $this;
462
    }
463
464
    /**
465
     * Handle the updates using the getUpdates method.
466
     *
467
     * @return \TelegramBot\TelegramBotManager\BotManager
468
     * @throws \Longman\TelegramBot\Exception\TelegramException
469
     */
470
    public function handleGetUpdates(): self
471
    {
472
        $get_updates_response = $this->telegram->handleGetUpdates();
473
474
        // Check if the user has set a custom callback for handling the response.
475
        if ($this->custom_get_updates_callback !== null) {
476
            $this->handleOutput(\call_user_func($this->custom_get_updates_callback, $get_updates_response));
477
        } else {
478
            $this->handleOutput($this->defaultGetUpdatesCallback($get_updates_response));
479
        }
480
481
        return $this;
482
    }
483
484
    /**
485
     * Return the default output for getUpdates handling.
486
     *
487
     * @param Entities\ServerResponse $get_updates_response
488
     *
489
     * @return string
490
     */
491
    protected function defaultGetUpdatesCallback($get_updates_response): string
492
    {
493
        if (!$get_updates_response->isOk()) {
494
            return sprintf(
495
                '%s - Failed to fetch updates' . PHP_EOL . '%s',
496
                date('Y-m-d H:i:s'),
497
                $get_updates_response->printError(true)
498
            );
499
        }
500
501
        /** @var Entities\Update[] $results */
502
        $results = array_filter((array) $get_updates_response->getResult());
503
504
        $output = sprintf(
505
            '%s - Updates processed: %d' . PHP_EOL,
506
            date('Y-m-d H:i:s'),
507
            count($results)
508
        );
509
510
        foreach ($results as $result) {
511
            $chat_id = 0;
512
            $text    = '<n/a>';
513
514
            $update_content = $result->getUpdateContent();
515
            if ($update_content instanceof Entities\Message) {
516
                $chat_id = $update_content->getChat()->getId();
517
                $text    = sprintf('<%s>', $update_content->getType());
518
            } elseif ($update_content instanceof Entities\InlineQuery ||
519
                      $update_content instanceof Entities\ChosenInlineResult
520
            ) {
521
                $chat_id = $update_content->getFrom()->getId();
522
                $text    = sprintf('<query> %s', $update_content->getQuery());
523
            }
524
525
            $output .= sprintf(
526
                '%d: %s' . PHP_EOL,
527
                $chat_id,
528
                preg_replace('/\s+/', ' ', trim($text))
529
            );
530
        }
531
532
        return $output;
533
    }
534
535
    /**
536
     * Handle the updates using the Webhook method.
537
     *
538
     * @return \TelegramBot\TelegramBotManager\BotManager
539
     * @throws \Longman\TelegramBot\Exception\TelegramException
540
     */
541
    public function handleWebhook(): self
542
    {
543
        $this->telegram->handle();
544
545
        return $this;
546
    }
547
548
    /**
549
     * Return the current test output and clear it.
550
     *
551
     * @return string
552
     */
553
    public function getOutput(): string
554
    {
555
        $output       = $this->output;
556
        $this->output = '';
557
558
        return $output;
559
    }
560
561
    /**
562
     * Check if this is a valid request coming from a Telegram API IP address.
563
     *
564
     * @link https://core.telegram.org/bots/webhooks#the-short-version
565
     *
566
     * @return bool
567
     */
568
    public function isValidRequest(): bool
569
    {
570
        // If we're running from CLI, requests are always valid, unless we're running the tests.
571
        if ((!self::inTest() && 'cli' === PHP_SAPI) || false === $this->params->getBotParam('validate_request')) {
572
            return true;
573
        }
574
575
        $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
576
        foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR'] as $key) {
577
            if (filter_var($_SERVER[$key] ?? null, FILTER_VALIDATE_IP)) {
578
                $ip = $_SERVER[$key];
579
                break;
580
            }
581
        }
582
583
        return Ip::match($ip, array_merge(
584
            self::TELEGRAM_IP_RANGES,
585
            (array) $this->params->getBotParam('valid_ips', [])
586
        ));
587
    }
588
589
    /**
590
     * Make sure this is a valid request.
591
     *
592
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
593
     */
594
    private function validateRequest()
595
    {
596
        if (!$this->isValidRequest()) {
597
            throw new InvalidAccessException('Invalid access');
598
        }
599
    }
600
}
601