Completed
Push — develop ( b4485a...786faa )
by Armando
05:23
created

BotManager::run()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 21
rs 8.7624
c 0
b 0
f 0
cc 5
eloc 13
nc 5
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 string Telegram post servers IP range
25
     */
26
    const TELEGRAM_IP_RANGE = '149.154.167.197-149.154.167.233';
27
28
    /**
29
     * @var string The output for testing, instead of echoing
30
     */
31
    private $output = '';
32
33
    /**
34
     * @var \Longman\TelegramBot\Telegram
35
     */
36
    private $telegram;
37
38
    /**
39
     * @var \TelegramBot\TelegramBotManager\Params Object that manages the parameters.
40
     */
41
    private $params;
42
43
    /**
44
     * @var \TelegramBot\TelegramBotManager\Action Object that contains the current action.
45
     */
46
    private $action;
47
48
    /**
49
     * BotManager constructor.
50
     *
51
     * @param array $params
52
     *
53
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidParamsException
54
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidActionException
55
     * @throws \Longman\TelegramBot\Exception\TelegramException
56
     */
57
    public function __construct(array $params)
58
    {
59
        // Initialise logging before anything else, to allow errors to be logged.
60
        $this->initLogging($params['logging'] ?? []);
61
62
        $this->params = new Params($params);
63
        $this->action = new Action($this->params->getScriptParam('a'));
64
65
        // Set up a new Telegram instance.
66
        $this->telegram = new Telegram(
67
            $this->params->getBotParam('api_key'),
68
            $this->params->getBotParam('bot_username')
69
        );
70
    }
71
72
    /**
73
     * Check if we're busy running the PHPUnit tests.
74
     *
75
     * @return bool
76
     */
77
    public static function inTest(): bool
78
    {
79
        return defined('PHPUNIT_TEST') && PHPUNIT_TEST === true;
80
    }
81
82
    /**
83
     * Return the Telegram object.
84
     *
85
     * @return \Longman\TelegramBot\Telegram
86
     */
87
    public function getTelegram(): Telegram
88
    {
89
        return $this->telegram;
90
    }
91
92
    /**
93
     * Get the Params object.
94
     *
95
     * @return \TelegramBot\TelegramBotManager\Params
96
     */
97
    public function getParams(): Params
98
    {
99
        return $this->params;
100
    }
101
102
    /**
103
     * Get the Action object.
104
     *
105
     * @return \TelegramBot\TelegramBotManager\Action
106
     */
107
    public function getAction(): Action
108
    {
109
        return $this->action;
110
    }
111
112
    /**
113
     * Run this thing in all its glory!
114
     *
115
     * @return \TelegramBot\TelegramBotManager\BotManager
116
     * @throws \Longman\TelegramBot\Exception\TelegramException
117
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
118
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidWebhookException
119
     * @throws \Exception
120
     */
121
    public function run(): self
122
    {
123
        // Make sure this is a valid call.
124
        $this->validateSecret();
125
126
        if (!$this->isValidRequest()) {
127
            throw new InvalidAccessException('Invalid access');
128
        }
129
130
        if ($this->action->isAction(['set', 'unset', 'reset'])) {
131
            $this->validateAndSetWebhook();
132
        } elseif ($this->action->isAction('handle')) {
133
            $this->setBotExtras();
134
            $this->handleRequest();
135
        } elseif ($this->action->isAction('cron')) {
136
            $this->setBotExtras();
137
            $this->handleCron();
138
        }
139
140
        return $this;
141
    }
142
143
    /**
144
     * Initialise all loggers.
145
     *
146
     * @param array $log_paths
147
     *
148
     * @return \TelegramBot\TelegramBotManager\BotManager
149
     * @throws \Exception
150
     */
151
    public function initLogging(array $log_paths): self
152
    {
153
        foreach ($log_paths as $logger => $logfile) {
154
            ('debug' === $logger) && TelegramLog::initDebugLog($logfile);
155
            ('error' === $logger) && TelegramLog::initErrorLog($logfile);
156
            ('update' === $logger) && TelegramLog::initUpdateLog($logfile);
157
        }
158
159
        return $this;
160
    }
