Passed
Branch develop (0d58e4)
by Michele
02:41
created

Zanzara::deleteGlobalDataItem()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Zanzara;
6
7
use Clue\React\Buzz\Browser;
8
use DI\Container;
9
use DI\DependencyException;
10
use DI\NotFoundException;
11
use Psr\Http\Message\ServerRequestInterface;
12
use Psr\Log\LoggerInterface;
13
use React\Cache\ArrayCache;
14
use React\Cache\CacheInterface;
15
use React\EventLoop\Factory;
16
use React\EventLoop\LoopInterface;
17
use React\Filesystem\Filesystem;
18
use React\Http\Response;
19
use React\Http\Server;
20
use React\Promise\PromiseInterface;
21
use Zanzara\Listener\ListenerResolver;
22
use Zanzara\Telegram\Telegram;
23
use Zanzara\Telegram\Type\Response\TelegramException;
24
use Zanzara\Telegram\Type\Update;
25
use Zanzara\Telegram\Type\Webhook\WebhookInfo;
26
27
/**
28
 * Framework workflow is:
29
 * 1) register the listeners (onCommand, onUpdate, etc.) @see ListenerCollector
30
 * 2) start listening for updates (either via webhook or polling) @see Zanzara::run()
31
 * 3) when a new update is received, deserialize it and, according to its type, execute the correct
32
 * listener functions. @see ListenerResolver::resolve()
33
 *
34
 */
