Completed
Pull Request — develop (#29)
by Armando
03:17 queued 01:26
created

BotManager::getTelegram()   A

Complexity

Conditions 1
Paths 1

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