Passed
Push — master ( 1ce9aa...29cd64 )
by Shahrad
02:01
created

Telegram::setAdminChatId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
nc 1
nop 1
cc 1
1
<?php
2
3
namespace TelegramBot;
4
5
use Symfony\Component\Dotenv\Dotenv;
6
use TelegramBot\Entities\Response;
7
use TelegramBot\Entities\Update;
8
use TelegramBot\Exception\TelegramException;
9
use TelegramBot\Util\Toolkit;
10
11
/**
12
 * Telegram class
13
 *
14
 * @link    https://github.com/telegram-bot-php/core
15
 * @author  Shahrad Elahi (https://github.com/shahradelahi)
16
 * @license https://github.com/telegram-bot-php/core/blob/master/LICENSE (MIT License)
17
 */
18
class Telegram
19
{
20
21
    /**
22
     * @var string
23
     */
24
    public static string $VERSION = 'v1.0.0';
25
26
    /**
27
     * Admin chat id
28
     *
29
     * @var int
30
     */
31
    private static int $adminChatId = -1;
32
33
    /**
34
     * @var string|null
35
     */
36
    private static string|null $api_key = null;
37
38
    /**
39
     * Telegram constructor.
40
     *
41
     * @param string $api_token
42
     */
43
    public function __construct(string $api_token = '')
44
    {
45
        if ($api_token === '') {
46
            (new Dotenv())->load($this->getEnvFilePath());
47
            $api_token = $_ENV['TELEGRAM_BOT_TOKEN'];
48
        }
49
50
        if (empty($api_token) || self::validateToken($api_token) === false) {
51
            throw new TelegramException(sprintf(
52
                'Invalid Telegram API token: %s',
53
                $api_token
54
            ));
55
        }
56
57
        self::setToken($api_token);
58
    }
59
60
    /**
61
     * Set the current bot token.
62
     *
63
     * @param string $api_key
64
     * @return void
65
     */
66
    public static function setToken(string $api_key): void
67
    {
68
        static::$api_key = $api_key;
0 ignored issues
show
Bug introduced by
Since $api_key is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $api_key to at least protected.
Loading history...
69
        $_ENV['TELEGRAM_BOT_TOKEN'] = $api_key;
70
    }
71
72
    /**
73
     * Get the current bot token.
74
     *
75
     * @return string|false
76
     */
77
    public static function getApiToken(): string|false
78
    {
79
        return self::$api_key !== null ? (self::validateToken(self::$api_key) ? self::$api_key : false) : false;
80
    }
81
82
    /**
83
     * Get env file path and return it
84
     *
85
     * @return string
86
     */
87
    private function getEnvFilePath(): string
88
    {
89
        $defaultEnvPaths = [
90
            $_SERVER['DOCUMENT_ROOT'] . '/.env',
91
            getcwd() . '/../.env',
92
            getcwd() . '/.env',
93
        ];
94
95
        foreach ($defaultEnvPaths as $path) {
96
            if (file_exists($path)) {
97
                return $path;
98
            }
99
        }
100
101
        return '';
102
    }
103
104
    /**
105
     * Get token from env file.
106
     *
107
     * @param string $file
108
     * @return string|null
109
     */
110
    protected function getEnvToken(string $file): string|null
111
    {
112
        if (!file_exists($file)) return null;
113
        return $_ENV['TELEGRAM_BOT_TOKEN'] ?? null;
114
    }
115
116
    /**
117
     * @return int
118
     */
119
    public static function getAdminChatId(): int
120
    {
121
        return static::$adminChatId;
0 ignored issues
show
Bug introduced by
Since $adminChatId is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $adminChatId to at least protected.
Loading history...
122
    }
123
124
    /**
125
     * @param int $adminChatId
126
     */
127
    public static function setAdminChatId(int $adminChatId): void
128
    {
129
        static::$adminChatId = $adminChatId;
0 ignored issues
show
Bug introduced by
Since $adminChatId is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $adminChatId to at least protected.
Loading history...
130
    }
131
132
    /**
133
     * Get bot info from given API key
134
     *
135
     * @return Response
136
     * @throws TelegramException
137
     */
138
    public function getInfo(): Response
139
    {
140
        $result = Request::getMe();
141
142
        if (!$result->isOk()) {
143
            throw new TelegramException($result->getErrorCode() . ': ' . $result->getDescription());
144
        }
145
146
        return $result;
147
    }
148
149
    /**
150
     * Set Webhook for bot
151
     *
152
     * @param string $url
153
     * @param array $data Optional parameters.
154
     * @return Response
155
     * @throws TelegramException
156
     */
157
    public function setWebhook(string $url, array $data = []): Response
158
    {
159
        if ($url === '') {
160
            throw new TelegramException('Hook url is empty!');
161
        }
162
163
        if (!str_starts_with($url, 'https://')) {
164
            throw new TelegramException('Hook url must start with https://');
165
        }
166
167
        $data = array_intersect_key($data, array_flip([
168
            'certificate',
169
            'ip_address',
170
            'max_connections',
171
            'allowed_updates',
172
            'drop_pending_updates',
173
        ]));
174
        $data['url'] = $url;
175
176
        $result = Request::setWebhook($data);
177
178
        if (!$result->isOk()) {
179
            throw new TelegramException(
180
                'Webhook was not set! Error: ' . $result->getErrorCode() . ' ' . $result->getDescription()
181
            );
182
        }
183
184
        return $result;
185
    }
186
187
    /**
188
     * Delete any assigned webhook
189
     *
190
     * @param array $data
191
     * @return Response
192
     * @throws TelegramException
193
     */
194
    public function deleteWebhook(array $data = []): Response
195
    {
196
        $result = Request::deleteWebhook($data);
197
198
        if (!$result->isOk()) {
199
            throw new TelegramException(
200
                'Webhook was not deleted! Error: ' . $result->getErrorCode() . ' ' . $result->getDescription()
201
            );
202
        }
203
204
        return $result;
205
    }
206
207
    /**
208
     * Pass the update to the given webhook handler
209
     *
210
     * @param UpdateHandler $webhook_handler The webhook handler
211
     * @param Update|null $update By default, it will get the update from input
212
     * @return void
213
     */
214
    public function fetchWith(UpdateHandler $webhook_handler, Update|null $update = null): void
215
    {
216
        if (is_subclass_of($webhook_handler, UpdateHandler::class)) {
217
            if ($update === null) $update = self::getUpdate();
218
            $webhook_handler->resolve($update);
0 ignored issues
show
Bug introduced by
It seems like $update can also be of type false; however, parameter $update of TelegramBot\UpdateHandler::resolve() does only seem to accept TelegramBot\Entities\Update|null, maybe add an additional type check? ( Ignorable by Annotation )

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

218
            $webhook_handler->resolve(/** @scrutinizer ignore-type */ $update);
Loading history...
219
        }
220
    }
221
222
    /**
223
     * Get the update from input
224
     *
225
     * @return Update|false
226
     */
227
    public static function getUpdate(): Update|false
228
    {
229
        $input = self::getInput();
230
        if (empty($input)) return false;
231
        return Telegram::processUpdate($input, self::getApiToken());
0 ignored issues
show
Bug introduced by
It seems like self::getApiToken() can also be of type false; however, parameter $apiKey of TelegramBot\Telegram::processUpdate() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

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

231
        return Telegram::processUpdate($input, /** @scrutinizer ignore-type */ self::getApiToken());
Loading history...
232
    }
233
234
    /**
235
     * Get input from stdin and return it
236
     *
237
     * @return string|null
238
     */
239
    public static function getInput(): string|null
240
    {
241
        return file_get_contents('php://input') ?? null;
242
    }
243
244
    /**
245
     * This method will convert a string to an update object
246
     *
247
     * @param string $input The input string
248
     * @param string|null $apiKey The API key
249
     * @return Update|false
250
     */
251
    public static function processUpdate(string $input, string|null $apiKey = null): Update|false
252
    {
253
        if (empty($input)) {
254
            throw new TelegramException(
255
                'Input is empty! Please check your code and try again.'
256
            );
257
        }
258
259
        if ($apiKey !== null && !self::validateToken($apiKey)) {
260
            throw new TelegramException(
261
                'Invalid token! Please check your code and try again.'
262
            );
263
        }
264
265
        if ($apiKey !== null && self::validateWebData($apiKey, $input)) {
266
            if (Toolkit::isUrlEncode($input)) {
267
                $web_data = Toolkit::urlDecode($input);
268
            }
269
270
            if (Toolkit::isJson($input)) {
271
                $web_data = json_decode($input, true);
272
            }
273
274
            if (!empty($web_data) && is_array($web_data)) {
275
                $input = json_encode([
276
                    'web_data' => $web_data,
277
                ]);
278
            }
279
        }
280
281
        if (!Toolkit::isJson($input)) {
282
            throw new TelegramException(sprintf(
283
                "Input is not a valid JSON string! Please check your code and try again.\nInput: %s",
284
                $input
285
            ));
286
        }
287
288
        $input = json_decode($input, true);
289
290
        return new Update($input);
291
    }
292
293
    /**
294
     * Validate the token
295
     *
296
     * @param string $token (e.g. 123456789:ABC-DEF1234ghIkl-zyx57W2v1u123ew11) {digit}:{alphanumeric[34]}
297
     * @return bool
298
     */
299
    public static function validateToken(string $token): bool
300
    {
301
        preg_match_all('/([0-9]+:[a-zA-Z0-9-_]+)/', $token, $matches);
302
        return count($matches[0]) == 1;
303
    }
304
305
    /**
306
     * Validate webapp data from is from Telegram
307
     *
308
     * @link https://core.telegram.org/bots/webapps#validating-data-received-via-the-web-app
309
     *
310
     * @param string $token The bot token
311
     * @param string $body The message body from getInput()
312
     * @return bool
313
     */
314
    public static function validateWebData(string $token, string $body): bool
315
    {
316
        if (!Toolkit::isJson($body)) {
317
            $raw_data = rawurldecode(str_replace('_auth=', '', $body));
318
            $data = Toolkit::urlDecode($raw_data);
319
320
            if (empty($data['user'])) {
321
                return false;
322
            }
323
324
            $data['user'] = urldecode($data['user']);
325
326
        } else {
327
            $data = json_decode($body, true);
328
329
            if (empty($data['user'])) {
330
                return false;
331
            }
332
333
            $data['user'] = json_encode($data['user']);
334
        }
335
336
        $data_check_string = "auth_date={$data['auth_date']}\nquery_id={$data['query_id']}\nuser={$data['user']}";
337
        $secret_key = hash_hmac('sha256', $token, "WebAppData", true);
338
339
        return hash_hmac('sha256', $data_check_string, $secret_key) == $data['hash'];
340
    }
341
342
}