Issues (236)

src/tools/tools.php (11 issues)

1
<?php
2
3
namespace BPT\tools;
4
5
use BPT\constants\chatMemberStatus;
6
use BPT\constants\codecAction;
7
use BPT\constants\fields;
8
use BPT\constants\fileTypes;
9
use BPT\constants\loggerTypes;
10
use BPT\constants\parseMode;
11
use BPT\constants\pollType;
12
use BPT\exception\bptException;
13
use BPT\logger;
14
use BPT\settings;
15
use BPT\telegram\request;
16
use BPT\telegram\telegram;
17
use BPT\types\inlineKeyboardButton;
18
use BPT\types\inlineKeyboardMarkup;
19
use BPT\types\keyboardButton;
20
use BPT\types\keyboardButtonPollType;
21
use BPT\types\replyKeyboardMarkup;
22
use BPT\types\user;
23
use BPT\types\webAppInfo;
24
use DateTime;
25
use Exception;
26
use FilesystemIterator;
27
use RecursiveDirectoryIterator;
28
use RecursiveIteratorIterator;
29
use ZipArchive;
30
31
/**
32
 * tools class , gather what ever you need
33
 */
34
class tools{
35
    /**
36
     * Check the given username format
37
     *
38
     * e.g. => tools::isUsername('BPT_CH');
39
     *
40
     * e.g. => tools::isUsername(username: 'BPT_CH');
41
     *
42
     * @param string $username Your text to be check is it username or not , @ is not needed
43
     *
44
     * @return bool
45
     */
46
    public static function isUsername (string $username): bool {
47
        $length = strlen($username);
48
        return !str_contains($username, '__') && $length >= 4 && $length <= 33 && preg_match('/^@?([a-zA-Z])(\w{4,31})$/', $username);
49
    }
50
51
    /**
52
     * Check given IP is in the given IP range or not
53
     *
54
     * e.g. => tools::ipInRange('192.168.1.1','149.154.160.0/20');
55
     *
56
     * e.g. => tools::ipInRange(ip: '192.168.1.1',range: '149.154.160.0/20');
57
     *
58
     * @param string $ip    Your ip
59
     * @param string $range Your range ip for check , if you didn't specify the block , it will be 32
60
     *
61
     * @return bool
62
     */
63
    public static function ipInRange (string $ip, string $range): bool {
64
        if (!str_contains($range, '/')) {
65
            $range .= '/32';
66
        }
67
        $range_full = explode('/', $range, 2);
68
        $netmask_decimal = ~(pow(2, (32 - $range_full[1])) - 1);
69
        return (ip2long($ip) & $netmask_decimal) == (ip2long($range_full[0]) & $netmask_decimal);
70
    }
71
72
    /**
73
     * Check the given IP is from telegram or not
74
     *
75
     * e.g. => tools::isTelegram('192.168.1.1');
76
     *
77
     * e.g. => tools::isTelegram(ip: '192.168.1.1');
78
     *
79
     * @param string $ip Your ip to be check is telegram or not e.g. '192.168.1.1'
80
     *
81
     * @return bool
82
     */
83
    public static function isTelegram (string $ip): bool {
84
        return tools::ipInRange($ip, '149.154.160.0/20') || tools::ipInRange($ip, '91.108.4.0/22');
85
    }
86
87
    /**
88
     * Check the given IP is from CloudFlare or not
89
     *
90
     * e.g. => tools::isCloudFlare('192.168.1.1');
91
     *
92
     * e.g. =>tools::isCloudFlare(ip: '192.168.1.1');
93
     *
94
     * @param string $ip Your ip to be check is CloudFlare or not
95
     *
96
     * @return bool
97
     */
98
    public static function isCloudFlare (string $ip): bool {
99
        $cf_ips = ['173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/12', '104.24.0.0/14', '172.64.0.0/13', '131.0.72.0/22'];
100
        foreach ($cf_ips as $cf_ip) {
101
            if (self::ipInRange($ip,$cf_ip)) {
102
                return true;
103
            }
104
        }
105
        return false;
106
    }
107
108
    /**
109
     * Check the given IP is from ArvanCloud or not
110
     *
111
     * e.g. => tools::isCloudFlare('192.168.1.1');
112
     *
113
     * e.g. =>tools::isCloudFlare(ip: '192.168.1.1');
114
     *
115
     * @param string $ip Your ip to be checked is ArvanCloud or not
116
     *
117
     * @return bool
118
     */
119
    public static function isArvanCloud (string $ip): bool {
120
        $ar_ips = ['185.143.232.0/22', '92.114.16.80/28', '2.146.0.0/28', '46.224.2.32/29', '89.187.178.96/29', '195.181.173.128/29', '89.187.169.88/29', '188.229.116.16/29', '83.123.255.56/31', '164.138.128.28/31', '94.182.182.28/30', '185.17.115.176/30', '5.213.255.36/31', '138.128.139.144/29', '5.200.14.8/29', '188.122.68.224/29', '188.122.83.176/29', '213.179.217.16/29', '185.179.201.192/29', '43.239.139.192/29', '213.179.197.16/29', '213.179.201.192/29', '109.200.214.248/29', '138.128.141.16/29', '188.122.78.136/29', '213.179.211.32/29', '103.194.164.24/29', '185.50.105.136/29', '213.179.213.16/29', '162.244.52.120/29', '188.122.80.240/29', '109.200.195.64/29', '109.200.199.224/29', '185.228.238.0/28', '94.182.153.24/29', '94.101.182.0/27', '37.152.184.208/28', '78.39.156.192/28', '158.255.77.238/31', '81.12.28.16/29', '176.65.192.202/31', '2.144.3.128/28', '89.45.48.64/28', '37.32.16.0/27', '37.32.17.0/27', '37.32.18.0/27'];
121
        foreach ($ar_ips as $ar_ip) {
122
            if (self::ipInRange($ip,$ar_ip)) {
123
                return true;
124
            }
125
        }
126
        return false;
127
    }
128
129
    /**
130
     * Check the given token format
131
     *
132
     * if you want to verify token with telegram , you should set `verify` parameter => true.
133
     * in that case , if token was right , you will receive getMe result , otherwise you will receive false
134
     *
135
     * e.g. => tools::isToken('123123123:abcabcabcabc');
136
     *
137
     * @param string $token  your token e.g. => '123123123:abcabcabcabc'
138
     * @param bool   $verify check token with telegram or not
139
     *
140
     * @return bool|user return array when verify is active and token is true array of telegram getMe result
141
     */
142
    public static function isToken (string $token, bool $verify = false): bool|user {
143
        if (!preg_match('/^(\d{8,10}):[\w\-]{35}$/', $token)) {
144
            return false;
145
        }
146
        if (!$verify){
147
            return true;
148
        }
149
        $res = telegram::me($token);
150
        if (!telegram::$status) {
151
            return false;
152
        }
153
        return $res;
154
    }
155
156
    /**
157
     * check user joined in channels or not
158
     *
159
     * this method only return true or false, if user join in all channels true, and if user not joined in one channel false
160
     *
161
     * this method does not care about not founded channel and count them as joined channel
162
     *
163
     * ids parameter can be array for multi channels or can be string for one channel
164
     *
165
     * NOTE : each channel will decrease speed a little(because of request count)
166
     *
167
     * e.g. => tools::isJoined('BPT_CH','442109602');
168
     *
169
     * e.g. => tools::isJoined(['BPT_CH','-1005465465454']);
170
     *
171
     * @param array|string|int $ids     could be username or id, you can pass multi or single id
172
     * @param int|null         $user_id if not set , will generate by request::catchFields method
173
     *
174
     * @return bool
175
     */
176
    public static function isJoined (array|string|int $ids , int|null $user_id = null): bool {
177
        if (!is_array($ids)) {
0 ignored issues
show
The condition is_array($ids) is always true.
Loading history...
178
            $ids = [$ids];
179
        }
180
        $user_id = $user_id ?? request::catchFields('user_id');
181
182
        foreach ($ids as $id) {
183
            $check = telegram::getChatMember($id,$user_id);
184
            if (telegram::$status) {
185
                $check = $check->status;
186
                if ($check === chatMemberStatus::LEFT || $check === chatMemberStatus::KICKED) {
187
                    return false;
188
                }
189
            }
190
        }
191
        return true;
192
    }
193
194
    /**
195
     * check user joined in channels or not
196
     *
197
     * ids parameter can be array for multi channels or can be string for one channel
198
     *
199
     * NOTE : each channel will decrease speed a little(because of request count)
200
     *
201
     * e.g. => tools::joinChecker('BPT_CH','442109602');
202
     *
203
     * e.g. => tools::joinChecker(['BPT_CH','-1005465465454']);
204
     *
205
     * @param array|string|int $ids     could be username or id, you can pass multi or single id
206
     * @param int|null         $user_id if not set , will generate by request::catchFields method
207
     *
208
     * @return array keys will be id and values will be bool(null for not founded ids)
209
     */
210
    public static function joinChecker (array|string|int $ids , int|null $user_id = null): array {
211
        if (!is_array($ids)) {
0 ignored issues
show
The condition is_array($ids) is always true.
Loading history...
212
            $ids = [$ids];
213
        }
214
        $user_id = $user_id ?? request::catchFields('user_id');
215
216
        $result = [];
217
        foreach ($ids as $id) {
218
            $check = telegram::getChatMember($id,$user_id);
219
            if (telegram::$status) {
220
                $check = $check->status;
221
                $result[$id] = $check !== chatMemberStatus::LEFT && $check !== chatMemberStatus::KICKED;
222
            }
223
            else $result[$id] = null;
224
        }
225
        return $result;
226
    }
227
228
    /**
229
     * check is it short encoded or not
230
     *
231
     * e.g. => tools::isShorted('abc');
232
     *
233
     * @param string $text
234
     *
235
     * @return bool
236
     */
237
    public static function isShorted(string $text): bool{
238
        return preg_match('/^[a-zA-Z0-9]+$/',$text);
0 ignored issues
show
Bug Best Practice introduced by
The expression return preg_match('/^[a-zA-Z0-9]+$/', $text) returns the type integer which is incompatible with the type-hinted return boolean.
Loading history...
239
    }
240
241
    /**
242
     * receive size from path(can be url or file path)
243
     *
244
     * NOTE : some url will not return real size!
245
     *
246
     * e.g. => tools::size('xFile.zip');
247
     *
248
     * e.g. => tools::size(path: 'xFile.zip');
249
     *
250
     * @param string $path   file path, could be url
251
     * @param bool   $format if you set this true , you will receive symbolic string like 2.76MB for return
252
     * @param bool   $space_between this will only work when format is true, convert 2.76MB to 2.76 MB
253
     *
254
     * @return string|int|false string for formatted data , int for normal data , false when size can not be found(file not found or ...)
255
     */
256
    public static function size (string $path, bool $format = true, bool $space_between = true): string|int|false {
257
        if (filter_var($path, FILTER_VALIDATE_URL)) {
258
            $ch = curl_init($path);
259
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
260
            curl_setopt($ch, CURLOPT_HEADER, true);
261
            curl_setopt($ch, CURLOPT_NOBODY, true);
262
            curl_exec($ch);
263
            $size = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
264
            curl_close($ch);
265
        }
266
        else {
267
            $path = realpath($path);
268
            $size = file_exists($path) ? filesize($path) : false;
269
        }
270
        if (isset($size) && is_numeric($size)) {
271
            return $format ? tools::byteFormat($size, space_between: $space_between) : $size;
272
        }
273
        return false;
274
    }
275
276
    /**
277
     * Delete a folder or file if exist
278
     *
279
     * e.g. => tools::delete(path: 'xfolder/yfolder');
280
     *
281
     * e.g. => tools::delete('xfolder/yfolder',false);
282
     *
283
     * @param string $path folder or file path
284
     * @param bool   $sub  set true for removing subFiles too, if folder has subFiles and this set to false , you will receive error
285
     *
286
     * @return bool
287
     * @throws bptException
288
     */
289
    public static function delete (string $path, bool $sub = true): bool {
290
        $path = realpath($path);
291
        if (!is_dir($path)) {
292
            return unlink($path);
293
        }
294
        if (count(scandir($path)) <= 2) {
295
            return rmdir($path);
296
        }
297
        if (!$sub) {
298
            logger::write("tools::delete function used\ndelete function cannot delete folder because its have subFiles and sub parameter haven't true value",loggerTypes::ERROR);
299
            throw new bptException('DELETE_FOLDER_HAS_SUB');
300
        }
301
        $it = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
302
        $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
303
        foreach ($files as $file) {
304
            $file->isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath());
305
        }
306
        return rmdir($path);
307
    }
