Completed
Push — develop ( 2587d4...2d5867 )
by Armando
03:59
created

BotManager::initLogging()   B

Complexity

Conditions 6
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 8.8571
c 0
b 0
f 0
cc 6
eloc 8
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->action->isAction(['set', 'unset', 'reset'])) {
127
            $this->validateAndSetWebhook();
128
        } elseif ($this->action->isAction('handle')) {
129
            // Set any extras.
130
            $this->setBotExtras();
131
            $this->handleRequest();
132
        }
133
134
        return $this;
135
    }
136
137
    /**
138
     * Initialise all loggers.
139
     *
140
     * @return \NPM\TelegramBotManager\BotManager
141
     * @throws \Exception
142
     */
143
    public function initLogging(): self
144
    {
145
        $logging = $this->params->getBotParam('logging');
146
        if (is_array($logging)) {
147
            /** @var array $logging */
148
            foreach ($logging as $logger => $logfile) {
149
                ('debug' === $logger) && TelegramLog::initDebugLog($logfile);
150
                ('error' === $logger) && TelegramLog::initErrorLog($logfile);
151
                ('update' === $logger) && TelegramLog::initUpdateLog($logfile);
152
            }
153
        }
154
155
        return $this;
156
    }
157
158
    /**
159
     * Make sure the passed secret is valid.
160
     *
161
     * @param bool $force Force validation, even on CLI.
162
     *
163
     * @return \NPM\TelegramBotManager\BotManager
164
     * @throws \NPM\TelegramBotManager\Exception\InvalidAccessException
165
     */
166
    public function validateSecret(bool $force = false): self
167
    {
168
        // If we're running from CLI, secret isn't necessary.
169
        if ($force || 'cli' !== PHP_SAPI) {
170
            $secret     = $this->params->getBotParam('secret');
171
            $secret_get = $this->params->getScriptParam('s');
172
            if ($secret_get !== $secret) {
173
                throw new InvalidAccessException('Invalid access');
174
            }
175
        }
176
177
        return $this;
178
    }
179
180
    /**
181
     * Make sure the webhook is valid and perform the requested webhook operation.
182
     *
183
     * @return \NPM\TelegramBotManager\BotManager
184
     * @throws \Longman\TelegramBot\Exception\TelegramException
185
     * @throws \NPM\TelegramBotManager\Exception\InvalidWebhookException
186
     */
187
    public function validateAndSetWebhook(): self
188
    {
189
        $webhook = $this->params->getBotParam('webhook');
190
        if (empty($webhook) && $this->action->isAction(['set', 'reset'])) {
191
            throw new InvalidWebhookException('Invalid webhook');
192
        }
193
194
        if ($this->action->isAction(['unset', 'reset'])) {
195
            $this->handleOutput($this->telegram->deleteWebhook()->getDescription() . PHP_EOL);
196
            // When resetting the webhook, sleep for a bit to prevent too many requests.
197
            $this->action->isAction('reset') && sleep(1);
198
        }
199
200
        if ($this->action->isAction(['set', 'reset'])) {
201
            $webhook_params = array_filter([
202
                'certificate'     => $this->params->getBotParam('certificate'),
203
                'max_connections' => $this->params->getBotParam('max_connections'),
204
                'allowed_updates' => $this->params->getBotParam('allowed_updates'),
205
            ]);
206
207
            $this->handleOutput(
208
                $this->telegram->setWebhook(
209
                    $webhook . '?a=handle&s=' . $this->params->getBotParam('secret'),
210
                    $webhook_params
211
                )->getDescription() . PHP_EOL
212
            );
213
        }
214
215
        return $this;
216
    }
217
218
    /**
219
     * Save the test output and echo it if we're not in a test.
220
     *
221
     * @param string $output
222
     *
223
     * @return \NPM\TelegramBotManager\BotManager
224
     */
225
    private function handleOutput(string $output): self
226
    {
227
        $this->output .= $output;
228
229
        if (!self::inTest()) {
230
            echo $output;
231
        }
232
233
        return $this;
234
    }
