Completed
Pull Request — develop (#50)
by Armando
03:16 queued 02:12
created

BotManager::setBotExtrasTelegram()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

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