308
309
    /**
310
     * convert all files in selected path to zip and then save it in dest path
311
     *
312
     * e.g. => tools::zip('xFolder','yFolder/xFile.zip');
313
     *
314
     * @param string $path        your file or folder to be zipped
315
     * @param string $destination destination path for create file
316
     *
317
     * @return bool
318
     * @throws bptException when zip extension not found
319
     */
320
    public static function zip (string $path, string $destination): bool {
321
        if (!extension_loaded('zip')) {
322
            logger::write("tools::zip function used\nzip extension is not found , It may not be installed or enabled", loggerTypes::ERROR);
323
            throw new bptException('ZIP_EXTENSION_MISSING');
324
        }
325
        $rootPath = realpath($path);
326
        $zip = new ZipArchive();
327
        $zip->open($destination, ZipArchive::CREATE | ZipArchive::OVERWRITE);
328
        if (!is_dir($path)) {
329
            $zip->addFile($path, basename($path));
330
            return $zip->close();
331
        }
332
        $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($rootPath), RecursiveIteratorIterator::LEAVES_ONLY);
333
        $root_len = strlen($rootPath) + 1;
334
        foreach ($files as $file) {
335
            if (!$file->isDir()) {
336
                $filePath = $file->getRealPath();
337
                $zip->addFile($filePath, substr($filePath, $root_len));
338
            }
339
        }
