Completed
Pull Request — master (#11)
by Armando
01:47
created

BotManager::inTest()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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