Completed
Push — develop ( da9458...baec8e )
by Armando
01:58
created

BotManager::handleCron()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 2
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 NPM\TelegramBotManager;
12
13
use Allty\Utils\IpTools;
14
use Longman\TelegramBot\Entities;
15
use Longman\TelegramBot\Request;
16
use Longman\TelegramBot\Telegram;
17
use Longman\TelegramBot\TelegramLog;
18
use NPM\TelegramBotManager\Exception\InvalidAccessException;
19
use NPM\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 \NPM\TelegramBotManager\Params Object that manages the parameters.
40
     */
41
    private $params;
42
43
    /**
44
     * @var \NPM\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 \NPM\TelegramBotManager\Exception\InvalidParamsException
54
     * @throws \NPM\TelegramBotManager\Exception\InvalidActionException
55
     * @throws \Longman\TelegramBot\Exception\TelegramException
56
     */
57
    public function __construct(array $params)
58
    {
59
        $this->params = new Params($params);
60
        $this->action = new Action($this->params->getScriptParam('a'));
61
62
        // Set up a new Telegram instance.
63
        $this->telegram = new Telegram(
64
            $this->params->getBotParam('api_key'),
65
            $this->params->getBotParam('bot_username')
66
        );
67
    }
68
69
    /**
70
     * Check if we're busy running the PHPUnit tests.
71
     *
72
     * @return bool
73
     */
74
    public static function inTest(): bool
75
    {
76
        return defined('PHPUNIT_TEST') && PHPUNIT_TEST === true;
77
    }
78
79
    /**
80
     * Return the Telegram object.
81
     *
82
     * @return \Longman\TelegramBot\Telegram
83
     */
84
    public function getTelegram(): Telegram
85
    {
86
        return $this->telegram;
87
    }
88
89
    /**
90
     * Get the Params object.
91
     *
92
     * @return \NPM\TelegramBotManager\Params
93
     */
94
    public function getParams(): Params
95
    {
96
        return $this->params;
97
    }
98
99
    /**
100
     * Get the Action object.
101
     *
102
     * @return \NPM\TelegramBotManager\Action
103
     */
104
    public function getAction(): Action
105
    {
106
        return $this->action;
107
    }
108
109
    /**
110
     * Run this thing in all its glory!
111
     *
112
     * @return \NPM\TelegramBotManager\BotManager
113
     * @throws \Longman\TelegramBot\Exception\TelegramException
114
     * @throws \NPM\TelegramBotManager\Exception\InvalidAccessException
115
     * @throws \NPM\TelegramBotManager\Exception\InvalidWebhookException
116
     * @throws \Exception
117
     */
118
    public function run(): self
