Passed
Pull Request — develop (#51)
by Armando
01:33
created

BotManager::initLogging()   A

Complexity

Conditions 6
Paths 18

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 6
c 1
b 0
f 0
dl 0
loc 11
rs 9.2222
cc 6
nc 18
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\ChosenInlineResult;
16
use Longman\TelegramBot\Entities\InlineQuery;
17
use Longman\TelegramBot\Entities\Message;
18
use Longman\TelegramBot\Entities\ServerResponse;
19
use Longman\TelegramBot\Entities\Update;
20
use Longman\TelegramBot\Exception\TelegramException;
21
use Longman\TelegramBot\Request;
22
use Longman\TelegramBot\Telegram;
23
use Longman\TelegramBot\TelegramLog;
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
     * @var array Telegram webhook servers IP ranges
33
     * @link https://core.telegram.org/bots/webhooks#the-short-version
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
        // Initialise logging before anything else, to allow errors to be logged.
75
        $this->initLogging($params['logging'] ?? []);
76
77
        $this->params = new Params($params);
78
        $this->action = new Action($this->params->getScriptParam('a'));
79
80
        // Set up a new Telegram instance.
81
        $this->telegram = new Telegram(
82
            $this->params->getBotParam('api_key'),
83
            $this->params->getBotParam('bot_username')
84
        );
85
    }
86
87
    /**
88
     * Check if we're busy running the PHPUnit tests.
89
     *
90
     * @return bool
91
     */
92
    public static function inTest(): bool
93
    {
94
        return defined('PHPUNIT_TESTSUITE') && PHPUNIT_TESTSUITE === true;
95
    }
96
97
    /**
98
     * Return the Telegram object.
99
     *
100
     * @return Telegram
101
     */
102
    public function getTelegram(): Telegram
103
    {
104
        return $this->telegram;
105
    }
106
107
    /**
108
     * Get the Params object.
109
     *
110
     * @return Params
111
     */
112
    public function getParams(): Params
113
    {
114
        return $this->params;
115
    }
116
117
    /**
118
     * Get the Action object.
119
     *
120
     * @return Action
121
     */
122
    public function getAction(): Action
123
    {
124
        return $this->action;
125
    }
126
127
    /**
128
     * Run this thing in all its glory!
129
     *
130
     * @return BotManager
131
     * @throws TelegramException
132
     * @throws InvalidAccessException
133
     * @throws InvalidWebhookException
134
     * @throws Exception
135
     */
136
    public function run(): self
137
    {
138
        // Make sure this is a valid call.
139
        $this->validateSecret();
140
        $this->validateRequest();
141
142
        if ($this->action->isAction('webhookinfo')) {
143
            $webhookinfo = Request::getWebhookInfo();
144
            /** @noinspection ForgottenDebugOutputInspection */
145
            print_r($webhookinfo->getResult() ?: $webhookinfo->printError(true));
146
            return $this;
147
        }
148
        if ($this->action->isAction(['set', 'unset', 'reset'])) {
149
            return $this->validateAndSetWebhook();
150
        }
151
152
        $this->setBotExtras();
153
154
        if ($this->action->isAction('handle')) {
155
            $this->handleRequest();
156
        } elseif ($this->action->isAction('cron')) {
157
            $this->handleCron();
158
        }
159
160
        return $this;
161
    }
162
163
    /**
164
     * Initialise all loggers.
165
     *
166
     * @param array $log_paths
167
     *
168
     * @return BotManager
169
     * @throws Exception
170
     */
171
    public function initLogging(array $log_paths): self
172
    {
173
        self::inTest() || trigger_error(__METHOD__ . ' is deprecated and will be removed soon. Initialise with a preconfigured logger instance instead using "TelegramLog::initialize($logger)".', E_USER_DEPRECATED);
174
175
        foreach ($log_paths as $logger => $logfile) {
176
            ('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

176
            ('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...
177
            ('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

177
            ('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...
178
            ('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

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