161
162
    /**
163
     * Make sure the passed secret is valid.
164
     *
165
     * @param bool $force Force validation, even on CLI.
166
     *
167
     * @return \TelegramBot\TelegramBotManager\BotManager
168
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
169
     */
170
    public function validateSecret(bool $force = false): self
171
    {
172
        // If we're running from CLI, secret isn't necessary.
173
        if ($force || 'cli' !== PHP_SAPI) {
174
            $secret     = $this->params->getBotParam('secret');
175
            $secret_get = $this->params->getScriptParam('s');
176
            if (!isset($secret, $secret_get) || $secret !== $secret_get) {
177
                throw new InvalidAccessException('Invalid access');
178
            }
179
        }
180
181
        return $this;
182
    }
183
184
    /**
185
     * Make sure the webhook is valid and perform the requested webhook operation.
186
     *
187
     * @return \TelegramBot\TelegramBotManager\BotManager
188
     * @throws \Longman\TelegramBot\Exception\TelegramException
189
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidWebhookException
190
     */
191
    public function validateAndSetWebhook(): self
192
    {
193
        $webhook = $this->params->getBotParam('webhook');
194
        if (empty($webhook['url'] ?? null) && $this->action->isAction(['set', 'reset'])) {
195
            throw new InvalidWebhookException('Invalid webhook');
196
        }
197
198
        if ($this->action->isAction(['unset', 'reset'])) {
199
            $this->handleOutput($this->telegram->deleteWebhook()->getDescription() . PHP_EOL);
200
            // When resetting the webhook, sleep for a bit to prevent too many requests.
201
            $this->action->isAction('reset') && sleep(1);
202
        }
203
204
        if ($this->action->isAction(['set', 'reset'])) {
205
            $webhook_params = array_filter([
206
                'certificate'     => $webhook['certificate'] ?? null,
207
                'max_connections' => $webhook['max_connections'] ?? null,
208
                'allowed_updates' => $webhook['allowed_updates'] ?? null,
209
            ]);
210
211
            $this->handleOutput(
212
                $this->telegram->setWebhook(
213
                    $webhook['url'] . '?a=handle&s=' . $this->params->getBotParam('secret'),
214
                    $webhook_params
215
                )->getDescription() . PHP_EOL
216
            );
217
        }
218
219
        return $this;
220
    }
221
222
    /**
223
     * Save the test output and echo it if we're not in a test.
224
     *
225
     * @param string $output
226
     *
227
     * @return \TelegramBot\TelegramBotManager\BotManager
228
     */
229
    private function handleOutput(string $output): self
230
    {
231
        $this->output .= $output;
232
233
        if (!self::inTest()) {
234
            echo $output;
235
        }
236
237
        return $this;
238
    }
239
240
    /**
241
     * Set any extra bot features that have been assigned on construction.
242
     *
243
     * @return \TelegramBot\TelegramBotManager\BotManager
244
     * @throws \Longman\TelegramBot\Exception\TelegramException
245
     */
246
    public function setBotExtras(): self
247
    {
248
        $this->setBotExtrasTelegram();
249
        $this->setBotExtrasRequest();
250
251
        return $this;
252
    }
253
254
    /**
255
     * Set extra bot parameters for Telegram object.
256
     *
257
     * @return \TelegramBot\TelegramBotManager\BotManager
258
     * @throws \Longman\TelegramBot\Exception\TelegramException
259
     */
260
    protected function setBotExtrasTelegram(): self
261
    {
262
        $simple_extras = [
263
            'admins'         => 'enableAdmins',
264
            'mysql'          => 'enableMySql',
265
            'commands.paths' => 'addCommandsPaths',
266
            'custom_input'   => 'setCustomInput',
267
            'paths.download' => 'setDownloadPath',
268
            'paths.upload'   => 'setUploadPath',
269
        ];
270
        // For simple telegram extras, just pass the single param value to the Telegram method.
271
        foreach ($simple_extras as $param_key => $method) {
272
            $param = $this->params->getBotParam($param_key);
273
            if (null !== $param) {
274
                $this->telegram->$method($param);
275
            }
276
        }
277
278
        // Custom command configs.
279
        $command_configs = $this->params->getBotParam('commands.configs', []);
280
        foreach ($command_configs as $command => $config) {
281
            $this->telegram->setCommandConfig($command, $config);
282
        }
283
284
        // Botan with options.
285
        if ($botan_token = $this->params->getBotParam('botan.token')) {
286
            $botan_options = $this->params->getBotParam('botan.options', []);
287
            $this->telegram->enableBotan($botan_token, $botan_options);
288
        }
289
290
        return $this;
291
    }