340
        return $zip->close();
341
    }
342
343
    /**
344
     * download url and save it to path
345
     *
346
     * e.g. => tools::downloadFile('http://example.com/exmaple.mp4','movie.mp4');
347
     *
348
     * @param string $url your url to be downloaded
349
     * @param string $path destination path for saving url
350
     * @param int $chunk_size size of each chunk of data (in KB)
351
     *
352
     * @return bool true on success and false in failure
353
     */
354
    public static function downloadFile (string $url, string $path, int $chunk_size = 512): bool {
355
        $file = fopen($url, 'rb');
356
        if (!$file) return false;
0 ignored issues
show
$file is of type resource, thus it always evaluated to false.
Loading history...
357
        $path = fopen($path, 'wb');
358
        if (!$path) return false;
359
360
        $length = $chunk_size * 1024;
361
        while (!feof($file)){
362
            fwrite($path, fread($file, $length), $length);
363
        }
364
        fclose($path);
365
        fclose($file);
366
        return true;
367
    }
368
369
    /** @todo receive ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'] from parameters */
370
    /**
371
     * Convert byte to symbolic size like 2.98 MB
372
     *
373
     * You could set `precision` to configure decimals after number(2 for 2.98 and 3 for 2.987)
374
     *
375
     * e.g. => tools::byteFormat(123456789);
376
     *
377
     * e.g. => tools::byteFormat(byte: 123456789);
378
     *
379
     * @param int  $byte      size in byte
380
     * @param int  $precision decimal precision
381
     * @param bool $space_between
382
     *
383
     * @return string
384
     */
