Completed
Push — master ( 67ee26...fdd38c )
by Armando
03:35
created

BotManager::isValidRequest()   B

Complexity

Conditions 5
Paths 7

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 7
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 Longman\TelegramBot\Entities;
14
use Longman\TelegramBot\Telegram;
15
use Longman\TelegramBot\TelegramLog;
16
17
/**
18
 * Class BotManager.php
19
 *
20
 * Leave all member variables public to allow easy modification.
21
 *
22
 * @package NPM\TelegramBotManager
23
 */
24
class BotManager
25
{
26
    /**
27
     * @var string Telegram post servers lower IP limit
28
     */
29
    const TELEGRAM_IP_LOWER = '149.154.167.197';
30
31
    /**
32
     * @var string Telegram post servers upper IP limit
33
     */
34
    const TELEGRAM_IP_UPPER = '149.154.167.233';
35
36
    /**
37
     * @var string The output for testing, instead of echoing
38
     */
39
    private $output = '';
40
41
    /**
42
     * @var \Longman\TelegramBot\Telegram
43
     */
44
    private $telegram;
45
46
    /**
47
     * @var \NPM\TelegramBotManager\Params Object that manages the parameters.
48
     */
49
    private $params;
50
51
    /**
52
     * @var \NPM\TelegramBotManager\Action Object that contains the current action.
53
     */
54
    private $action;
55
56
    /**
57
     * BotManager constructor.
58
     *
59
     * @param array $params
60
     *
61
     * @throws \InvalidArgumentException
62
     */
63
    public function __construct(array $params)
64
    {
65
        $this->params = new Params($params);
66
        $this->action = new Action($this->params->getScriptParam('a'));
67
    }
68
69
    /**
70
     * Return the Telegram object.
71
     *
72
     * @return \Longman\TelegramBot\Telegram
73
     */
74
    public function getTelegram(): Telegram
75
    {
76
        return $this->telegram;
77
    }
78
79
    /**
80
     * Get the Params object.
81
     *
82
     * @return \NPM\TelegramBotManager\Params
83
     */
84
    public function getParams(): Params
85
    {
86
        return $this->params;
87
    }
88
89
    /**
90
     * Get the Action object.
91
     *
92
     * @return \NPM\TelegramBotManager\Action
93
     */
94
    public function getAction(): Action
95
    {
96
        return $this->action;
97
    }
98
99
    /**
100
     * Run this thing in all its glory!
101
     *
102
     * @return \NPM\TelegramBotManager\BotManager
103
     * @throws \Longman\TelegramBot\Exception\TelegramException
104
     * @throws \InvalidArgumentException
105
     * @throws \Exception
106
     */
107
    public function run(): self
108
    {
109
        // Initialise logging.
110
        $this->initLogging();
111
112
        // Make sure this is a valid call.
113
        $this->validateSecret();
114
115
        // Set up a new Telegram instance.
116
        $this->telegram = new Telegram(
117
            $this->params->getBotParam('api_key'),
118
            $this->params->getBotParam('botname')
119
        );
120
121
        if ($this->action->isAction(['set', 'unset', 'reset'])) {
122
            $this->validateAndSetWebhook();
123
        } elseif ($this->action->isAction('handle')) {
124
            // Set any extras.
125
            $this->setBotExtras();
126
            $this->handleRequest();
127
        }
128
129
        return $this;
130
    }
131
132
    /**
133
     * Initialise all loggers.
134
     *
135
     * @return \NPM\TelegramBotManager\BotManager
136
     * @throws \Exception
137
     */
138
    public function initLogging(): self
139
    {
140
        $logging = $this->params->getBotParam('logging');
141
        if (is_array($logging)) {
142
            /** @var array $logging */
143
            foreach ($logging as $logger => $logfile) {
144
                ('debug' === $logger) && TelegramLog::initDebugLog($logfile);
145
                ('error' === $logger) && TelegramLog::initErrorLog($logfile);
146
                ('update' === $logger) && TelegramLog::initUpdateLog($logfile);
147
            }
148
        }
149
150
        return $this;
151
    }
152
153
    /**
154
     * Make sure the passed secret is valid.
155
     *
156
     * @param bool $force Force validation, even on CLI.
157
     *
158
     * @return \NPM\TelegramBotManager\BotManager
159
     * @throws \InvalidArgumentException
160
     */
161
    public function validateSecret(bool $force = false): self
162
    {
163
        // If we're running from CLI, secret isn't necessary.
164
        if ($force || 'cli' !== PHP_SAPI) {
165
            $secret    = $this->params->getBotParam('secret');
166
            $secretGet = $this->params->getScriptParam('s');
167
            if ($secretGet !== $secret) {
168
                throw new \InvalidArgumentException('Invalid access');
169
            }
170
        }
171
172
        return $this;
173
    }
174
175
    /**
176
     * Make sure the webhook is valid and perform the requested webhook operation.
177
     *
178
     * @return \NPM\TelegramBotManager\BotManager
179
     * @throws \Longman\TelegramBot\Exception\TelegramException
180
     * @throws \InvalidArgumentException
181
     */
182
    public function validateAndSetWebhook(): self
183
    {
184
        $webhook = $this->params->getBotParam('webhook');
185
        if (empty($webhook) && $this->action->isAction(['set', 'reset'])) {
186
            throw new \InvalidArgumentException('Invalid webhook');
187
        }
188
189
        if ($this->action->isAction(['unset', 'reset'])) {
190
            $this->handleOutput($this->telegram->deleteWebhook()->getDescription() . PHP_EOL);
191
            // When resetting the webhook, sleep for a bit to prevent too many requests.
192
            $this->action->isAction('reset') && sleep(1);
193
        }
194
195
        if ($this->action->isAction(['set', 'reset'])) {
196
            $webhook_params = array_filter([
197
                'certificate'     => $this->params->getBotParam('certificate'),
198
                'max_connections' => $this->params->getBotParam('max_connections'),
199
                'allowed_updates' => $this->params->getBotParam('allowed_updates'),
200
            ]);
201
202
            $this->handleOutput(
203
                $this->telegram->setWebhook(
204
                    $webhook . '?a=handle&s=' . $this->params->getBotParam('secret'),
205
                    $webhook_params
206
                )->getDescription() . PHP_EOL
207
            );
208
        }
209
210
        return $this;
211
    }
212
213
    /**
214
     * Save the test output and echo it if we're not in a test.
215
     *
216
     * @param string $output
217
     *
218
     * @return \NPM\TelegramBotManager\BotManager
219
     */
220
    private function handleOutput($output): self
221
    {
222
        $this->output .= $output;
223
224
        if (!(defined('PHPUNIT_TEST') && PHPUNIT_TEST === true)) {
225
            echo $output;
226
        }
227
228
        return $this;
229
    }
230
231
    /**
232
     * Set any extra bot features that have been assigned on construction.
233
     *
234
     * @return \NPM\TelegramBotManager\BotManager
235
     */
236
    public function setBotExtras(): self
237
    {
238
        $simple_extras = [
239
            'admins'         => 'enableAdmins',
240
            'mysql'          => 'enableMySql',
241
            'botan_token'    => 'enableBotan',
242
            'commands_paths' => 'addCommandsPaths',
243
            'custom_input'   => 'setCustomInput',
244
            'download_path'  => 'setDownloadPath',
245
            'upload_path'    => 'setUploadPath',
246
        ];
247
        // For simple extras, just pass the single param value to the Telegram method.
248
        foreach ($simple_extras as $param_key => $method) {
249
            $param = $this->params->getBotParam($param_key);
250
            if (null !== $param) {
251
                $this->telegram->$method($param);
252
            }
253
        }
254
255
        $command_configs = $this->params->getBotParam('command_configs');
256
        if (is_array($command_configs)) {
257
            /** @var array $command_configs */
258
            foreach ($command_configs as $command => $config) {
259
                $this->telegram->setCommandConfig($command, $config);
260
            }
261
        }
262
263
        return $this;
264
    }
265
266
    /**
267
     * Handle the request, which calls either the Webhook or getUpdates method respectively.
268
     *
269
     * @return \NPM\TelegramBotManager\BotManager
270
     * @throws \Longman\TelegramBot\Exception\TelegramException
271
     * @throws \Exception
272
     */
273
    public function handleRequest(): self
274
    {
275
        if (empty($this->params->getBotParam('webhook'))) {
276
            if ($loop_time = $this->getLoopTime()) {
277
                $this->handleGetUpdatesLoop($loop_time, $this->getLoopInterval());
278
            } else {
279
                $this->handleGetUpdates();
280
            }
281
        } else {
282
            $this->handleWebhook();
283
        }
284
285
        return $this;
286
    }
287
288
    /**
289
     * Get the number of seconds the script should loop.
290
     *
291
     * @return int
292
     */
293 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...
294
    {
295
        $loop_time = $this->params->getScriptParam('l');
296
297
        if (null === $loop_time) {
298
            return 0;
299
        }
300
301
        if (is_string($loop_time) && '' === trim($loop_time)) {
302
            return 604800; // Default to 7 days.
303
        }
304
305
        return max(0, (int)$loop_time);
306
    }
307
308
    /**
309
     * Get the number of seconds the script should wait after each getUpdates request.
310
     *
311
     * @return int
312
     */
313 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...
314
    {
315
        $interval_time = $this->params->getScriptParam('i');
316
317
        if (null === $interval_time || (is_string($interval_time) && '' === trim($interval_time))) {
318
            return 2;
319
        }
320
321
        // Minimum interval is 1 second.
322
        return max(1, (int)$interval_time);
323
    }
324
325
    /**
326
     * Loop the getUpdates method for the passed amount of seconds.
327
     *
328
     * @param int $loop_time_in_seconds
329
     * @param int $loop_interval_in_seconds
330
     *
331
     * @return \NPM\TelegramBotManager\BotManager
332
     * @throws \Longman\TelegramBot\Exception\TelegramException
333
     */
334
    public function handleGetUpdatesLoop(int $loop_time_in_seconds, int $loop_interval_in_seconds = 2): self
335
    {
336
        // Remember the time we started this loop.
337
        $now = time();
338
339
        $this->handleOutput('Looping getUpdates until ' . date('Y-m-d H:i:s', $now + $loop_time_in_seconds) . PHP_EOL);
340
341
        while ($now > time() - $loop_time_in_seconds) {
342
            $this->handleGetUpdates();
343
344
            // Chill a bit.
345
            sleep($loop_interval_in_seconds);
346
        }
347
348
        return $this;
349
    }
350
351
    /**
352
     * Handle the updates using the getUpdates method.
353
     *
354
     * @return \NPM\TelegramBotManager\BotManager
355
     * @throws \Longman\TelegramBot\Exception\TelegramException
356
     */
357
    public function handleGetUpdates(): self
358
    {
359
        $output = date('Y-m-d H:i:s', time()) . ' - ';
360
361
        $response = $this->telegram->handleGetUpdates();
362
        if ($response->isOk()) {
363
            $results = array_filter((array)$response->getResult());
364
365
            $output .= sprintf('Updates processed: %d' . PHP_EOL, count($results));
366
367
            /** @var Entities\Update $result */
368
            foreach ($results as $result) {
369
                $chat_id = 0;
370
                $text    = 'Nothing';
371
372
                $update_content = $result->getUpdateContent();
373
                if ($update_content instanceof Entities\Message) {
374
                    $chat_id = $update_content->getFrom()->getId();
375
                    $text    = $update_content->getText();
376
                } elseif ($update_content instanceof Entities\InlineQuery || $update_content instanceof Entities\ChosenInlineResult) {
377
                    $chat_id = $update_content->getFrom()->getId();
378
                    $text    = $update_content->getQuery();
379
                }
380
381
                $output .= sprintf(
382
                    '%d: %s' . PHP_EOL,
383
                    $chat_id,
384
                    preg_replace('/\s+/', ' ', trim($text))
385
                );
386
            }
387
        } else {
388
            $output .= sprintf('Failed to fetch updates: %s' . PHP_EOL, $response->printError());
389
        }
390
391
        $this->handleOutput($output);
392
393
        return $this;
394
    }
395
396
    /**
397
     * Handle the updates using the Webhook method.
398
     *
399
     * @return \NPM\TelegramBotManager\BotManager
400
     * @throws \Longman\TelegramBot\Exception\TelegramException
401
     * @throws \Exception
402
     */
403
    public function handleWebhook(): self
404
    {
405
        if (!$this->isValidRequest()) {
406
            throw new \Exception('Invalid access');
407
        }
408
409
        $this->telegram->handle();
410
411
        return $this;
412
    }
413
414
    /**
415
     * Return the current test output and clear it.
416
     *
417
     * @return string
418
     */
419
    public function getOutput(): string
420
    {
421
        $output       = $this->output;
422
        $this->output = '';
423
424
        return $output;
425
    }
426
427
    /**
428
     * Check if this is a valid request coming from a Telegram API IP address.
429
     *
430
     * @link https://core.telegram.org/bots/webhooks#the-short-version
431
     *
432
     * @return bool
433
     */
434
    public function isValidRequest(): bool
435
    {
436
        if (false === $this->params->getBotParam('validate_request')) {
437
            return true;
438
        }
439
440
        $ip = @$_SERVER['REMOTE_ADDR'];
441
        foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR'] as $key) {
442
            $addr = @$_SERVER[$key];
443
            if (filter_var($addr, FILTER_VALIDATE_IP)) {
444
                $ip = $addr;
445
                break;
446
            }
447
        }
448
449
        $lower_dec = (float)sprintf('%u', ip2long(self::TELEGRAM_IP_LOWER));
450
        $upper_dec = (float)sprintf('%u', ip2long(self::TELEGRAM_IP_UPPER));
451
        $ip_dec    = (float)sprintf('%u', ip2long($ip));
452
453
        return $ip_dec >= $lower_dec && $ip_dec <= $upper_dec;
454
    }
455
}
456