Passed
Push — main ( 8fc5c6...356e8c )
by Sys
09:51
created

API::dropUpdates()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
c 0
b 0
f 0
dl 0
loc 14
rs 9.9666
cc 2
nc 2
nop 0
1
<?php
2
3
4
namespace Sysbot\Telegram;
5
6
use Generator;
7
use GuzzleHttp\Client;
8
use GuzzleHttp\Promise\PromiseInterface;
9
use Psr\Http\Message\ResponseInterface;
10
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
11
use Symfony\Component\Serializer\Encoder\JsonEncoder;
12
use Symfony\Component\Serializer\Exception\ExceptionInterface;
13
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
14
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
15
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
16
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
17
use Symfony\Component\Serializer\Serializer;
18
use Sysbot\Telegram\Common\NameNormalizer;
19
use Sysbot\Telegram\Constants\ApiClasses;
20
use Sysbot\Telegram\Constants\UpdateTypes;
21
use Sysbot\Telegram\Exceptions\TelegramAPIException;
22
use Sysbot\Telegram\Exceptions\TelegramBadRequestException;
23
use Sysbot\Telegram\Exceptions\TelegramForbiddenException;
24
use Sysbot\Telegram\Exceptions\TelegramNotFoundException;
25
use Sysbot\Telegram\Exceptions\TelegramUnauthorizedException;
26
use Sysbot\Telegram\Helpers\FileDownloader;
27
use Sysbot\Telegram\Helpers\TypeInstantiator;
28
use Sysbot\Telegram\Types\InputFile;
29
use Sysbot\Telegram\Types\InputMedia;
30
use Sysbot\Telegram\Types\Response;
31
use Sysbot\Telegram\Types\Update;
32
use Sysbot\Telegram\Types\WebhookInfo;
33
34
/**
35
 * Class API
36
 * @package Sysbot\Telegram
37
 */