385
    public static function byteFormat (int $byte, int $precision = 2, bool $space_between = true): string {
386
        $rate_counter = 0;
387
388
        while ($byte > 1024){
389
            $byte /= 1024;
390
            $rate_counter++;
391
        }
392
393
        if ($rate_counter !== 0) {
394
            $byte = round($byte, $precision);
395
        }
396
397
        return $byte . ($space_between ? ' ' : '') . ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'][$rate_counter];
398
    }
399
400
    /**
401
     * Escape text for different parse_modes
402
     *
403
     * mode parameter can be : `MarkdownV2` , `Markdown` , `HTML` , default : `parseMode::HTML`(`HTML`)
404
     *
405
     * e.g. => tools::modeEscape('hello men! *I* Have nothing anymore');
406
     *
407
     * e.g. => tools::modeEscape(text: 'hello men! *I* Have nothing anymore');
408
     *
409
     * @param string $text Your text e.g. => 'hello men! *I* Have nothing anymore'
410
     * @param string $mode Your selected mode e.g. => `parseMode::HTML` | `HTML`
411
     *
412
     * @return string|false return false when mode is incorrect
413
     */
414
    public static function modeEscape (string $text, string $mode = parseMode::HTML): string|false {
415
        return match ($mode) {
416
            parseMode::HTML => str_replace(['&', '<', '>',], ['&amp;', '&lt;', '&gt;',], $text),
417
            parseMode::MARKDOWN => str_replace(['\\', '_', '*', '`', '['], ['\\\\', '\_', '\*', '\`', '\[',], $text),
418
            parseMode::MARKDOWNV2 => str_replace(
419
                ['\\', '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'],
420
                ['\\\\', '\_', '\*', '\[', '\]', '\(', '\)', '\~', '\`', '\>', '\#', '\+', '\-', '\=', '\|', '\{', '\}', '\.', '\!'],
421
                $text),
422
            default => false
423
        };
424
    }