292
293
    /**
294
     * Set extra bot parameters for Request class.
295
     *
296
     * @return \TelegramBot\TelegramBotManager\BotManager
297
     * @throws \Longman\TelegramBot\Exception\TelegramException
298
     */
299
    protected function setBotExtrasRequest(): self
300
    {
301
        $request_extras = [
302
            // None at the moment...
303
        ];
304
        // For request extras, just pass the single param value to the Request method.
305
        foreach ($request_extras as $param_key => $method) {
306
            $param = $this->params->getBotParam($param_key);
307
            if (null !== $param) {
308
                Request::$method($param);
309
            }
310
        }
311
312
        // Special cases.
313
        $limiter_enabled = $this->params->getBotParam('limiter.enabled');
314
        if ($limiter_enabled !== null) {
315
            $limiter_options = $this->params->getBotParam('limiter.options', []);
316
            Request::setLimiter($limiter_enabled, $limiter_options);
317
        }
318
319
        return $this;
320
    }
321
322
    /**
323
     * Handle the request, which calls either the Webhook or getUpdates method respectively.
324
     *
325
     * @return \TelegramBot\TelegramBotManager\BotManager
326
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
327
     * @throws \Longman\TelegramBot\Exception\TelegramException
328
     */
329
    public function handleRequest(): self
330
    {
331
        if (empty($this->params->getBotParam('webhook.url'))) {
332
            if ($loop_time = $this->getLoopTime()) {
333
                $this->handleGetUpdatesLoop($loop_time, $this->getLoopInterval());
334
            } else {
335
                $this->handleGetUpdates();
336
            }
337
        } else {
338
            $this->handleWebhook();
339
        }
340
341
        return $this;
342
    }
343
344
    /**
345
     * Handle cron.
346
     *
347
     * @return \TelegramBot\TelegramBotManager\BotManager
348
     * @throws \Longman\TelegramBot\Exception\TelegramException
349
     */
350
    public function handleCron(): self
351
    {
352
        $groups = explode(',', $this->params->getScriptParam('g', 'default'));
353
354
        $commands = [];
355
        foreach ($groups as $group) {
356
            $commands = array_merge($commands, $this->params->getBotParam('cron.groups.' . $group, []));
357
        }
358
        $this->telegram->runCommands($commands);
359
360
        return $this;
361
    }
362
363
    /**
364
     * Get the number of seconds the script should loop.
365
     *
366
     * @return int
367
     */
368 View Code Duplication
    public function getLoopTime(): int
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
369
    {
370
        $loop_time = $this->params->getScriptParam('l');
371
372
        if (null === $loop_time) {
373
            return 0;
374
        }
375
376
        if (is_string($loop_time) && '' === trim($loop_time)) {
377
            return 604800; // Default to 7 days.
378
        }
379
380
        return max(0, (int) $loop_time);
381
    }
382
383
    /**
384
     * Get the number of seconds the script should wait after each getUpdates request.
385
     *
386
     * @return int
387
     */
388 View Code Duplication
    public function getLoopInterval(): int
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
389
    {
390
        $interval_time = $this->params->getScriptParam('i');
391
392
        if (null === $interval_time || (is_string($interval_time) && '' === trim($interval_time))) {
393
            return 2;
394
        }
395
396
        // Minimum interval is 1 second.
397
        return max(1, (int) $interval_time);
398
    }
399
400
    /**
401
     * Loop the getUpdates method for the passed amount of seconds.
402
     *
403
     * @param int $loop_time_in_seconds
404
     * @param int $loop_interval_in_seconds
405
     *
406
     * @return \TelegramBot\TelegramBotManager\BotManager
407
     * @throws \Longman\TelegramBot\Exception\TelegramException
408
     */
