Passed
Push — main ( ee195b...97f704 )
by Miaad
01:57
created

tools::crypto()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

506
        $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

506
        $pos = strpos($subject, /** @scrutinizer ignore-type */ $search);
Loading history...
507
        if ($pos !== false) {
508
            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

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