425
426
    /**
427
     * Clear text and make it safer to use
428
     *
429
     * e.g. => tools::clearText(text: 'asdasdasdas');
430
     *
431
     * e.g. => tools::clearText($message->text);
432
     *
433
     * @param string $text your text to be cleaned
434
     *
435
     * @return string
436
     */
437
    public static function clearText(string $text): string {
438
        return htmlentities(strip_tags(htmlspecialchars(stripslashes(trim($text)))));
439
    }
440
441
    /**
442
     * Show time different in array format
443
     *
444
     * Its calculated different between given time and now
445
     *
446
     * e.g. => tools::time2string(datetime: 1636913656);
447
     *
448
     * e.g. => tools::time2string(time());
449
     *
450
     * @param int|string $target_time your chosen time for compare with base_time, could be timestamp or could be a string like `next sunday`
451
     * @param int|string|null $base_time base time, could be timestamp or could be a string like `next sunday`, set null for current time
452
     *
453
     * @return array{status: string,year: string,month: string,day: string,hour: string,minute: string,second: string}
454
     * @throws Exception
455
     */
456
    public static function timeDiff (int|string $target_time, int|string|null $base_time = null): array {
457
        $base_time = new DateTime($base_time ?? '@'.time());
458
        $target_time = new DateTime(is_numeric($target_time) ? '@' . $target_time : $target_time . ' +00:00');
459
460
        $diff = $base_time->diff($target_time);
461
        $string = ['year' => 'y', 'month' => 'm', 'day' => 'd', 'hour' => 'h', 'minute' => 'i', 'second' => 's'];
462
        foreach ($string as $k => &$v) {
463
            if ($diff->{$v}) {
464
                $v = $diff->{$v};
465
            }
466
            else unset($string[$k]);
467
        }
468
        $string['status'] = $base_time < $target_time ? 'later' : 'ago';
469
470
        return count($string) > 1 ? $string : ['status' => 'now'];
471
    }
472
473
    /**
474
     * same as mysqli::real_escape_string but does not need a db connection and allow array escape
475
     *
476
     * e.g. => tools::realEscapeString(input: $text1);
477
     *
478
     * e.g. => tools::realEscapeString([$text1,$text2,$text3]);
479
     *
480
     * @param string|string[] $input
481
     *
482
     * @return string[]|string
483
     */
484
    public static function realEscapeString(string|array $input): string|array {
485
        if(is_array($input)) {
0 ignored issues
show
The condition is_array($input) is always true.
Loading history...
486
            return array_map(__METHOD__, $input);
487
        }
488
489
        if(!empty($input) && is_string($input)) {
490
            return str_replace(['\\', "\0", "\n", "\r", "'", '"', "\x1a"], ['\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'], $input);
491
        }
492
493
        return $input;
494
    }
495
496
    /**
497
     * replace `search` with `replace` in `subject` but only one of it(the first result)
498
     *
499
     * e.g. => tools::strReplaceFirst('hello','bye','hello :)');
500
     *
501
     * @param string|string[] $search
502
     * @param string|string[] $replace
503
     * @param string|string[] $subject
504
     *
505
     * @return string[]|string
506
     */
507
    public static function strReplaceFirst(string|array $search, string|array $replace, string|array $subject): string|array {
508
        $pos = strpos($subject, $search);
0 ignored issues
show
$subject of type string[] is incompatible with the type string expected by parameter $haystack of strpos(). ( Ignorable by Annotation )

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

508
        $pos = strpos(/** @scrutinizer ignore-type */ $subject, $search);
Loading history...
$search of type string[] is incompatible with the type string expected by parameter $needle of strpos(). ( Ignorable by Annotation )

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

508
        $pos = strpos($subject, /** @scrutinizer ignore-type */ $search);
Loading history...
509
        if ($pos !== false) {
510
            return substr_replace($subject, $replace, $pos, strlen($search));
0 ignored issues
show
$search of type string[] is incompatible with the type string expected by parameter $string of strlen(). ( Ignorable by Annotation )

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

510
            return substr_replace($subject, $replace, $pos, strlen(/** @scrutinizer ignore-type */ $search));
Loading history...
511
        }
512
        return $subject;
513
    }
514
515
    /**
516
     * replace `search` with `replace` in `subject` but only one of it(the last result)
517
     *
518
     * e.g. => tools::strReplaceLast('hello','bye','hello :)');
519
     *
520
     * @param string|string[] $search
521
     * @param string|string[] $replace
522
     * @param string|string[] $subject
523
     *
524
     * @return string[]|string
525
     */