35
class Zanzara extends ListenerResolver
36
{
37
38
    /**
39
     * @var Config
40
     */
41
    private $config;
42
43
    /**
44
     * @var ZanzaraMapper
45
     */
46
    private $zanzaraMapper;
47
48
    /**
49
     * @var Telegram
50
     */
51
    private $telegram;
52
53
    /**
54
     * @var ZanzaraLogger
55
     */
56
    private $logger;
57
58
    /**
59
     * @var LoopInterface
60
     */
61
    private $loop;
62
63
    /**
64
     * @var Server
65
     */
66
    private $server;
67
68
    /**
69
     * @param string $botToken
70
     * @param Config|null $config
71
     */
72
    public function __construct(string $botToken, ?Config $config = null)
73
    {
74
        $this->config = $config ?? new Config();
75
        $this->config->setBotToken($botToken);
76
        $this->container = $this->config->getContainer() ?? new Container();
77
        $this->loop = $this->config->getLoop() ?? Factory::create();
78
        $this->container->set(LoopInterface::class, $this->loop); // loop cannot be created by container
79
        $this->container->set(LoggerInterface::class, $this->config->getLogger());
80
        $this->logger = $this->container->get(ZanzaraLogger::class);
81
        $this->zanzaraMapper = $this->container->get(ZanzaraMapper::class);
82
        $this->container->set(Browser::class, (new Browser($this->loop)) // browser cannot be created by container
83
        ->withBase("{$this->config->getApiTelegramUrl()}/bot{$botToken}"));
84
        $this->telegram = $this->container->get(Telegram::class);
85
        $this->container->set(CacheInterface::class, $this->config->getCache() ?? new ArrayCache());
86
        $this->container->set(Config::class, $this->config);
87
        if ($this->config->isReactFileSystem()) {
88
            $this->container->set(Filesystem::class, Filesystem::create($this->loop));
89
        }
90
        if ($this->config->getUpdateMode() === Config::REACTPHP_WEBHOOK_MODE) {
91
            $this->prepareServer();
92
        }
93
        $this->cache = $this->container->get(ZanzaraCache::class);
94
    }
95
96
    public function run(): void
97
    {
98
        $this->feedMiddlewareStack();
99
100
        switch ($this->config->getUpdateMode()) {
101
102
            case Config::REACTPHP_WEBHOOK_MODE:
103
                $this->telegram->getWebhookInfo()->then(
104
                    function (WebhookInfo $webhookInfo) {
105
                        if (!$webhookInfo->getUrl()) {
106
                            $message = "Your bot doesn't have a webhook set, please set one before running Zanzara in webhook" .
107
                                " mode. See https://github.com/badfarm/zanzara/wiki#set-webhook";
108
                            $this->logger->error($message);
109
                            return;
110
                        }
111
                        $this->startServer();
112
                    }
113
                );
114
                break;
115
116
            case Config::POLLING_MODE:
117
                $this->telegram->getWebhookInfo()->then(
118
                    function (WebhookInfo $webhookInfo) {
119
                        if (!$webhookInfo->getUrl()) {
120
                            $this->loop->futureTick([$this, 'polling']);
121
                            $this->logger->info("Zanzara is listening...");
122
                            return;
123
                        }
124
                        $message = "Your bot has a webhook set, please delete it before running Zanzara in polling mode. " .
125
                            "See https://core.telegram.org/bots/api#deletewebhook";
126
                        $this->logger->error($message);
127
                        echo "Type 'yes' if you want to delete the webhook: ";
128
                        $answer = readline();
129
                        if (strtoupper($answer) === "YES") {
130
                            $this->telegram->deleteWebhook()->then(
131
                                function ($res) {
132
                                    if ($res === true) {
133
                                        $this->logger->info("Webhook is deleted, Zanzara is starting in polling ...");
134
                                        $this->loop->futureTick([$this, 'polling']);
135
                                        echo "Zanzara is listening...\n";
136
                                    } else {
137
                                        $this->logger->error("Error deleting webhook");
138
                                    }
139
                                }
140
                            );
141
                        } else {
142
                            $this->logger->error("Shutdown, you have to manually delete the webhook or start in webhook mode");
143
                        }
144
                    });
145
                break;
146
147
            case Config::WEBHOOK_MODE:
148
                $token = $this->resolveTokenFromPath($_SERVER['REQUEST_URI'] ?? '');
149
                if (!$this->isWebhookAuthorized($token)) {
150
                    http_response_code(403);
151
                    $this->logger->errorNotAuthorized();
152
                } else {
153
                    $json = file_get_contents($this->config->getUpdateStream());
154
                    /** @var Update $update */
155
                    $update = $this->zanzaraMapper->mapJson($json, Update::class);
156
                    $this->processUpdate($update);
157
                }
158
                break;
159
160
        }
161
162
        $this->loop->run();
163
    }
164
165
    /**
166
     * @param string|null $token
167
     * @return bool
168
     */
169
    private function isWebhookAuthorized(?string $token = null): bool
170
    {
171
        if (!$this->config->isWebhookTokenCheckEnabled()) {
172
            return true;
173
        }
174
        return $token === $this->config->getBotToken();
175
    }
176
177
    /**
178
     * @param string $path
179
     * @return string|null
180
     */
181
    private function resolveTokenFromPath(string $path): ?string
182
    {
183
        $pathParams = explode('/', $path);
184
        return end($pathParams) ?? null;
185
    }
186
187
    private function prepareServer()
188
    {
189
        $processingUpdate = null;
190
        $this->server = new Server(function (ServerRequestInterface $request) use (&$processingUpdate) {
191
            $token = $this->resolveTokenFromPath($request->getUri()->getPath());
192
            if (!$this->isWebhookAuthorized($token)) {
193
                $this->logger->errorNotAuthorized();
194
                return new Response(403, [], $this->logger->getNotAuthorizedMessage());
195
            }
196
            $json = (string)$request->getBody();
197
            /** @var Update $processingUpdate */
198
            $processingUpdate = $this->zanzaraMapper->mapJson($json, Update::class);
199
            $this->processUpdate($processingUpdate);
200
            return new Response();
201
        });
202
        $this->server->on('error', function ($e) use (&$processingUpdate) {
203
            $this->logger->errorUpdate($e, $processingUpdate);
204
            $errorHandler = $this->config->getErrorHandler();
205
            if ($errorHandler) {
206
                $errorHandler($e, new Context($processingUpdate, $this->container));
207
            }
208
        });
209
    }
210
211
    /**
212
     *
213
     */
214
    private function startServer()
215
    {
216
        $socket = new \React\Socket\Server($this->config->getServerUri(), $this->loop, $this->config->getServerContext());
217
        $this->server->listen($socket);
218
        $this->logger->info("Zanzara is listening...");
219
    }
220
221
    /**
222
     * @param int $offset
223
     */
224
    public function polling(int $offset = 1)
225
    {
226
        $processingUpdate = null;
227
        $this->telegram->getUpdates([
228
            'offset' => $offset,
229
            'limit' => $this->config->getPollingLimit(),
230
            'timeout' => $this->config->getPollingTimeout(),
231
            'allowed_updates' => $this->config->getPollingAllowedUpdates(),
232
        ])->then(function (array $updates) use (&$offset, &$processingUpdate) {
233
            if ($offset === 1) {
234
                //first run I need to get the current updateId from telegram
235
                $lastUpdate = end($updates);
236
                if ($lastUpdate) {
237
                    $offset = $lastUpdate->getUpdateId();
238
                }
239
                $this->polling($offset);
240
            } else {
241
                /** @var Update[] $updates */
242
                foreach ($updates as $update) {
243
                    // increase the offset before executing the update, this way if the update processing fails
244
                    // the framework doesn't try to execute it endlessly
245
                    $offset++;
246
                    $processingUpdate = $update;
247
                    $this->processUpdate($update);
248
                }
249
                $this->polling($offset);
250
            }
251
        }, function (TelegramException $error) use (&$offset) {
252
            $this->logger->error("Failed to fetch updates from Telegram: $error");
253
            $this->polling($offset); // consider place a delay before restarting to poll
254
        })->otherwise(function ($e) use (&$offset, &$processingUpdate) {
0 ignored issues
show
Bug introduced by
The method otherwise() does not exist on React\Promise\PromiseInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Zanzara\Test\PromiseWrapper\ZanzaraPromise or React\Promise\CancellablePromiseInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

254
        })->/** @scrutinizer ignore-call */ otherwise(function ($e) use (&$offset, &$processingUpdate) {
Loading history...
255
            $this->logger->errorUpdate($e);
256
            $errorHandler = $this->config->getErrorHandler();
257
            if ($errorHandler) {
258
                $errorHandler($e, new Context($processingUpdate, $this->container));
259
            }
260
            $this->polling($offset); // consider place a delay before restarting to poll
261
        });
262
    }
263
264
    /**
265
     * @param Update $update
266
     */
267
    private function processUpdate(Update $update)
268
    {
269
        $update->detectUpdateType();
270
        $context = new Context($update, $this->container);
271
        $listeners = $this->resolve($update);
272
        foreach ($listeners as $listener) {
273
            $middlewareTip = $listener->getTip();
274
            $middlewareTip($context);
275
        }
276
    }
277
278
    /**
279
     * @return Telegram
280
     */
281
    public function getTelegram(): Telegram
282
    {
283
        return $this->telegram;
284
    }
285
286
    /**
287
     * @return LoopInterface
288
     */
289
    public function getLoop(): LoopInterface
290
    {
291
        return $this->loop;
292
    }
293
294
    /**
295
     * @return Server
296
     */
297
    public function getServer(): Server
298
    {
299
        return $this->server;
300
    }
301
302
    /**
303
     * @return Container
304
     */
305
    public function getContainer(): Container
306
    {
307
        return $this->container;
308
    }
309
310
    /**
311
     * Sets an item of the global data.
312
     * This cache is not related to any chat or user.
313
     *
314
     * Eg:
315
     * $ctx->setGlobalData('age', 21)->then(function($result) {
316
     *
317
     * });
318
     *
319
     * @param $key
320
     * @param $data
321
     * @return PromiseInterface
322
     * @throws DependencyException
323
     * @throws NotFoundException
324
     */
325
    public function setGlobalData($key, $data)
326
    {
327
        return $this->cache->setGlobalCacheData($key, $data);
328
    }
329
330
    /**
331
     * Returns all the global data.
332
     * This cache is not related to any chat or user.
333
     *
334
     * Eg:
335
     * $ctx->getGlobalData()->then(function($data) {
336
     *      $age = $data['age'];
337
     * });
338
     *
339
     * @return PromiseInterface
340
     * @throws DependencyException
341
     * @throws NotFoundException
342
     */
343
    public function getGlobalData()
344
    {
345
        return $this->cache->getGlobalCacheData();
346
    }
347
348
    /**
349
     * Gets an item of the global data.
350
     * This cache is not related to any chat or user.
351
     *
352
     * Eg:
353
     * $ctx->getGlobalDataItem('age')->then(function($age) {
354
     *
355
     * });
356
     *
357
     * @param $key
358
     * @return PromiseInterface
359
     * @throws DependencyException
360
     * @throws NotFoundException
361
     */
362
    public function getGlobalDataItem($key)
363
    {
364
        return $this->cache->getCacheGlobalDataItem($key);
365
    }
366
367
    /**
368
     * Deletes an item from the global data.
369
     * This cache is not related to any chat or user.
370
     *
371
     * Eg:
372
     * $ctx->deleteGlobalDataItem('age')->then(function($result) {
373
     *
374
     * });
375
     *
376
     * @param $key
377
     * @return PromiseInterface
378
     * @throws DependencyException
379
     * @throws NotFoundException
380
     */
381
    public function deleteGlobalDataItem($key)
382
    {
383
        return $this->cache->deleteCacheItemGlobalData($key);
384
    }
385
386
    /**
387
     * Deletes all global data.
388
     *
389
     * Eg:
390
     * $ctx->deleteGlobalData()->then(function($result) {
391
     *
392
     * });
393
     *
394
     * @return PromiseInterface
395
     * @throws DependencyException
396
     * @throws NotFoundException
397
     */
398
    public function deleteGlobalData()
399
    {
400
        return $this->cache->deleteCacheGlobalData();
401
    }
402
403
    /**
404
     * Wipe entire cache.
405
     *
406
     * @return PromiseInterface
407
     * @throws DependencyException
408
     * @throws NotFoundException
409
     */
410
    public function wipeCache()
411
    {
412
        return $this->cache->wipeCache();
413
    }
414
415
}
416