Completed
Pull Request — develop (#29)
by Armando
01:51
created

BotManager::setBotExtrasTelegram()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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