526
    public static function strReplaceLast(string|array $search, string|array $replace, string|array $subject): string|array {
527
        $pos = strrpos($subject, $search);
0 ignored issues
show
$subject of type string[] is incompatible with the type string expected by parameter $haystack of strrpos(). ( Ignorable by Annotation )

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

527
        $pos = strrpos(/** @scrutinizer ignore-type */ $subject, $search);
Loading history...
$search of type string[] is incompatible with the type string expected by parameter $needle of strrpos(). ( Ignorable by Annotation )

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

527
        $pos = strrpos($subject, /** @scrutinizer ignore-type */ $search);
Loading history...
528
        if ($pos !== false) {
529
            return substr_replace($subject, $replace, $pos, strlen($search));
0 ignored issues
show
$search of type string[] is incompatible with the type string expected by parameter $string of strlen(). ( Ignorable by Annotation )

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

529
            return substr_replace($subject, $replace, $pos, strlen(/** @scrutinizer ignore-type */ $search));
Loading history...
530
        }
531
        return $subject;
532
    }
533
534
    /**
535
     * Convert file_id to fileType
536
     *
537
     * Thank you [Daniil](https://github.com/danog) for file_id decode pattern
538
     *
539
     * @param string $file_id
540
     *
541
     * @return string see possible values in fileType class
542
     */
543
    public static function fileType (string $file_id): string {
544
        $data = base64_decode(str_pad(strtr($file_id, '-_', '+/'), strlen($file_id) % 4, '='));
545
        $new = '';
546
        $last = '';
547
        foreach (str_split($data) as $char) {
548
            if ($last === "\0") {
549
                $new .= str_repeat($last, ord($char));
550
                $last = '';
551
            }
552
            else {
553
                $new .= $last;
554
                $last = $char;
555
            }
556
        }
557
        $data = unpack('VtypeId/Vdc_id', $new . $last);
558
        $data['typeId'] = $data['typeId'] & ~33554432 & ~16777216;
559
        return [
560
            fileTypes::THUMBNAIL,
561
            fileTypes::PROFILE_PHOTO,
562
            fileTypes::PHOTO,
563
            fileTypes::VOICE,
564
            fileTypes::VIDEO,
565
            fileTypes::DOCUMENT,
566
            fileTypes::ENCRYPTED,
567
            fileTypes::TEMP,
568
            fileTypes::STICKER,
569
            fileTypes::AUDIO,
570
            fileTypes::ANIMATION,
571
            fileTypes::ENCRYPTED_THUMBNAIL,
572
            fileTypes::WALLPAPER,
573
            fileTypes::VIDEO_NOTE,
574
            fileTypes::SECURE_RAW,
575
            fileTypes::SECURE,
576
            fileTypes::BACKGROUND,
577
            fileTypes::SIZE
578
        ][$data['typeId']];
579
    }
580
581
    /**
582
     * Generate random string
583
     *
584
     * e.g. => tools::randomString();
585
     *
586
     * e.g. => tools::randomString(16,'abcdefg');
587
     *
588
     * e.g. => tools::randomString(length: 16,characters: 'abcdefg');
589
     *
590
     * @param int    $length     length of generated string
591
     * @param string $characters string constructor characters
592
     *
593
     * @return string
594
     */
595
    public static function randomString (int $length = 16, string $characters = 'aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ'): string {
596
        $rand_string = '';
597
        $char_len = strlen($characters) - 1;
598
        for ($i = 0; $i < $length; $i ++) {
599
            $rand_string .= $characters[rand(0, $char_len)];
600
        }
601
        return $rand_string;
602
    }
603
604
    /**
605
     * create normal keyboard and inline keyboard easily
606
     *
607
     * you must set keyboard parameter(for normal keyboard) or inline parameter(for inline keyboard)
608
     *
609
     * if you set both , keyboard will be processed and inline will be ignored
610
     *
611
     *  
612
     *
613
     * con for request contact , loc for request location, web||URL for webapp, pull||POLLTYPE for poll
614
     *
615
     * e.g. => tools::easyKey([['button 1 in row 1','button 2 in row 1'],['button 1 in row 2'],['contact button in row 3||con'],['location button in row 4||loc']]);
616
     *
617
     *  
618
     *
619
     * e.g. => tools::easyKey(inline: [[['button 1 in row 1','this is callback button'],['button 2 in row 1','https://this-is-url-button.com']],[['demo button in row 2']]]);
620
     *
621
     * @param string[][] $keyboard array(as rows) of array(buttons) of string
622
     * @param array[][]  $inline   array(as rows) of array(buttons) of array(button data)
623
     *
624
     * @return inlineKeyboardMarkup|replyKeyboardMarkup replyKeyboardMarkup for keyboard and inlineKeyboardMarkup for inline
625
     * @throws bptException
626
     */