235
236
    /**
237
     * Set any extra bot features that have been assigned on construction.
238
     *
239
     * @return \NPM\TelegramBotManager\BotManager
240
     * @throws \Longman\TelegramBot\Exception\TelegramException
241
     */
242
    public function setBotExtras(): self
243
    {
244
        $telegram_extras = [
245
            'admins'         => 'enableAdmins',
246
            'mysql'          => 'enableMySql',
247
            'botan_token'    => 'enableBotan',
248
            'commands_paths' => 'addCommandsPaths',
249
            'custom_input'   => 'setCustomInput',
250
            'download_path'  => 'setDownloadPath',
251
            'upload_path'    => 'setUploadPath',
252
        ];
253
        // For telegram extras, just pass the single param value to the Telegram method.
254
        foreach ($telegram_extras as $param_key => $method) {
255
            $param = $this->params->getBotParam($param_key);
256
            if (null !== $param) {
257
                $this->telegram->$method($param);
258
            }
259
        }
260
261
        $request_extras = [
262
            // None at the moment...
263
        ];
264
        // For request extras, just pass the single param value to the Request method.
265
        foreach ($request_extras as $param_key => $method) {
266
            $param = $this->params->getBotParam($param_key);
267
            if (null !== $param) {
268
                Request::$method($param);
269
            }
270
        }
271
272
        // Special cases.
273
        $limiter = $this->params->getBotParam('limiter', []);
274
        if (is_array($limiter)) {
275
            Request::setLimiter(true, $limiter);
276
        } else {
277
            Request::setLimiter($limiter);
278
        }
279
280
        $command_configs = $this->params->getBotParam('command_configs');
281
        if (is_array($command_configs)) {
282
            /** @var array $command_configs */
283
            foreach ($command_configs as $command => $config) {
284
                $this->telegram->setCommandConfig($command, $config);
285
            }
286
        }
287
288
        return $this;
289
    }
290
291
    /**
292
     * Handle the request, which calls either the Webhook or getUpdates method respectively.
293
     *
294
     * @return \NPM\TelegramBotManager\BotManager
295
     * @throws \NPM\TelegramBotManager\Exception\InvalidAccessException
296
     * @throws \Longman\TelegramBot\Exception\TelegramException
297
     */
298
    public function handleRequest(): self
299
    {
300
        if (empty($this->params->getBotParam('webhook'))) {
301
            if ($loop_time = $this->getLoopTime()) {
302
                $this->handleGetUpdatesLoop($loop_time, $this->getLoopInterval());
303
            } else {
304
                $this->handleGetUpdates();
305
            }
306
        } else {
307
            $this->handleWebhook();
308
        }
309
310
        return $this;
311
    }
312
313
    /**
314
     * Get the number of seconds the script should loop.
315
     *
316
     * @return int
317
     */
318 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...
319
    {
320
        $loop_time = $this->params->getScriptParam('l');
321
322
        if (null === $loop_time) {
323
            return 0;
324
        }
325
326
        if (is_string($loop_time) && '' === trim($loop_time)) {
327
            return 604800; // Default to 7 days.
328
        }
329
330
        return max(0, (int) $loop_time);
331
    }
332
333
    /**
334
     * Get the number of seconds the script should wait after each getUpdates request.
335
     *
336
     * @return int
337
     */
338 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...
339
    {
340
        $interval_time = $this->params->getScriptParam('i');
341
342
        if (null === $interval_time || (is_string($interval_time) && '' === trim($interval_time))) {
343
            return 2;
344
        }
345
346
        // Minimum interval is 1 second.
347
        return max(1, (int) $interval_time);
348
    }
349
350
    /**
351
     * Loop the getUpdates method for the passed amount of seconds.
352
     *
353
     * @param int $loop_time_in_seconds
354
     * @param int $loop_interval_in_seconds
355
     *
356
     * @return \NPM\TelegramBotManager\BotManager
357
     * @throws \Longman\TelegramBot\Exception\TelegramException
358
     */
359
    public function handleGetUpdatesLoop(int $loop_time_in_seconds, int $loop_interval_in_seconds = 2): self