409
    public function handleGetUpdatesLoop(int $loop_time_in_seconds, int $loop_interval_in_seconds = 2): self
410
    {
411
        // Remember the time we started this loop.
412
        $now = time();
413
414
        $this->handleOutput('Looping getUpdates until ' . date('Y-m-d H:i:s', $now + $loop_time_in_seconds) . PHP_EOL);
415
416
        while ($now > time() - $loop_time_in_seconds) {
417
            $this->handleGetUpdates();
418
419
            // Chill a bit.
420
            sleep($loop_interval_in_seconds);
421
        }
422
423
        return $this;
424
    }
425
426
    /**
427
     * Handle the updates using the getUpdates method.
428
     *
429
     * @return \TelegramBot\TelegramBotManager\BotManager
430
     * @throws \Longman\TelegramBot\Exception\TelegramException
431
     */
432
    public function handleGetUpdates(): self
433
    {
434
        $output = date('Y-m-d H:i:s', time()) . ' - ';
435
436
        $response = $this->telegram->handleGetUpdates();
437
        if ($response->isOk()) {
438
            $results = array_filter((array) $response->getResult());
439
440
            $output .= sprintf('Updates processed: %d' . PHP_EOL, count($results));
441
442
            /** @var Entities\Update $result */
443
            foreach ($results as $result) {
444
                $chat_id = 0;
445
                $text    = 'Nothing';
446
447
                $update_content = $result->getUpdateContent();
448
                if ($update_content instanceof Entities\Message) {
449
                    $chat_id = $update_content->getFrom()->getId();
450
                    $text    = $update_content->getText();
451
                } elseif ($update_content instanceof Entities\InlineQuery ||
452
                          $update_content instanceof Entities\ChosenInlineResult
453
                ) {
454
                    $chat_id = $update_content->getFrom()->getId();
455
                    $text    = $update_content->getQuery();
456
                }
457
458
                $output .= sprintf(
459
                    '%d: %s' . PHP_EOL,
460
                    $chat_id,
461
                    preg_replace('/\s+/', ' ', trim($text))
462
                );
463
            }
464
        } else {
465
            $output .= sprintf('Failed to fetch updates: %s' . PHP_EOL, $response->printError());
466
        }
467
468
        $this->handleOutput($output);
469
470
        return $this;
471
    }
472
473
    /**
474
     * Handle the updates using the Webhook method.
475
     *
476
     * @return \TelegramBot\TelegramBotManager\BotManager
477
     * @throws \Longman\TelegramBot\Exception\TelegramException
478
     * @throws \TelegramBot\TelegramBotManager\Exception\InvalidAccessException
479
     */
480
    public function handleWebhook(): self
481
    {
482
        $this->telegram->handle();
483
484
        return $this;
485
    }
486
487
    /**
488
     * Return the current test output and clear it.
489
     *
490
     * @return string
491
     */
492
    public function getOutput(): string
493
    {
494
        $output       = $this->output;
495
        $this->output = '';
496
497
        return $output;
498
    }
499
500
    /**
501
     * Check if this is a valid request coming from a Telegram API IP address.
502
     *
503
     * @link https://core.telegram.org/bots/webhooks#the-short-version
504
     *
505
     * @return bool
506
     */
507
    public function isValidRequest(): bool
508
    {
509
        // If we're running from CLI, requests are always valid, unless we're running the tests.
510
        if ((!self::inTest() && 'cli' === PHP_SAPI) || false === $this->params->getBotParam('validate_request')) {
511
            return true;
512
        }
513
514
        $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
515
        foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR'] as $key) {
516
            if (filter_var($_SERVER[$key] ?? null, FILTER_VALIDATE_IP)) {
517
                $ip = $_SERVER[$key];
518
                break;
519
            }
520
        }
521
522
        return Ip::match($ip, array_merge(
523
            [self::TELEGRAM_IP_RANGE],
524
            (array) $this->params->getBotParam('valid_ips', [])
525
        ));
526
    }
527
}
528