627
    public static function easyKey(array $keyboard = [], array $inline = []): inlineKeyboardMarkup|replyKeyboardMarkup {
628
        if (!empty($keyboard)) {
629
            $keyboard_object = new replyKeyboardMarkup();
630
            $keyboard_object->setResize_keyboard($keyboard['resize'] ?? true);
631
            if (isset($keyboard['one_time'])) {
632
                $keyboard_object->setOne_time_keyboard($keyboard['one_time']);
633
            }
634
            $rows = [];
635
            foreach ($keyboard as $row) {
636
                if (!is_array($row)) continue;
637
                $buttons = [];
638
                foreach ($row as $base_button) {
639
                    $button_info = explode('||', $base_button);
640
                    $button = new keyboardButton();
641
                    $button->setText($button_info[0] ?? $base_button);
642
                    if (count($button_info) > 1) {
643
                        if ($button_info[1] === 'con') {
644
                            $button->setRequest_contact(true);
645
                        }
646
                        elseif ($button_info[1] === 'loc') {
647
                            $button->setRequest_location(true);
648
                        }
649
                        elseif ($button_info[1] === 'poll') {
650
                            $type = $button_info[2] === pollType::QUIZ ? pollType::QUIZ : pollType::REGULAR;
651
                            $button->setRequest_poll((new keyboardButtonPollType())->setType($type));
652
                        }
653
                        elseif ($button_info[1] === 'web' && isset($button_info[2])) {
654
                            $url = $button_info[2];
655
                            $button->setWeb_app((new webAppInfo())->setUrl($url));
656
                        }
657
                    }
658
                    $buttons[] = $button;
659
                }
660
                $rows[] = $buttons;
661
            }
662
            $keyboard_object->setKeyboard($rows);
663
            return $keyboard_object;
664
        }
665
        if (!empty($inline)) {
666
            $keyboard_object = new inlineKeyboardMarkup();
667
            $rows = [];
668
            foreach ($inline as $row) {
669
                $buttons = [];
670
                foreach ($row as $button_info) {
671
                    $button = new inlineKeyboardButton();
672
                    $button->setText($button_info[0]);
673
                    if (isset($button_info[1])) {
674
                        if (filter_var($button_info[1], FILTER_VALIDATE_URL) && str_starts_with($button_info[1], 'http')) {
675
                            $button->setUrl($button_info[1]);
676
                        }
677
                        else {
678
                            $button->setCallback_data($button_info[1]);
679
                        }
680
                    }
681
                    else {
682
                        $button->setUrl('https://t.me/BPT_CH');
683
                    }
684
                    $buttons[] = $button;
685
                }
686
                $rows[] = $buttons;
687
            }
688
            $keyboard_object->setInline_keyboard($rows);
689
            return $keyboard_object;
690
        }
691
        logger::write("tools::eKey function used\nkeyboard or inline parameter must be set",loggerTypes::ERROR);
692
        throw new bptException('ARGUMENT_NOT_FOUND_KEYBOARD_INLINE');
693
    }
694
695
    /**
696
     * create invite link for user which use shortEncode method and can be handled by BPT database
697
     *
698
     * e.g. => tools::inviteLink(123456789,'Username_bot');
699
     *
700
     * e.g. => tools::inviteLink(123456789);
701
     *
702
     * @param int|null $user_id user id , default : catchFields(fields::USER_ID)
703
     * @param string|null  $bot_username bot username , default : telegram::getMe()->username
704
     *
705
     * @return string
706
     */
707
    public static function inviteLink (int $user_id = null, string $bot_username = null): string {
708
        if (empty($user_id)) $user_id = telegram::catchFields(fields::USER_ID);
709
        if (empty($bot_username)) $bot_username = telegram::getMe()->username;
710
        return 'https://t.me/' . str_replace('@', '', $bot_username) . '?start=ref_' . tools::shortEncode($user_id);
711
    }