360
    {
361
        // Remember the time we started this loop.
362
        $now = time();
363
364
        $this->handleOutput('Looping getUpdates until ' . date('Y-m-d H:i:s', $now + $loop_time_in_seconds) . PHP_EOL);
365
366
        while ($now > time() - $loop_time_in_seconds) {
367
            $this->handleGetUpdates();
368
369
            // Chill a bit.
370
            sleep($loop_interval_in_seconds);
371
        }
372
373
        return $this;
374
    }
375
376
    /**
377
     * Handle the updates using the getUpdates method.
378
     *
379
     * @return \NPM\TelegramBotManager\BotManager
380
     * @throws \Longman\TelegramBot\Exception\TelegramException
381
     */
382
    public function handleGetUpdates(): self
383
    {
384
        $output = date('Y-m-d H:i:s', time()) . ' - ';
385
386
        $response = $this->telegram->handleGetUpdates();
387
        if ($response->isOk()) {
388
            $results = array_filter((array) $response->getResult());
389
390
            $output .= sprintf('Updates processed: %d' . PHP_EOL, count($results));
391
392
            /** @var Entities\Update $result */
393
            foreach ($results as $result) {
394
                $chat_id = 0;
395
                $text    = 'Nothing';
396
397
                $update_content = $result->getUpdateContent();
398
                if ($update_content instanceof Entities\Message) {
399
                    $chat_id = $update_content->getFrom()->getId();
400
                    $text    = $update_content->getText();
401
                } elseif ($update_content instanceof Entities\InlineQuery ||
402
                          $update_content instanceof Entities\ChosenInlineResult
403
                ) {
404
                    $chat_id = $update_content->getFrom()->getId();
405
                    $text    = $update_content->getQuery();
406
                }
407
408
                $output .= sprintf(
409
                    '%d: %s' . PHP_EOL,
410
                    $chat_id,
411
                    preg_replace('/\s+/', ' ', trim($text))
412
                );
413
            }
414
        } else {
415
            $output .= sprintf('Failed to fetch updates: %s' . PHP_EOL, $response->printError());
416
        }
417
418
        $this->handleOutput($output);
419
420
        return $this;
421
    }
422
423
    /**
424
     * Handle the updates using the Webhook method.
425
     *
426
     * @return \NPM\TelegramBotManager\BotManager
427
     * @throws \Longman\TelegramBot\Exception\TelegramException
428
     * @throws \NPM\TelegramBotManager\Exception\InvalidAccessException
429
     */
430
    public function handleWebhook(): self
431
    {
432
        if (!$this->isValidRequest()) {
433
            throw new InvalidAccessException('Invalid access');
434
        }
435
436
        $this->telegram->handle();
437
438
        return $this;
439
    }
440
441
    /**
442
     * Return the current test output and clear it.
443
     *
444
     * @return string
445
     */
446
    public function getOutput(): string
447
    {
448
        $output       = $this->output;
449
        $this->output = '';
450
451
        return $output;
452
    }
453
454
    /**
455
     * Check if this is a valid request coming from a Telegram API IP address.
456
     *
457
     * @link https://core.telegram.org/bots/webhooks#the-short-version
458
     *
459
     * @return bool
460
     */
461
    public function isValidRequest(): bool
462
    {
463
        if ((!self::inTest() && 'cli' === PHP_SAPI) || false === $this->params->getBotParam('validate_request')) {
464
            return true;
465
        }
466
467
        $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
468
        foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR'] as $key) {
469
            if (filter_var($_SERVER[$key] ?? null, FILTER_VALIDATE_IP)) {
470
                $ip = $_SERVER[$key];
471
                break;
472
            }
473
        }
474
475
        $valid_ips = array_merge(
476
            [self::TELEGRAM_IP_RANGE],
477
            (array) $this->params->getBotParam('valid_ips', [])
478
        );
479
        foreach ($valid_ips as $valid_ip) {
480
            if (IpTools::ipInRange($ip, $valid_ip)) {
481
                return true;
482
            }
483
        }
484
485
        return false;
486
    }
487
}
488