Passed
Push — develop ( 35ca6c...1098ca )
by Armando
02:04
created

BotManager::initLogging()   B

Complexity

Conditions 7
Paths 27

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 6
dl 0
loc 11
c 1
b 0
f 0
rs 8.8333
cc 7
nc 27
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 Exception;
14
use Longman\IPTools\Ip;
15
use Longman\TelegramBot\Entities\CallbackQuery;
16
use Longman\TelegramBot\Entities\ChosenInlineResult;
17
use Longman\TelegramBot\Entities\InlineQuery;
18
use Longman\TelegramBot\Entities\Message;
19
use Longman\TelegramBot\Entities\ServerResponse;
20
use Longman\TelegramBot\Entities\Update;
21
use Longman\TelegramBot\Exception\TelegramException;
22
use Longman\TelegramBot\Request;
23
use Longman\TelegramBot\Telegram;
24
use TelegramBot\TelegramBotManager\Exception\InvalidAccessException;
25
use TelegramBot\TelegramBotManager\Exception\InvalidActionException;
26
use TelegramBot\TelegramBotManager\Exception\InvalidParamsException;
27
use TelegramBot\TelegramBotManager\Exception\InvalidWebhookException;
28
29
class BotManager
30
{
31
    /**
32
     * @link https://core.telegram.org/bots/webhooks#the-short-version
33
     * @var array Telegram webhook servers IP ranges
34
     */
35
    public const TELEGRAM_IP_RANGES = ['149.154.160.0/20', '91.108.4.0/22'];
36
37
    /**
38
     * @var string The output for testing, instead of echoing
39
     */
40
    private $output = '';
41
42
    /**
43
     * @var Telegram
44
     */
45
    private $telegram;
46
47
    /**
48
     * @var Params Object that manages the parameters.
49
     */
50
    private $params;
51
52
    /**
53
     * @var Action Object that contains the current action.
54
     */
55
    private $action;
56
57
    /**
58
     * @var callable
59
     */
60
    private $custom_get_updates_callback;
61
62
    /**
63
     * BotManager constructor.
64
     *
65
     * @param array $params
66
     *
67
     * @throws InvalidParamsException
68
     * @throws InvalidActionException
69
     * @throws TelegramException
70
     * @throws Exception
71
     */
72
    public function __construct(array $params)
73
    {
74
        $this->params = new Params($params);
75
        $this->action = new Action($this->params->getScriptParam('a'));
76
77
        // Set up a new Telegram instance.
78
        $this->telegram = new Telegram(
79
            $this->params->getBotParam('api_key'),
0 ignored issues
show
Bug introduced by
It seems like $this->params->getBotParam('api_key') can also be of type null; however, parameter $api_key of Longman\TelegramBot\Telegram::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

79
            /** @scrutinizer ignore-type */ $this->params->getBotParam('api_key'),
Loading history...
80
            $this->params->getBotParam('bot_username') ?? ''
81
        );
82
    }
83
84
    /**
85
     * Check if we're busy running the PHPUnit tests.
86
     *
87
     * @return bool
88
     */
89
    public static function inTest(): bool
90
    {
91
        return defined('PHPUNIT_TESTSUITE') && PHPUNIT_TESTSUITE === true;
0 ignored issues
show
Bug introduced by
The constant TelegramBot\TelegramBotManager\PHPUNIT_TESTSUITE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
92
    }
93
94
    /**
95
     * Return the Telegram object.
96
     *
97
     * @return Telegram
98
     */
99
    public function getTelegram(): Telegram
100
    {
101
        return $this->telegram;
102
    }
103
104
    /**
105
     * Get the Params object.
106
     *
107
     * @return Params
108
     */
109
    public function getParams(): Params
110
    {
111
        return $this->params;
112
    }
113
114
    /**
115
     * Get the Action object.
116
     *
117
     * @return Action
118
     */
119
    public function getAction(): Action
120
    {
121
        return $this->action;
122
    }
123
124
    /**
125
     * Run this thing in all its glory!
126
     *
127
     * @return BotManager
128
     * @throws TelegramException
129
     * @throws InvalidAccessException
130
     * @throws InvalidWebhookException
131
     * @throws Exception
132
     */
133
    public function run(): self
134
    {
135
        // Make sure this is a valid call.
136
        $this->validateSecret();
137
        $this->validateRequest();
138
139
        if ($this->action->isAction('webhookinfo')) {
140
            $webhookinfo = Request::getWebhookInfo();
141
            /** @noinspection ForgottenDebugOutputInspection */
142
            print_r($webhookinfo->getResult() ?: $webhookinfo->printError(true));
143
            return $this;
144
        }
145
        if ($this->action->isAction(['set', 'unset', 'reset'])) {
146
            return $this->validateAndSetWebhook();
147
        }
148
149
        $this->setBotExtras();
150
151
        if ($this->action->isAction('handle')) {
152
            $this->handleRequest();
153
        } elseif ($this->action->isAction('cron')) {
154
            $this->handleCron();
155
        }
156
157
        return $this;
158
    }
159
160
    /**
161
     * Make sure the passed secret is valid.
162
     *
163
     * @param bool $force Force validation, even on CLI.
164
     *
165
     * @return BotManager
166
     * @throws InvalidAccessException
167
     */
168
    public function validateSecret(bool $force = false): self
169
    {
170
        // If we're running from CLI, secret isn't necessary.
171
        if ($force || 'cli' !== PHP_SAPI) {
172
            $secret     = $this->params->getBotParam('secret');
173
            $secret_get = $this->params->getScriptParam('s');
174
            if (!isset($secret, $secret_get) || $secret !== $secret_get) {
175
                throw new InvalidAccessException('Invalid access');
176
            }
177
        }
178
179
        return $this;
180
    }
181
182
    /**
183
     * Make sure the webhook is valid and perform the requested webhook operation.
184
     *
185
     * @return BotManager
186
     * @throws TelegramException
187
     * @throws InvalidWebhookException
188
     */
189
    public function validateAndSetWebhook(): self
190
    {
191
        $webhook = $this->params->getBotParam('webhook');
192
        if (empty($webhook['url'] ?? null) && $this->action->isAction(['set', 'reset'])) {
193
            throw new InvalidWebhookException('Invalid webhook');
194
        }
195
196
        if ($this->action->isAction(['unset', 'reset'])) {
197
            $this->handleOutput($this->telegram->deleteWebhook()->getDescription() . PHP_EOL);
198
            // When resetting the webhook, sleep for a bit to prevent too many requests.
199
            $this->action->isAction('reset') && sleep(1);
200
        }
201
202
        if ($this->action->isAction(['set', 'reset'])) {
203
            $webhook_params = array_filter([
204
                'certificate'     => $webhook['certificate'] ?? null,
205
                'max_connections' => $webhook['max_connections'] ?? null,
206
                'allowed_updates' => $webhook['allowed_updates'] ?? null,
207
            ], function ($v, $k) {
208
                if ($k === 'allowed_updates') {
209
                    // Special case for allowed_updates, which can be an empty array.
210
                    return is_array($v);
211
                }
212
                return !empty($v);
213
            }, ARRAY_FILTER_USE_BOTH);
214
215
            $this->handleOutput(
216
                $this->telegram->setWebhook(
217
                    $webhook['url'] . '?a=handle&s=' . $this->params->getBotParam('secret'),
218
                    $webhook_params
219
                )->getDescription() . PHP_EOL
220
            );
221
        }
222
223
        return $this;
224
    }
225
226
    /**
227
     * Save the test output and echo it if we're not in a test.
228
     *
229
     * @param string $output
230
     *
231
     * @return BotManager
232
     */
233
    private function handleOutput(string $output): self
234
    {
235
        $this->output .= $output;
236
237
        if (!self::inTest()) {
238
            echo $output;
239
        }
240
241
        return $this;
242
    }
243
244
    /**
245
     * Set any extra bot features that have been assigned on construction.
246
     *
247
     * @return BotManager
248
     * @throws TelegramException
249
     */
250
    public function setBotExtras(): self
251
    {
252
        $this->setBotExtrasTelegram();
253
        $this->setBotExtrasRequest();
254
255
        return $this;
256
    }
257
258
    /**
259
     * Set extra bot parameters for Telegram object.
260
     *
261
     * @return BotManager
262
     * @throws TelegramException
263
     */
264
    protected function setBotExtrasTelegram(): self
265
    {
266
        $simple_extras = [
267
            'admins'         => 'enableAdmins',
268
            'commands.paths' => 'addCommandsPaths',
269
            'custom_input'   => 'setCustomInput',
270
            'paths.download' => 'setDownloadPath',
271
            'paths.upload'   => 'setUploadPath',
272
        ];
273
        // For simple telegram extras, just pass the single param value to the Telegram method.
274
        foreach ($simple_extras as $param_key => $method) {
275
            $param = $this->params->getBotParam($param_key);
276
            if (null !== $param) {
277
                $this->telegram->$method($param);
278
            }
279
        }
280
281
        // Database.
282
        if ($mysql_config = $this->params->getBotParam('mysql', [])) {
283
            $this->telegram->enableMySql(
284
                $mysql_config,
285
                $mysql_config['table_prefix'] ?? '',
286
                $mysql_config['encoding'] ?? 'utf8mb4'
287
            );
288
        }
289
290
        // Custom command configs.
291
        $command_configs = $this->params->getBotParam('commands.configs', []);
292
        foreach ($command_configs as $command => $config) {
293
            $this->telegram->setCommandConfig($command, $config);
294
        }
295
296
        return $this;
297
    }
298
299
    /**
300
     * Set extra bot parameters for Request class.
301
     *
302
     * @return BotManager
303
     * @throws 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 BotManager
332
     * @throws TelegramException
333
     */
334
    public function handleRequest(): self
335
    {
336
        if ($this->params->getBotParam('webhook.url')) {
337
            return $this->handleWebhook();
338
        }
339
340
        if ($loop_time = $this->getLoopTime()) {
341
            return $this->handleGetUpdatesLoop($loop_time, $this->getLoopInterval());
342
        }
343
344
        return $this->handleGetUpdates();
345
    }
346
347
    /**
348
     * Handle cron.
349
     *
350
     * @return BotManager
351
     * @throws TelegramException
352
     */
353
    public function handleCron(): self
354
    {
355
        $groups = explode(',', $this->params->getScriptParam('g', 'default'));
356
357
        $commands = [];
358
        foreach ($groups as $group) {
359
            $commands[] = $this->params->getBotParam('cron.groups.' . $group, []);
360
        }
361
        $this->telegram->runCommands(array_merge(...$commands));
362
363
        return $this;
364
    }
365
366
    /**
367
     * Get the number of seconds the script should loop.
368
     *
369
     * @return int
370
     */
371
    public function getLoopTime(): int
372
    {
373
        $loop_time = $this->params->getScriptParam('l');
374
375
        if (null === $loop_time) {
376
            return 0;
377
        }
378
379
        if (is_string($loop_time) && '' === trim($loop_time)) {
380
            return 604800; // Default to 7 days.
381
        }
382
383
        return max(0, (int) $loop_time);
384
    }
385
386
    /**
387
     * Get the number of seconds the script should wait after each getUpdates request.
388
     *
389
     * @return int
390
     */
391
    public function getLoopInterval(): int
392
    {
393
        $interval_time = $this->params->getScriptParam('i');
394
395
        if (null === $interval_time || (is_string($interval_time) && '' === trim($interval_time))) {
396
            return 2;
397
        }
398
399
        // Minimum interval is 1 second.
400
        return max(1, (int) $interval_time);
401
    }
402
403
    /**
404
     * Loop the getUpdates method for the passed amount of seconds.
405
     *
406
     * @param int $loop_time_in_seconds
407
     * @param int $loop_interval_in_seconds
408
     *
409
     * @return BotManager
410
     * @throws TelegramException
411
     */
412
    public function handleGetUpdatesLoop(int $loop_time_in_seconds, int $loop_interval_in_seconds = 2): self
413
    {
414
        // Remember the time we started this loop.
415
        $now = time();
416
417
        $this->handleOutput('Looping getUpdates until ' . date('Y-m-d H:i:s', $now + $loop_time_in_seconds) . PHP_EOL);
418
419
        while ($now > time() - $loop_time_in_seconds) {
420
            $this->handleGetUpdates();
421
422
            // Chill a bit.
423
            sleep($loop_interval_in_seconds);
424
        }
425
426
        return $this;
427
    }
428
429
    /**
430
     * Set a custom callback for handling the output of the getUpdates results.
431
     *
432
     * @param callable $callback
433
     *
434
     * @return BotManager
435
     */
436
    public function setCustomGetUpdatesCallback(callable $callback): BotManager
437
    {
438
        $this->custom_get_updates_callback = $callback;
439
        return $this;
440
    }
441
442
    /**
443
     * Handle the updates using the getUpdates method.
444
     *
445
     * @return BotManager
446
     * @throws TelegramException
447
     */
448
    public function handleGetUpdates(): self
449
    {
450
        $get_updates_response = $this->telegram->handleGetUpdates();
451
452
        // Check if the user has set a custom callback for handling the response.
453
        if ($this->custom_get_updates_callback !== null) {
454
            $this->handleOutput(call_user_func($this->custom_get_updates_callback, $get_updates_response));
455
        } else {
456
            $this->handleOutput($this->defaultGetUpdatesCallback($get_updates_response));
457
        }
458
459
        return $this;
460
    }
461
462
    /**
463
     * Return the default output for getUpdates handling.
464
     *
465
     * @param ServerResponse $get_updates_response
466
     *
467
     * @return string
468
     */
469
    protected function defaultGetUpdatesCallback($get_updates_response): string
470
    {
471
        if (!$get_updates_response->isOk()) {
472
            return sprintf(
473
                '%s - Failed to fetch updates' . PHP_EOL . '%s',
474
                date('Y-m-d H:i:s'),
475
                $get_updates_response->printError(true)
476
            );
477
        }
478
479
        /** @var Update[] $results */
480
        $results = array_filter((array) $get_updates_response->getResult());
481
482
        $output = sprintf(
483
            '%s - Updates processed: %d' . PHP_EOL,
484
            date('Y-m-d H:i:s'),
485
            count($results)
486
        );
487
488
        foreach ($results as $result) {
489
            $update_content = $result->getUpdateContent();
490
491
            $chat_id = 'n/a';
492
            $text    = $result->getUpdateType();
493
494
            if ($update_content instanceof Message) {
495
                /** @var Message $update_content */
496
                $chat_id = $update_content->getChat()->getId();
497
                $text    .= ";{$update_content->getType()}";
498
            } elseif ($update_content instanceof InlineQuery || $update_content instanceof ChosenInlineResult) {
499
                /** @var InlineQuery|ChosenInlineResult $update_content */
500
                $chat_id = $update_content->getFrom()->getId();
501
                $text    .= ";{$update_content->getQuery()}";
502
            } elseif ($update_content instanceof CallbackQuery) {
503
                /** @var CallbackQuery $update_content */
504
                $chat_id = $update_content->getMessage()->getChat()->getId();
505
                $text    .= ";{$update_content->getData()}";
506
            }
507
508
            $output .= sprintf(
509
                '%d: <%s>' . PHP_EOL,
510
                $chat_id,
511
                preg_replace('/\s+/', ' ', trim($text))
512
            );
513
        }
514
515
        return $output;
516
    }
517
518
    /**
519
     * Handle the updates using the Webhook method.
520
     *
521
     * @return BotManager
522
     * @throws TelegramException
523
     */
524
    public function handleWebhook(): self
525
    {
526
        $this->telegram->handle();
527
528
        return $this;
529
    }
530
531
    /**
532
     * Return the current test output and clear it.
533
     *
534
     * @return string
535
     */
536
    public function getOutput(): string
537
    {
538
        $output       = $this->output;
539
        $this->output = '';
540
541
        return $output;
542
    }
543
544
    /**
545
     * Check if this is a valid request coming from a Telegram API IP address.
546
     *
547
     * @link https://core.telegram.org/bots/webhooks#the-short-version
548
     *
549
     * @return bool
550
     */
551
    public function isValidRequest(): bool
552
    {
553
        // If we're running from CLI, requests are always valid, unless we're running the tests.
554
        if ((!self::inTest() && 'cli' === PHP_SAPI) || false === $this->params->getBotParam('validate_request')) {
555
            return true;
556
        }
557
558
        $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
559
        foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR'] as $key) {
560
            if (filter_var($_SERVER[$key] ?? null, FILTER_VALIDATE_IP)) {
561
                $ip = $_SERVER[$key];
562
                break;
563
            }
564
        }
565
566
        return Ip::match($ip, array_merge(
567
            self::TELEGRAM_IP_RANGES,
568
            (array) $this->params->getBotParam('valid_ips', [])
569
        ));
570
    }
571
572
    /**
573
     * Make sure this is a valid request.
574
     *
575
     * @throws InvalidAccessException
576
     */
577
    private function validateRequest(): void
578
    {
579
        if (!$this->isValidRequest()) {
580
            throw new InvalidAccessException('Invalid access');
581
        }
582
    }
583
}
584