712
713
    /**
714
     * encrypt or decrypt a text with really high security
715
     *
716
     * action parameter must be `encrypt` or `decrypt` ( use codecAction constant class for easy use )
717
     *
718
     * string parameter is your hash(received when use encrypt) or the text you want to encrypt
719
     *
720
     * for decrypt , you must have key and iv parameter. you can found them in result of encrypt(or if you specified them in encrypt call)
721
     *
722
     * e.g. => tools::codec(action: 'decrypt', text: '9LqUf9DSuRRwfo03RnA5Kw==', key: '39aaadf402f9b921b1d44e33ee3b022716a518e97d6a7b55de8231de501b4f34', iv: 'a2e5904a4110169e');
723
     *
724
     * e.g. => tools::codec(codecAction::ENCRYPT,'hello world');
725
     *
726
     * e.g. => tools::codec(codecAction::ENCRYPT,'hello world', 'key' ,'iv');
727
     *
728
     * @param string      $action e.g. => codecAction::ENCRYPT | 'encrypt'
729
     * @param string      $text   e.g. => 'hello world'
730
     * @param null|string $key    e.g. => Optional, 39aaadf402f9b921b1d44e33ee3b022716a518e97d6a7b55de8231de501b4f34
731
     * @param null|string $iv     e.g. => Optional, a2e5904a4110169e
732
     *
733
     * @return string|bool|array{hash:string, key:string, iv:string}
734
     * @throws bptException
735
     */
736
    public static function codec (string $action, string $text, string $key = null, string $iv = null): bool|array|string {
737
        if (!extension_loaded('openssl')) {
738
            logger::write("tools::codec function used\nopenssl extension is not found , It may not be installed or enabled",loggerTypes::ERROR);
739
            throw new bptException('OPENSSL_EXTENSION_MISSING');
740
        }
741
        if ($action === codecAction::ENCRYPT) {
742
            $key = $key ?? self::randomString(64);
743
            $iv = $iv ?? self::randomString();
744
            $output = base64_encode(openssl_encrypt($text, 'AES-256-CBC', $key, 1, $iv));
745
            return ['hash' => $output, 'key' => $key, 'iv' => $iv];
746
        }
747
        if ($action === codecAction::DECRYPT) {
748
            if (empty($key)) {
749
                logger::write("tools::codec function used\nkey parameter is not set",loggerTypes::ERROR);
750
                throw new bptException('ARGUMENT_NOT_FOUND_KEY');
751
            }
752
            if (empty($iv)) {
753
                logger::write("tools::codec function used\niv parameter is not set",loggerTypes::ERROR);
754
                throw new bptException('ARGUMENT_NOT_FOUND_IV');
755
            }
756
            return openssl_decrypt(base64_decode($text), 'AES-256-CBC', $key, 1, $iv);
757
        }
758
        logger::write("tools::codec function used\naction is not right, its must be `encode` or `decode`",loggerTypes::WARNING);
759
        return false;
760
    }
761
762
    /**
763
     * encode int to a string
764
     *
765
     * e.g. => tools::shortEncode(123456789);
766
     *
767
     * @param int $num
768
     *
769
     * @return string
770
     */
771
    public static function shortEncode(int $num): string {
772
        $codes = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
773
        $array = [];
774
        while ($num > 0){
775
            $array[] = $num % 62;
776
            $num = floor($num / 62);
777
        }
778
        if (count($array) < 1) $array = [0];
779
        foreach ($array as &$value) {
780
            $value = $codes[$value];
781
        }
782
        return strrev(implode('',$array));
783
    }
784
785
    /**
786
     * decode string to int
787
     *
788
     * e.g. => tools::shortDecode('8m0Kx');
789
     *
790
     * @param string $text
791
     *
792
     * @return int
793
     */
794
    public static function shortDecode(string $text): int{
795
        $codes = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
796
        $num = 0;
797
        $text = str_split(strrev($text));
798
        foreach ($text as $key=>$value) {
799
            $num += strpos($codes,$value) * pow(62,$key);
800
        }
801
        return $num;
802
    }
803
804
    /**
805
     * Get remote IP address
806
     *
807
     * @return string
808
     */
809
    public static function remoteIP(): string {
810
        $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
811
        if (settings::$cloudflare_verify && isset($_SERVER['HTTP_CF_CONNECTING_IP']) && tools::isCloudFlare($ip)) {
812
            return $_SERVER['HTTP_CF_CONNECTING_IP'];
813
        }
814
        if (settings::$arvancloud_verify && isset($_SERVER['HTTP_AR_REAL_IP']) && tools::isArvanCloud($ip)) {
815
            return $_SERVER['HTTP_AR_REAL_IP'];
816
        }
817
        return $ip;
818
    }
819
}