Passed
Push — main ( 64ea8c...cb0dfb )
by Miaad
10:02
created

tools   F

Complexity

Total Complexity 115

Size/Duplication

Total Lines 763
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 264
c 1
b 0
f 0
dl 0
loc 763
rs 2
wmc 115

27 Methods

Rating   Name   Duplication   Size   Complexity  
A zip() 0 22 5
A fileType() 0 36 3
A modeEscape() 0 9 1
A shortEncode() 0 12 4
A isUsername() 0 3 4
A byteFormat() 0 13 4
A timeDiff() 0 16 6
A size() 0 18 6
A isArvanCloud() 0 8 3
A inviteLink() 0 4 3
B remoteIP() 0 9 7
A isShorted() 0 2 1
A codec() 0 24 6
A isToken() 0 12 4
A isJoined() 0 16 6
A randomString() 0 7 2
A joinChecker() 0 16 5
A downloadFile() 0 13 4
A realEscapeString() 0 10 4
A shortDecode() 0 8 2
A ipInRange() 0 7 2
A isCloudFlare() 0 8 3
D easyKey() 0 65 19
A strReplaceFirst() 0 6 2
A delete() 0 18 6
A isTelegram() 0 2 2
A clearText() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like tools often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use tools, and based on these observations, apply Extract Interface, too.

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
introduced by
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
introduced by
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
            $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($rootPath), RecursiveIteratorIterator::LEAVES_ONLY);
330
            $root_len = strlen($rootPath) + 1;
331
            foreach ($files as $file) {
332
                if (!$file->isDir()) {
333
                    $filePath = $file->getRealPath();
334
                    $zip->addFile($filePath, substr($filePath, $root_len));
335
                }
336
            }
337
        }
338
        else {
339
            $zip->addFile($path, basename($path));
340
        }
341
        return $zip->close();
342
    }
343
344
    /**
345
     * download url and save it to path
346
     *
347
     * e.g. => tools::downloadFile('http://example.com/exmaple.mp4','movie.mp4');
348
     *
349
     * @param string $url your url to be downloaded
350
     * @param string $path destination path for saving url
351
     * @param int $chunk_size size of each chunk of data (in KB)
352
     *
353
     * @return bool true on success and false in failure
354
     */
355
    public static function downloadFile (string $url, string $path, int $chunk_size = 512): bool {
356
        $file = fopen($url, 'rb');
357
        if (!$file) return false;
0 ignored issues
show
introduced by
$file is of type resource, thus it always evaluated to false.
Loading history...
358
        $path = fopen($path, 'wb');
359
        if (!$path) return false;
360
361
        $length = $chunk_size * 1024;
362
        while (!feof($file)){
363
            fwrite($path, fread($file, $length), $length);
364
        }
365
        fclose($path);
366
        fclose($file);
367
        return true;
368
    }
369
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
462
        $string = ['year' => 'y', 'month' => 'm', 'day' => 'd', 'hour' => 'h', 'minute' => 'i', 'second' => 's'];
463
        foreach ($string as $k => &$v) {
464
            if ($diff->$v) {
465
                $v = $diff->$v;
466
            }
467
            else unset($string[$k]);
468
        }
469
        $string['status'] = $base_time < $target_time ? 'later' : 'ago';
470
471
        return count($string) > 1 ? $string : ['status' => 'now'];
472
    }
473
474
    /**
475
     * same as mysqli::real_escape_string but does not need a db connection and allow array escape
476
     *
477
     * e.g. => tools::realEscapeString(input: $text1);
478
     *
479
     * e.g. => tools::realEscapeString([$text1,$text2,$text3]);
480
     *
481
     * @param string|string[] $input
482
     *
483
     * @return string[]|string
484
     */
485
    public static function realEscapeString(string|array $input): string|array {
486
        if(is_array($input)) {
0 ignored issues
show
introduced by
The condition is_array($input) is always true.
Loading history...
487
            return array_map(__METHOD__, $input);
488
        }
489
490
        if(!empty($input) && is_string($input)) {
491
            return str_replace(['\\', "\0", "\n", "\r", "'", '"', "\x1a"], ['\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'], $input);
492
        }
493
494
        return $input;
495
    }
496
497
    /**
498
     * replace `search` with `replace` in `subject` but only one of it(the first result)
499
     *
500
     * e.g. => tools::strReplaceFirst('hello','bye','hello :)');
501
     *
502
     * @param string|string[] $search
503
     * @param string|string[] $replace
504
     * @param string|string[] $subject
505
     *
506
     * @return string[]|string
507
     */
508
    public static function strReplaceFirst(string|array $search, string|array $replace, string|array $subject): string|array {
509
        $pos = strpos($subject, $search);
0 ignored issues
show
Bug introduced by
$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

509
        $pos = strpos(/** @scrutinizer ignore-type */ $subject, $search);
Loading history...
Bug introduced by
$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

509
        $pos = strpos($subject, /** @scrutinizer ignore-type */ $search);
Loading history...
510
        if ($pos !== false) {
511
            return substr_replace($subject, $replace, $pos, strlen($search));
0 ignored issues
show
Bug introduced by
$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

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