Passed
Push — main ( ccd8f9...b2c2ea )
by Miaad
01:47
created

tools::codec()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 25
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 19
c 1
b 0
f 0
nc 6
nop 4
dl 0
loc 25
rs 9.0111
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
     *
379
     * @return string
380
     */
381
    public static function byteFormat (int $byte, int $precision = 2): string {
382
        $rate_counter = 0;
383
384
        while ($byte > 1024){
385
            $byte /= 1024;
386
            $rate_counter++;
387
        }
388
389
        if ($rate_counter !== 0) {
390
            $byte = round($byte, $precision);
391
        }
392
393
        return $byte . ' ' . ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'][$rate_counter];
394
    }
395
396
    /**
397
     * Escape text for different parse_modes
398
     *
399
     * mode parameter can be : `MarkdownV2` , `Markdown` , `HTML` , default : `parseMode::HTML`(`HTML`)
400
     *
401
     * e.g. => tools::modeEscape('hello men! *I* Have nothing anymore');
402
     *
403
     * e.g. => tools::modeEscape(text: 'hello men! *I* Have nothing anymore');
404
     *
405
     * @param string $text Your text e.g. => 'hello men! *I* Have nothing anymore'
406
     * @param string $mode Your selected mode e.g. => `parseMode::HTML` | `HTML`
407
     *
408
     * @return string|false return false when mode is incorrect
409
     */
410
    public static function modeEscape (string $text, string $mode = parseMode::HTML): string|false {
411
        return match ($mode) {
412
            parseMode::HTML => str_replace(['&', '<', '>',], ["&amp;", "&lt;", "&gt;",], $text),
413
            parseMode::MARKDOWN => str_replace(['\\', '_', '*', '`', '['], ['\\\\', '\_', '\*', '\`', '\[',], $text),
414
            parseMode::MARKDOWNV2 => str_replace(
415
                ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!', '\\'],
416
                ['\_', '\*', '\[', '\]', '\(', '\)', '\~', '\`', '\>', '\#', '\+', '\-', '\=', '\|', '\{', '\}', '\.', '\!', '\\\\'],
417
                $text),
418
            default => false
419
        };
420
    }
421
422
    /**
423
     * Clear text and make it safer to use
424
     *
425
     * e.g. => tools::clearText(text: 'asdasdasdas');
426
     *
427
     * e.g. => tools::clearText($message->text);
428
     *
429
     * @param string $text your text to be cleaned
430
     *
431
     * @return string
432
     */
433
    public static function clearText(string $text): string {
434
        return htmlentities(strip_tags(htmlspecialchars(stripslashes(trim($text)))));
435
    }
436
437
    /**
438
     * Show time different in array format
439
     *
440
     * Its calculated different between given time and now
441
     *
442
     * e.g. => tools::time2string(datetime: 1636913656);
443
     *
444
     * e.g. => tools::time2string(time());
445
     *
446
     * @param int|string $target_time your chosen time for compare with base_time, could be timestamp or could be a string like `next sunday`
447
     * @param int|string|null $base_time base time, could be timestamp or could be a string like `next sunday`, set null for current time
448
     *
449
     * @return array{status: string,year: string,month: string,day: string,hour: string,minute: string,second: string}
450
     * @throws Exception
451
     */
452
    public static function timeDiff (int|string $target_time, int|string|null $base_time = null): array {
453
        if (!isset($base_time)) {
454
            $base_time = '@'.time();
455
        }
456
        $base_time = new DateTime($base_time);
457
        $target_time = new DateTime(is_numeric($target_time) ? '@' . $target_time : $target_time . ' +00:00');
458
459
        $diff = $base_time->diff($target_time);
460
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
introduced by
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
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

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

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

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