119
    {
120
        // Initialise logging.
121
        $this->initLogging();
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
     * @return \NPM\TelegramBotManager\BotManager
147
     * @throws \Exception
148
     */
149
    public function initLogging(): self
150
    {
151
        $logging = $this->params->getBotParam('logging');
152
        if (is_array($logging)) {
153
            /** @var array $logging */
154
            foreach ($logging as $logger => $logfile) {
155
                ('debug' === $logger) && TelegramLog::initDebugLog($logfile);
156
                ('error' === $logger) && TelegramLog::initErrorLog($logfile);
157
                ('update' === $logger) && TelegramLog::initUpdateLog($logfile);
158
            }
159
        }
160
161
        return $this;
162
    }
163
164
    /**
165
     * Make sure the passed secret is valid.
166
     *
167
     * @param bool $force Force validation, even on CLI.
168
     *
169
     * @return \NPM\TelegramBotManager\BotManager
170
     * @throws \NPM\TelegramBotManager\Exception\InvalidAccessException
171
     */
172
    public function validateSecret(bool $force = false): self
173
    {
174
        // If we're running from CLI, secret isn't necessary.
175
        if ($force || 'cli' !== PHP_SAPI) {
176
            $secret     = $this->params->getBotParam('secret');
177
            $secret_get = $this->params->getScriptParam('s');
178
            if ($secret_get !== $secret) {
179
                throw new InvalidAccessException('Invalid access');
180
            }
181
        }
182
183
        return $this;
184
    }
185
186
    /**
187
     * Make sure the webhook is valid and perform the requested webhook operation.
188
     *
189
     * @return \NPM\TelegramBotManager\BotManager
190
     * @throws \Longman\TelegramBot\Exception\TelegramException
191
     * @throws \NPM\TelegramBotManager\Exception\InvalidWebhookException
192
     */
193
    public function validateAndSetWebhook(): self
194
    {
195
        $webhook = $this->params->getBotParam('webhook');
196
        if (empty($webhook['url'] ?? null) && $this->action->isAction(['set', 'reset'])) {
197
            throw new InvalidWebhookException('Invalid webhook');
198
        }
199
200
        if ($this->action->isAction(['unset', 'reset'])) {
201
            $this->handleOutput($this->telegram->deleteWebhook()->getDescription() . PHP_EOL);
202
            // When resetting the webhook, sleep for a bit to prevent too many requests.
203
            $this->action->isAction('reset') && sleep(1);
204
        }
205
206
        if ($this->action->isAction(['set', 'reset'])) {
207
            $webhook_params = array_filter([
208
                'certificate'     => $webhook['certificate'] ?? null,
209
                'max_connections' => $webhook['max_connections'] ?? null,
210
                'allowed_updates' => $webhook['allowed_updates'] ?? null,
211
            ]);
212
213
            $this->handleOutput(
214
                $this->telegram->setWebhook(
215
                    $webhook['url'] . '?a=handle&s=' . $this->params->getBotParam('secret'),
216
                    $webhook_params
217
                )->getDescription() . PHP_EOL
218
            );
219
        }
220
221
        return $this;
222
    }
223
224
    /**
225
     * Save the test output and echo it if we're not in a test.
226
     *
227
     * @param string $output
228
     *
229
     * @return \NPM\TelegramBotManager\BotManager
230
     */
231
    private function handleOutput(string $output): self
232
    {
233
        $this->output .= $output;
234
235
        if (!self::inTest()) {
236
            echo $output;
237
        }
238
239
        return $this;
240
    }
241
242
    /**
243
     * Set any extra bot features that have been assigned on construction.
244
     *
245
     * @return \NPM\TelegramBotManager\BotManager
246
     * @throws \Longman\TelegramBot\Exception\TelegramException
247
     */
248
    public function setBotExtras(): self
249
    {
250
        $this->setBotExtrasTelegram();
251
        $this->setBotExtrasRequest();
252
253
        return $this;
254
    }
255
256
    /**
257
     * Set extra bot parameters for Telegram object.
258
     *
259
     * @return \NPM\TelegramBotManager\BotManager
260
     * @throws \Longman\TelegramBot\Exception\TelegramException
261
     */
262
    protected function setBotExtrasTelegram(): self
263
    {
264
        $simple_extras = [
265
            'admins'         => 'enableAdmins',
266
            'mysql'          => 'enableMySql',
267
            'commands.paths' => 'addCommandsPaths',
268
            'custom_input'   => 'setCustomInput',
269
            'paths.download' => 'setDownloadPath',
270
            'paths.upload'   => 'setUploadPath',
271
        ];
272
        // For simple telegram extras, just pass the single param value to the Telegram method.
273
        foreach ($simple_extras as $param_key => $method) {
274
            $param = $this->params->getBotParam($param_key);
275
            if (null !== $param) {
276
                $this->telegram->$method($param);
277
            }
278
        }
279
280
        // Custom command configs.
281
        $command_configs = $this->params->getBotParam('commands.configs', []);
282
        foreach ($command_configs as $command => $config) {
283
            $this->telegram->setCommandConfig($command, $config);
284
        }
285
286
        // Botan with options.
287
        if ($botan_token = $this->params->getBotParam('botan.token')) {
288
            $botan_options = $this->params->getBotParam('botan.options', []);
289
            $this->telegram->enableBotan($botan_token, $botan_options);
290
        }
291
292
        return $this;
293
    }
294
295
    /**
296
     * Set extra bot parameters for Request class.
297
     *
298
     * @return \NPM\TelegramBotManager\BotManager
299
     * @throws \Longman\TelegramBot\Exception\TelegramException
300
     */
301
    protected function setBotExtrasRequest(): self
302
    {
303
        $request_extras = [
304
            // None at the moment...
305
        ];
306
        // For request extras, just pass the single param value to the Request method.
307
        foreach ($request_extras as $param_key => $method) {
308
            $param = $this->params->getBotParam($param_key);
309
            if (null !== $param) {
310
                Request::$method($param);
311
            }
312
        }
313
314
        // Special cases.
315
        $limiter_enabled = $this->params->getBotParam('limiter.enabled');
316
        if ($limiter_enabled !== null) {
317
            $limiter_options = $this->params->getBotParam('limiter.options', []);
318
            Request::setLimiter($limiter_enabled, $limiter_options);
319
        }
320
321
        return $this;
322
    }
323
324
    /**
325
     * Handle the request, which calls either the Webhook or getUpdates method respectively.
326
     *
327
     * @return \NPM\TelegramBotManager\BotManager
328
     * @throws \NPM\TelegramBotManager\Exception\InvalidAccessException
329
     * @throws \Longman\TelegramBot\Exception\TelegramException
330
     */
331
    public function handleRequest(): self
332
    {
333
        if (empty($this->params->getBotParam('webhook.url'))) {
334
            if ($loop_time = $this->getLoopTime()) {
335
                $this->handleGetUpdatesLoop($loop_time, $this->getLoopInterval());
336
            } else {
337
                $this->handleGetUpdates();
338
            }
339
        } else {
340
            $this->handleWebhook();
341
        }
342
343
        return $this;
344
    }
345
346
    /**
347
     * Handle cron.
348
     *
349
     * @return \NPM\TelegramBotManager\BotManager
350
     * @throws \Longman\TelegramBot\Exception\TelegramException
351
     */
352
    public function handleCron(): self
353
    {
354
        $groups = explode(',', $this->params->getScriptParam('g', 'default'));
355
356
        $commands = [];
357
        foreach ($groups as $group) {
358
            $commands = array_merge($commands, $this->params->getBotParam('cron.groups.' . $group, []));
359
        }
360
        $this->telegram->runCommands($commands);
361
362
        return $this;
363
    }
364
365
    /**
366
     * Get the number of seconds the script should loop.
367
     *
368
     * @return int
369
     */
370 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...
371
    {
372
        $loop_time = $this->params->getScriptParam('l');
373
374
        if (null === $loop_time) {
375
            return 0;
376
        }
377
378
        if (is_string($loop_time) && '' === trim($loop_time)) {
379
            return 604800; // Default to 7 days.
380
        }
381
382
        return max(0, (int) $loop_time);
383
    }
384
385
    /**
386
     * Get the number of seconds the script should wait after each getUpdates request.
387
     *
388
     * @return int
389
     */
390 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...
391
    {
392
        $interval_time = $this->params->getScriptParam('i');
393
394
        if (null === $interval_time || (is_string($interval_time) && '' === trim($interval_time))) {
395
            return 2;
396
        }
397
398
        // Minimum interval is 1 second.
399
        return max(1, (int) $interval_time);
400
    }
401
402
    /**
403
     * Loop the getUpdates method for the passed amount of seconds.
404
     *
405
     * @param int $loop_time_in_seconds
406
     * @param int $loop_interval_in_seconds
407
     *
408
     * @return \NPM\TelegramBotManager\BotManager
409
     * @throws \Longman\TelegramBot\Exception\TelegramException
410
     */
411
    public function handleGetUpdatesLoop(int $loop_time_in_seconds, int $loop_interval_in_seconds = 2): self
412
    {
413
        // Remember the time we started this loop.
414
        $now = time();
415
416
        $this->handleOutput('Looping getUpdates until ' . date('Y-m-d H:i:s', $now + $loop_time_in_seconds) . PHP_EOL);
417
418
        while ($now > time() - $loop_time_in_seconds) {
419
            $this->handleGetUpdates();
420
421
            // Chill a bit.
422
            sleep($loop_interval_in_seconds);
423
        }
424
425
        return $this;
426
    }
427
428
    /**
429
     * Handle the updates using the getUpdates method.
430
     *
431
     * @return \NPM\TelegramBotManager\BotManager
432
     * @throws \Longman\TelegramBot\Exception\TelegramException
433
     */
434
    public function handleGetUpdates(): self
435
    {
436
        $output = date('Y-m-d H:i:s', time()) . ' - ';
437
438
        $response = $this->telegram->handleGetUpdates();
439
        if ($response->isOk()) {
440
            $results = array_filter((array) $response->getResult());
441
442
            $output .= sprintf('Updates processed: %d' . PHP_EOL, count($results));
443
444
            /** @var Entities\Update $result */
445
            foreach ($results as $result) {
446
                $chat_id = 0;
447
                $text    = 'Nothing';
448
449
                $update_content = $result->getUpdateContent();
450
                if ($update_content instanceof Entities\Message) {
451
                    $chat_id = $update_content->getFrom()->getId();
452
                    $text    = $update_content->getText();
453
                } elseif ($update_content instanceof Entities\InlineQuery ||
454
                          $update_content instanceof Entities\ChosenInlineResult
455
                ) {
456
                    $chat_id = $update_content->getFrom()->getId();
457
                    $text    = $update_content->getQuery();
458
                }
459
460
                $output .= sprintf(
461
                    '%d: %s' . PHP_EOL,
462
                    $chat_id,
463
                    preg_replace('/\s+/', ' ', trim($text))
464
                );
465
            }
466
        } else {
467
            $output .= sprintf('Failed to fetch updates: %s' . PHP_EOL, $response->printError());
468
        }
469
470
        $this->handleOutput($output);
471
472
        return $this;
473
    }
474
475
    /**
476
     * Handle the updates using the Webhook method.
477
     *
478
     * @return \NPM\TelegramBotManager\BotManager
479
     * @throws \Longman\TelegramBot\Exception\TelegramException
480
     * @throws \NPM\TelegramBotManager\Exception\InvalidAccessException
481
     */
482
    public function handleWebhook(): self
483
    {
484
        $this->telegram->handle();
485
486
        return $this;
487
    }
488
489
    /**
490
     * Return the current test output and clear it.
491
     *
492
     * @return string
493
     */
494
    public function getOutput(): string
495
    {
496
        $output       = $this->output;
497
        $this->output = '';
498
499
        return $output;
500
    }
501
502
    /**
503
     * Check if this is a valid request coming from a Telegram API IP address.
504
     *
505
     * @link https://core.telegram.org/bots/webhooks#the-short-version
506
     *
507
     * @return bool
508
     */
509
    public function isValidRequest(): bool
510
    {
511
        // If we're running from CLI, requests are always valid, unless we're running the tests.
512
        if ((!self::inTest() && 'cli' === PHP_SAPI) || false === $this->params->getBotParam('validate_request')) {
513
            return true;
514
        }
515
516
        $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
517
        foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR'] as $key) {
518
            if (filter_var($_SERVER[$key] ?? null, FILTER_VALIDATE_IP)) {
519
                $ip = $_SERVER[$key];
520
                break;
521
            }
522
        }
523
524
        $valid_ips = array_merge(
525
            [self::TELEGRAM_IP_RANGE],
526
            (array) $this->params->getBotParam('valid_ips', [])
527
        );
528
        foreach ($valid_ips as $valid_ip) {
529
            if (IpTools::ipInRange($ip, $valid_ip)) {
530
                return true;
531
            }
532
        }
533
534
        return false;
535
    }
536
}
537