38
class API
39
{
40
41
    use BaseAPI, FileDownloader, TypeInstantiator;
42
43
    /**
44
     * Official Telegram bot API URL.
45
     */
46
    public const BOT_API_URL = 'https://api.telegram.org/bot';
47
48
    /**
49
     * @var Client
50
     */
51
    private Client $client;
52
    /**
53
     * @var Serializer
54
     */
55
    private Serializer $serializer;
56
    /**
57
     * @var array
58
     */
59
    public array $defaultArgs = [];
60
    /**
61
     * @var array
62
     */
63
    public array $allowedUpdates = [];
64
65
66
    /**
67
     * API constructor.
68
     * @param string $token
69
     * @param string $apiEndpoint
70
     * @param float $timeout
71
     */
72
    public function __construct(
73
        private string $token,
74
        private string $apiEndpoint = self::BOT_API_URL,
75
        private float $timeout = 0
76
    ) {
77
        $apiUri = $this->apiEndpoint . $this->token . '/';
78
        $this->client = new Client(['base_uri' => $apiUri, 'timeout' => $timeout]);
79
        $this->serializer = new Serializer(
80
            [
81
                new ObjectNormalizer(null, new CamelCaseToSnakeCaseNameConverter(), null, new ReflectionExtractor()),
82
                new ArrayDenormalizer()
83
            ],
84
            [new JsonEncoder()]
85
        );
86
    }
87
88
    /**
89
     * @param array $requests
90
     * @param callable|null $onFulfilled
91
     * @param callable|null $onRejected
92
     * @return PromiseInterface[]
93
     * @throws ExceptionInterface
94
     */
95
    public function sendMultipleRequests(
96
        array $requests,
97
        ?callable $onFulfilled = null,
98
        ?callable $onRejected = null
99
    ): array {
100
        $result = [];
101
        foreach ($requests as $request) {
102
            $result[] = $this->sendRequest($request['method'], $request['args'])
103
                ->then($onFulfilled, $onRejected);
104
        }
105
        return $result;
106
    }
107
108
    /**
109
     * @throws ExceptionInterface
110
     */
111
    private function buildMultipartArgs(array $args): array
112
    {
113
        $result = [];
114
        $cleanInputMedia = function () {
115
            unset($this->inputMedia);
116
            unset($this->multipart);
117
        };
118
        foreach ($args as $index => $arg) {
119
            if ('media' == $index and is_array($arg)) {
120
                /** @var InputMedia $inputMedia */
121
                foreach ($arg as $inputMedia) {
122
                    if ($inputMedia->isMultipart()) {
123
                        $result[] = [
124
                            'name' => $inputMedia->media,
125
                            'contents' => (string)$inputMedia->getInputMedia(),
126
                            'filename' => $inputMedia->getFilename()
127
                        ];
128
                        $inputMedia->media = 'attach://' . $inputMedia->media;
129
                        $cleanInputMedia->call($inputMedia);
130
                    }
131
                }
132
            }
133
            if (($arg instanceof InputMedia) and $arg->isMultipart()) {
134
                $result[] = [
135
                    'name' => $arg->media,
136
                    'contents' => (string)$arg->getInputMedia(),
137
                    'filename' => $arg->getFilename()
138
                ];
139
                $arg->media = 'attach://' . $arg->media;
140
                $cleanInputMedia->call($arg);
141
            }
142
            $result[] = [
143
                'name' => $index,
144
                'contents' => $arg
145
            ];
146
        }
147
        $result = $this->serializer->normalize(
148
            $result,
149
            null,
150
            [AbstractObjectNormalizer::SKIP_NULL_VALUES => true]
151
        );
152
        foreach ($result as $index => $item) {
153
            if (is_array($item['contents'])) {
154
                $item['contents'] = $this->serializer->serialize(
155
                    $item['contents'],
156
                    'json',
157
                    [AbstractObjectNormalizer::SKIP_NULL_VALUES => true]
158
                );
159
                $result[$index] = $item;
160
            }
161
        }
162
        return $result;
163
    }
164
165
    /**
166
     * @param string $method
167
     * @param array $args
168
     * @return PromiseInterface
169
     * @throws ExceptionInterface
170
     */
171
    public function sendRequest(string $method, array $args): PromiseInterface
172
    {
173
        if (!empty($this->defaultArgs)) {
174
            $args = array_merge($this->defaultArgs, $args);
175
        }
176
        $useMultipart = false;
177
        foreach ($args as $index => $arg) {
178
            // I don't know why, but the Serializer sometimes freaks out when trying to convert camelCase to snake_case
179
            // So, I'm doing it myself
180
            $normalizedIndex = NameNormalizer::camelCaseToSnakeCase($index);
181
            unset($args[$index]);
182
            $args[$normalizedIndex] = $arg;
183
            if (($arg instanceof InputFile or $arg instanceof InputMedia) and $arg->isMultipart()) {
184
                $useMultipart = true;
185
            }
186
            if ('media' == $index and is_array($arg)) {
187
                /** @var InputMedia $media */
188
                foreach ($arg as $media) {
189
                    if ($media->isMultipart()) {
190
                        $useMultipart = true;
191
                    }
192
                }
193
            }
194
        }
195
        $promise = null;
196
        if ($useMultipart) {
197
            $args = $this->buildMultipartArgs($args);
198
            $promise = $this->client->postAsync(
199
                $method,
200
                [
201
                    'multipart' => $args,
202
                    'headers' => ['Connection' => 'Keep-Alive', 'Keep-Alive' => '120'],
203
                    'http_errors' => false
204
                ]
205
            );
206
        }
207
        $promise ??= $this->client->postAsync(
208
            $method,
209
            [
210
                'body' => $this->serializer->serialize(
211
                    (object)$args,
212
                    'json',
213
                    [AbstractObjectNormalizer::SKIP_NULL_VALUES => true]
214
                ),
215
                'headers' => [
216
                    'Connection' => 'Keep-Alive',
217
                    'Keep-Alive' => '120',
218
                    'Content-Type' => 'application/json'
219
                ],
220
                'http_errors' => false
221
            ]
222
        );
223
        $api = $this;
224
        return $promise->then(
225
            function (ResponseInterface $webResponse) use ($api, $method) {
226
                $data = json_decode($webResponse->getBody());
227
                $response = (new Response($api, $method))
228
                    ->setOk($data->ok)
229
                    ->setResult($data->result ?? null)
230
                    ->setErrorCode($data->error_code ?? null)
231
                    ->setDescription($data->description ?? null);
232
                if (!$response->ok) {
233
                    match ($response->errorCode) {
234
                        400 => throw new TelegramBadRequestException($response->description),
235
                        401 => throw new TelegramUnauthorizedException('Token invalid or expired'),
236
                        403 => throw new TelegramForbiddenException($response->description),
237
                        404 => throw new TelegramNotFoundException($response->description),
238
                        default => throw new TelegramAPIException(
239
                            sprintf(
240
                                'Telegram API returned an error (%s): %s',
241
                                $response->errorCode ?? 'Unknown',
242
                                $response->description ?? 'Unknown'
243
                            )
244
                        )
245
                    };
246
                }
247
                return $response->result;
248
            }
249
        );
250
    }
251
252
    /**
253
     * @param string $method
254
     * @param array $args
255
     * @return mixed
256
     * @throws ExceptionInterface
257
     */
258
    public function sendBlockingRequest(string $method, array $args): mixed
259
    {
260
        return $this->sendRequest($method, $args)->wait();
261
    }
262
263
    /**
264
     * @return Serializer
265
     */
266
    public function getSerializer(): Serializer
267
    {
268
        return $this->serializer;
269
    }
270
271
    /**
272
     * @return Generator
273
     */
274
    public function iterUpdates(): Generator
275
    {
276
        $timeout = 0 === $this->timeout ? 30 : $this->timeout;
0 ignored issues
show
introduced by
The condition 0 === $this->timeout is always false.
Loading history...
277
        $offset = 0;
278
        $args = [
279
            AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
280
            AbstractObjectNormalizer::ENABLE_MAX_DEPTH => true
281
        ];
282
        $args += ApiClasses::buildArgs($this);
283
        $data = file_get_contents('php://input');
284
        if (!empty($data) and is_string($data)) {
285
            yield $this->serializer->deserialize(
286
                $data,
287
                Update::class,
288
                'json',
289
                $args
290
            );
291
            return;
292
        }
293
        while (true) {
294
            $updates = $this->getUpdates(
295
                offset: $offset,
296
                timeout: $timeout,
297
                allowedUpdates: $this->allowedUpdates
298
            )->wait();
299
            /** @var Update $update */
300
            foreach ($updates as $update) {
301
                yield $update;
302
            }
303
            $offset = $update->updateId + 1;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $update does not seem to be defined for all execution paths leading up to this point.
Loading history...
304
        }
305
    }
306
307
    /**
308
     * @return PromiseInterface
309
     */
310
    public function dropUpdates(): PromiseInterface
311
    {
312
        /** @var WebhookInfo $webhookInfo */
313
        $webhookInfo = $this->getWebhookInfo()->wait();
314
        if (!empty($webhookInfo->url)) {
315
            return $this->setWebhook(
316
                url: $webhookInfo->url,
317
                ipAddress: $webhookInfo->ipAddress,
318
                maxConnections: $webhookInfo->maxConnections,
319
                allowedUpdates: $webhookInfo->allowedUpdates,
320
                dropPendingUpdates: true
321
            );
322
        }
323
        return $this->getUpdates(offset: -1);
324
    }
325
326
    /**
327
     * Note: 8191 automatically enables all available types (chat_member included)
328
     *
329
     * @param int $allowedUpdates
330
     * @return $this
331
     */
332
    public function setAllowedUpdates(int $allowedUpdates = 8191): self
333
    {
334
        $types = UpdateTypes::getAllowedTypes($allowedUpdates);
335
        /** @var WebhookInfo $webhookInfo */
336
        $webhookInfo = $this->getWebhookInfo()->wait();
337
        if (!empty($webhookInfo->url)) {
338
            $this->setWebhook(
339
                url: $webhookInfo->url,
340
                ipAddress: $webhookInfo->ipAddress,
341
                maxConnections: $webhookInfo->maxConnections,
342
                allowedUpdates: $types,
343
            )->wait();
344
        }
345
        $this->allowedUpdates = $types;
346
        return $this;
347
    }
348
349
}
350