diff --git a/package-lock.json b/package-lock.json index aca2c7294..54e6b5e99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "license": "GPL-3.0-only", "dependencies": { + "@types/crypto-js": "^4.1.1", + "crypto-js": "^4.1.1", "webpack-dev-server": "^3.11.2" }, "devDependencies": { @@ -24,6 +26,7 @@ "@cryptography/sha1": "^0.2.0", "@cryptography/sha256": "^0.2.0", "@peculiar/webcrypto": "^1.1.7", + "@types/big-integer": "^0.0.31", "@types/chrome": "0.0.139", "@types/jest": "^26.0.23", "@types/serviceworker-webpack-plugin": "^1.0.2", @@ -31,6 +34,7 @@ "babel-jest": "^26.6.3", "babel-loader": "^8.2.2", "babel-preset-es2015": "^6.24.1", + "big-integer": "^1.6.51", "compression": "^1.7.4", "css-loader": "^3.6.0", "dotenv-webpack": "^7.0.3", @@ -5146,6 +5150,16 @@ "@babel/types": "^7.3.0" } }, + "node_modules/@types/big-integer": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/@types/big-integer/-/big-integer-0.0.31.tgz", + "integrity": "sha512-nYrYenHwC07vTBXoQ8jUUi6sednNYHGQxh0ecvfWm46n3djgxxbe7AZIJVaGjzQaEQVEcH6KmB6VMt//vAP0AA==", + "deprecated": "This is a stub types definition for BigInteger.js (https://github.com/peterolson/BigInteger.js). BigInteger.js provides its own type definitions, so you don't need @types/big-integer installed!", + "dev": true, + "dependencies": { + "big-integer": "*" + } + }, "node_modules/@types/chrome": { "version": "0.0.139", "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.139.tgz", @@ -5156,6 +5170,11 @@ "@types/har-format": "*" } }, + "node_modules/@types/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==" + }, "node_modules/@types/filesystem": { "version": "0.0.30", "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.30.tgz", @@ -7024,6 +7043,15 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -7540,7 +7568,7 @@ "version": "3.5.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", - "dev": true, + "devOptional": true, "dependencies": { "anymatch": "~3.1.1", "braces": "~3.0.2", @@ -7561,7 +7589,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, + "devOptional": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -7574,7 +7602,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -7583,7 +7611,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, + "devOptional": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -7595,7 +7623,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, + "devOptional": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -7607,7 +7635,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -7621,7 +7648,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, + "devOptional": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -7633,7 +7660,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, + "devOptional": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -7645,7 +7672,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.12.0" } @@ -7654,7 +7681,7 @@ "version": "3.5.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", - "dev": true, + "devOptional": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -7666,7 +7693,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, + "devOptional": true, "dependencies": { "is-number": "^7.0.0" }, @@ -8215,6 +8242,11 @@ "node": "*" } }, + "node_modules/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, "node_modules/css-blank-pseudo": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz", @@ -10297,14 +10329,12 @@ "node_modules/fsevents/node_modules/abbrev": { "version": "1.1.1", "inBundle": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/fsevents/node_modules/ansi-regex": { "version": "2.1.1", "inBundle": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -10312,14 +10342,12 @@ "node_modules/fsevents/node_modules/aproba": { "version": "1.2.0", "inBundle": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/fsevents/node_modules/are-we-there-yet": { "version": "1.1.5", "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -10328,14 +10356,12 @@ "node_modules/fsevents/node_modules/balanced-match": { "version": "1.0.0", "inBundle": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/fsevents/node_modules/brace-expansion": { "version": "1.1.11", "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -10344,14 +10370,12 @@ "node_modules/fsevents/node_modules/chownr": { "version": "1.1.4", "inBundle": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/fsevents/node_modules/code-point-at": { "version": "1.1.0", "inBundle": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -10359,26 +10383,22 @@ "node_modules/fsevents/node_modules/concat-map": { "version": "0.0.1", "inBundle": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/fsevents/node_modules/console-control-strings": { "version": "1.1.0", "inBundle": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/fsevents/node_modules/core-util-is": { "version": "1.0.2", "inBundle": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/fsevents/node_modules/debug": { "version": "3.2.6", "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { "ms": "^2.1.1" } @@ -10387,7 +10407,6 @@ "version": "0.6.0", "inBundle": true, "license": "MIT", - "optional": true, "engines": { "node": ">=4.0.0" } @@ -10395,14 +10414,12 @@ "node_modules/fsevents/node_modules/delegates": { "version": "1.0.0", "inBundle": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/fsevents/node_modules/detect-libc": { "version": "1.0.3", "inBundle": true, "license": "Apache-2.0", - "optional": true, "bin": { "detect-libc": "bin/detect-libc.js" }, @@ -10414,7 +10431,6 @@ "version": "1.2.7", "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { "minipass": "^2.6.0" } @@ -10422,14 +10438,12 @@ "node_modules/fsevents/node_modules/fs.realpath": { "version": "1.0.0", "inBundle": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/fsevents/node_modules/gauge": { "version": "2.7.4", "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -10445,7 +10459,6 @@ "version": "7.1.6", "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -10464,14 +10477,12 @@ "node_modules/fsevents/node_modules/has-unicode": { "version": "2.0.1", "inBundle": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/fsevents/node_modules/iconv-lite": { "version": "0.4.24", "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -10483,7 +10494,6 @@ "version": "3.0.3", "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { "minimatch": "^3.0.4" } @@ -10492,7 +10502,6 @@ "version": "1.0.6", "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -10501,14 +10510,12 @@ "node_modules/fsevents/node_modules/inherits": { "version": "2.0.4", "inBundle": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/fsevents/node_modules/ini": { "version": "1.3.5", "inBundle": true, "license": "ISC", - "optional": true, "engines": { "node": "*" } @@ -10517,7 +10524,6 @@ "version": "1.0.0", "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { "number-is-nan": "^1.0.0" }, @@ -10528,14 +10534,12 @@ "node_modules/fsevents/node_modules/isarray": { "version": "1.0.0", "inBundle": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/fsevents/node_modules/minimatch": { "version": "3.0.4", "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -10546,14 +10550,12 @@ "node_modules/fsevents/node_modules/minimist": { "version": "1.2.5", "inBundle": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/fsevents/node_modules/minipass": { "version": "2.9.0", "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -10563,7 +10565,6 @@ "version": "1.3.3", "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { "minipass": "^2.9.0" } @@ -10573,7 +10574,6 @@ "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { "minimist": "^1.2.5" }, @@ -10584,14 +10584,12 @@ "node_modules/fsevents/node_modules/ms": { "version": "2.1.2", "inBundle": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/fsevents/node_modules/needle": { "version": "2.3.3", "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { "debug": "^3.2.6", "iconv-lite": "^0.4.4", @@ -10608,7 +10606,6 @@ "version": "0.14.0", "inBundle": true, "license": "BSD-3-Clause", - "optional": true, "dependencies": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", @@ -10629,7 +10626,6 @@ "version": "4.0.3", "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { "abbrev": "1", "osenv": "^0.1.4" @@ -10642,7 +10638,6 @@ "version": "1.1.1", "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { "npm-normalize-package-bin": "^1.0.1" } @@ -10650,14 +10645,12 @@ "node_modules/fsevents/node_modules/npm-normalize-package-bin": { "version": "1.0.1", "inBundle": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/fsevents/node_modules/npm-packlist": { "version": "1.4.8", "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { "ignore-walk": "^3.0.1", "npm-bundled": "^1.0.1", @@ -10668,7 +10661,6 @@ "version": "4.1.2", "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -10680,7 +10672,6 @@ "version": "1.0.1", "inBundle": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -10689,7 +10680,6 @@ "version": "4.1.1", "inBundle": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -10698,7 +10688,6 @@ "version": "1.4.0", "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { "wrappy": "1" } @@ -10707,7 +10696,6 @@ "version": "1.0.2", "inBundle": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -10716,7 +10704,6 @@ "version": "1.0.2", "inBundle": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -10725,7 +10712,6 @@ "version": "0.1.5", "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -10735,7 +10721,6 @@ "version": "1.0.1", "inBundle": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -10743,14 +10728,12 @@ "node_modules/fsevents/node_modules/process-nextick-args": { "version": "2.0.1", "inBundle": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/fsevents/node_modules/rc": { "version": "1.2.8", "inBundle": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "optional": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -10765,7 +10748,6 @@ "version": "2.3.7", "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -10780,7 +10762,6 @@ "version": "2.7.1", "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { "glob": "^7.1.3" }, @@ -10791,26 +10772,22 @@ "node_modules/fsevents/node_modules/safe-buffer": { "version": "5.1.2", "inBundle": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/fsevents/node_modules/safer-buffer": { "version": "2.1.2", "inBundle": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/fsevents/node_modules/sax": { "version": "1.2.4", "inBundle": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/fsevents/node_modules/semver": { "version": "5.7.1", "inBundle": true, "license": "ISC", - "optional": true, "bin": { "semver": "bin/semver" } @@ -10818,20 +10795,17 @@ "node_modules/fsevents/node_modules/set-blocking": { "version": "2.0.0", "inBundle": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/fsevents/node_modules/signal-exit": { "version": "3.0.2", "inBundle": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/fsevents/node_modules/string_decoder": { "version": "1.1.1", "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -10840,7 +10814,6 @@ "version": "1.0.2", "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -10854,7 +10827,6 @@ "version": "3.0.1", "inBundle": true, "license": "MIT", - "optional": true, "dependencies": { "ansi-regex": "^2.0.0" }, @@ -10866,7 +10838,6 @@ "version": "2.0.1", "inBundle": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -10875,7 +10846,6 @@ "version": "4.4.13", "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", @@ -10892,14 +10862,12 @@ "node_modules/fsevents/node_modules/util-deprecate": { "version": "1.0.2", "inBundle": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/fsevents/node_modules/wide-align": { "version": "1.1.3", "inBundle": true, "license": "ISC", - "optional": true, "dependencies": { "string-width": "^1.0.2 || 2" } @@ -10907,14 +10875,12 @@ "node_modules/fsevents/node_modules/wrappy": { "version": "1.0.2", "inBundle": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/fsevents/node_modules/yallist": { "version": "3.1.1", "inBundle": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/function-bind": { "version": "1.1.1", @@ -19361,7 +19327,7 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8.6" }, @@ -25658,7 +25624,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", - "dev": true, "optional": true, "dependencies": { "chokidar": "^2.1.8" @@ -25669,7 +25634,6 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", "deprecated": "Chokidar 2 will break on node v14+. Upgrade to chokidar 3 with 15x less dependencies.", - "dev": true, "optional": true, "dependencies": { "anymatch": "^2.0.0", @@ -30948,6 +30912,15 @@ "@babel/types": "^7.3.0" } }, + "@types/big-integer": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/@types/big-integer/-/big-integer-0.0.31.tgz", + "integrity": "sha512-nYrYenHwC07vTBXoQ8jUUi6sednNYHGQxh0ecvfWm46n3djgxxbe7AZIJVaGjzQaEQVEcH6KmB6VMt//vAP0AA==", + "dev": true, + "requires": { + "big-integer": "*" + } + }, "@types/chrome": { "version": "0.0.139", "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.139.tgz", @@ -30958,6 +30931,11 @@ "@types/har-format": "*" } }, + "@types/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==" + }, "@types/filesystem": { "version": "0.0.30", "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.30.tgz", @@ -32595,6 +32573,12 @@ "tweetnacl": "^0.14.3" } }, + "big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "dev": true + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -33044,7 +33028,7 @@ "version": "3.5.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", - "dev": true, + "devOptional": true, "requires": { "anymatch": "~3.1.1", "braces": "~3.0.2", @@ -33060,7 +33044,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, + "devOptional": true, "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -33070,13 +33054,13 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true + "devOptional": true }, "braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, + "devOptional": true, "requires": { "fill-range": "^7.0.1" } @@ -33085,7 +33069,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, + "devOptional": true, "requires": { "to-regex-range": "^5.0.1" } @@ -33094,14 +33078,13 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "optional": true }, "glob-parent": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, + "devOptional": true, "requires": { "is-glob": "^4.0.1" } @@ -33110,7 +33093,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, + "devOptional": true, "requires": { "binary-extensions": "^2.0.0" } @@ -33119,13 +33102,13 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true + "devOptional": true }, "readdirp": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", - "dev": true, + "devOptional": true, "requires": { "picomatch": "^2.2.1" } @@ -33134,7 +33117,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, + "devOptional": true, "requires": { "is-number": "^7.0.0" } @@ -33602,6 +33585,11 @@ "randomfill": "^1.0.3" } }, + "crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, "css-blank-pseudo": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz", @@ -35233,23 +35221,19 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "bundled": true, - "optional": true + "bundled": true }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", - "bundled": true, - "optional": true + "bundled": true }, "are-we-there-yet": { "version": "1.1.5", "bundled": true, - "optional": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -35257,13 +35241,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -35271,69 +35253,57 @@ }, "chownr": { "version": "1.1.4", - "bundled": true, - "optional": true + "bundled": true }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "debug": { "version": "3.2.6", "bundled": true, - "optional": true, "requires": { "ms": "^2.1.1" } }, "deep-extend": { "version": "0.6.0", - "bundled": true, - "optional": true + "bundled": true }, "delegates": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "detect-libc": { "version": "1.0.3", - "bundled": true, - "optional": true + "bundled": true }, "fs-minipass": { "version": "1.2.7", "bundled": true, - "optional": true, "requires": { "minipass": "^2.6.0" } }, "fs.realpath": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "gauge": { "version": "2.7.4", "bundled": true, - "optional": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -35348,7 +35318,6 @@ "glob": { "version": "7.1.6", "bundled": true, - "optional": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -35360,13 +35329,11 @@ }, "has-unicode": { "version": "2.0.1", - "bundled": true, - "optional": true + "bundled": true }, "iconv-lite": { "version": "0.4.24", "bundled": true, - "optional": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -35374,7 +35341,6 @@ "ignore-walk": { "version": "3.0.3", "bundled": true, - "optional": true, "requires": { "minimatch": "^3.0.4" } @@ -35382,7 +35348,6 @@ "inflight": { "version": "1.0.6", "bundled": true, - "optional": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -35390,44 +35355,37 @@ }, "inherits": { "version": "2.0.4", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", - "bundled": true, - "optional": true + "bundled": true }, "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } }, "isarray": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "1.2.5", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.9.0", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -35436,7 +35394,6 @@ "minizlib": { "version": "1.3.3", "bundled": true, - "optional": true, "requires": { "minipass": "^2.9.0" } @@ -35444,20 +35401,17 @@ "mkdirp": { "version": "0.5.3", "bundled": true, - "optional": true, "requires": { "minimist": "^1.2.5" } }, "ms": { "version": "2.1.2", - "bundled": true, - "optional": true + "bundled": true }, "needle": { "version": "2.3.3", "bundled": true, - "optional": true, "requires": { "debug": "^3.2.6", "iconv-lite": "^0.4.4", @@ -35467,7 +35421,6 @@ "node-pre-gyp": { "version": "0.14.0", "bundled": true, - "optional": true, "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", @@ -35484,7 +35437,6 @@ "nopt": { "version": "4.0.3", "bundled": true, - "optional": true, "requires": { "abbrev": "1", "osenv": "^0.1.4" @@ -35493,20 +35445,17 @@ "npm-bundled": { "version": "1.1.1", "bundled": true, - "optional": true, "requires": { "npm-normalize-package-bin": "^1.0.1" } }, "npm-normalize-package-bin": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "npm-packlist": { "version": "1.4.8", "bundled": true, - "optional": true, "requires": { "ignore-walk": "^3.0.1", "npm-bundled": "^1.0.1", @@ -35516,7 +35465,6 @@ "npmlog": { "version": "4.1.2", "bundled": true, - "optional": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -35526,36 +35474,30 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", - "bundled": true, - "optional": true + "bundled": true }, "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } }, "os-homedir": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "os-tmpdir": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "osenv": { "version": "0.1.5", "bundled": true, - "optional": true, "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -35563,18 +35505,15 @@ }, "path-is-absolute": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "process-nextick-args": { "version": "2.0.1", - "bundled": true, - "optional": true + "bundled": true }, "rc": { "version": "1.2.8", "bundled": true, - "optional": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -35585,7 +35524,6 @@ "readable-stream": { "version": "2.3.7", "bundled": true, - "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -35599,45 +35537,37 @@ "rimraf": { "version": "2.7.1", "bundled": true, - "optional": true, "requires": { "glob": "^7.1.3" } }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", - "bundled": true, - "optional": true + "bundled": true }, "sax": { "version": "1.2.4", - "bundled": true, - "optional": true + "bundled": true }, "semver": { "version": "5.7.1", - "bundled": true, - "optional": true + "bundled": true }, "set-blocking": { "version": "2.0.0", - "bundled": true, - "optional": true + "bundled": true }, "signal-exit": { "version": "3.0.2", - "bundled": true, - "optional": true + "bundled": true }, "string_decoder": { "version": "1.1.1", "bundled": true, - "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -35645,7 +35575,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -35655,20 +35584,17 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } }, "strip-json-comments": { "version": "2.0.1", - "bundled": true, - "optional": true + "bundled": true }, "tar": { "version": "4.4.13", "bundled": true, - "optional": true, "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", @@ -35681,26 +35607,22 @@ }, "util-deprecate": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "wide-align": { "version": "1.1.3", "bundled": true, - "optional": true, "requires": { "string-width": "^1.0.2 || 2" } }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.1.1", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -42059,7 +41981,7 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true + "devOptional": true }, "pify": { "version": "4.0.1", @@ -46911,7 +46833,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", - "dev": true, "optional": true, "requires": { "chokidar": "^2.1.8" @@ -46921,7 +46842,6 @@ "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, "optional": true, "requires": { "anymatch": "^2.0.0", diff --git a/package.json b/package.json index 1e3e0d2e8..91f558588 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "author": "", "license": "GPL-3.0-only", "dependencies": { + "@types/crypto-js": "^4.1.1", + "crypto-js": "^4.1.1", "webpack-dev-server": "^3.11.2" }, "devDependencies": { @@ -35,6 +37,7 @@ "@cryptography/sha1": "^0.2.0", "@cryptography/sha256": "^0.2.0", "@peculiar/webcrypto": "^1.1.7", + "@types/big-integer": "^0.0.31", "@types/chrome": "0.0.139", "@types/jest": "^26.0.23", "@types/serviceworker-webpack-plugin": "^1.0.2", @@ -42,6 +45,7 @@ "babel-jest": "^26.6.3", "babel-loader": "^8.2.2", "babel-preset-es2015": "^6.24.1", + "big-integer": "^1.6.51", "compression": "^1.7.4", "css-loader": "^3.6.0", "dotenv-webpack": "^7.0.3", diff --git a/public/assets/audio/call_busy.mp3 b/public/assets/audio/call_busy.mp3 new file mode 100644 index 000000000..968279215 Binary files /dev/null and b/public/assets/audio/call_busy.mp3 differ diff --git a/public/assets/audio/call_connect.mp3 b/public/assets/audio/call_connect.mp3 new file mode 100644 index 000000000..5db9355ea Binary files /dev/null and b/public/assets/audio/call_connect.mp3 differ diff --git a/public/assets/audio/call_end.mp3 b/public/assets/audio/call_end.mp3 new file mode 100644 index 000000000..177373e77 Binary files /dev/null and b/public/assets/audio/call_end.mp3 differ diff --git a/public/assets/audio/call_incoming.mp3 b/public/assets/audio/call_incoming.mp3 new file mode 100644 index 000000000..932a00b2d Binary files /dev/null and b/public/assets/audio/call_incoming.mp3 differ diff --git a/public/assets/audio/call_outgoing.mp3 b/public/assets/audio/call_outgoing.mp3 new file mode 100644 index 000000000..16125a945 Binary files /dev/null and b/public/assets/audio/call_outgoing.mp3 differ diff --git a/public/assets/audio/voip_busy.mp3 b/public/assets/audio/voip_busy.mp3 deleted file mode 100644 index 1d6741e5e..000000000 Binary files a/public/assets/audio/voip_busy.mp3 and /dev/null differ diff --git a/public/assets/audio/voip_end.mp3 b/public/assets/audio/voip_end.mp3 deleted file mode 100644 index 1d6741e5e..000000000 Binary files a/public/assets/audio/voip_end.mp3 and /dev/null differ diff --git a/src/components/animationIntersector.ts b/src/components/animationIntersector.ts index 864141644..e955361a1 100644 --- a/src/components/animationIntersector.ts +++ b/src/components/animationIntersector.ts @@ -8,8 +8,8 @@ import rootScope from "../lib/rootScope"; import { IS_SAFARI } from "../environment/userAgent"; import { MOUNT_CLASS_TO } from "../config/debug"; import isInDOM from "../helpers/dom/isInDOM"; -import { indexOfAndSplice } from "../helpers/array"; import RLottiePlayer from "../lib/rlottie/rlottiePlayer"; +import indexOfAndSplice from "../helpers/array/indexOfAndSplice"; export interface AnimationItem { el: HTMLElement, diff --git a/src/components/appMediaPlaybackController.ts b/src/components/appMediaPlaybackController.ts index 03e68dab3..c42311a53 100644 --- a/src/components/appMediaPlaybackController.ts +++ b/src/components/appMediaPlaybackController.ts @@ -13,8 +13,7 @@ import { MOUNT_CLASS_TO } from "../config/debug"; import appDownloadManager from "../lib/appManagers/appDownloadManager"; import simulateEvent from "../helpers/dom/dispatchEvent"; import type { SearchSuperContext } from "./appSearchSuper."; -import { copy, deepEqual } from "../helpers/object"; -import { DocumentAttribute, Message, MessageMedia, PhotoSize } from "../layer"; +import { DocumentAttribute, Message, PhotoSize } from "../layer"; import appPhotosManager from "../lib/appManagers/appPhotosManager"; import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport"; import appAvatarsManager from "../lib/appManagers/appAvatarsManager"; @@ -22,6 +21,8 @@ import appPeersManager from "../lib/appManagers/appPeersManager"; import I18n from "../lib/langPack"; import SearchListLoader from "../helpers/searchListLoader"; import { onMediaLoad } from "../helpers/files"; +import copy from "../helpers/object/copy"; +import deepEqual from "../helpers/object/deepEqual"; // TODO: Safari: проверить стрим, включить его и сразу попробовать включить видео или другую песню // TODO: Safari: попробовать замаскировать подгрузку последнего чанка diff --git a/src/components/appNavigationController.ts b/src/components/appNavigationController.ts index fbd3274af..2e19f89cd 100644 --- a/src/components/appNavigationController.ts +++ b/src/components/appNavigationController.ts @@ -7,11 +7,10 @@ import { MOUNT_CLASS_TO } from "../config/debug"; import { IS_MOBILE_SAFARI } from "../environment/userAgent"; import { logger } from "../lib/logger"; -import { doubleRaf } from "../helpers/schedulers"; import blurActiveElement from "../helpers/dom/blurActiveElement"; import { cancelEvent } from "../helpers/dom/cancelEvent"; -import { indexOfAndSplice } from "../helpers/array"; import isSwipingBackSafari from "../helpers/dom/isSwipingBackSafari"; +import indexOfAndSplice from "../helpers/array/indexOfAndSplice"; export type NavigationItem = { type: 'left' | 'right' | 'im' | 'chat' | 'popup' | 'media' | 'menu' | diff --git a/src/components/appSearchSuper..ts b/src/components/appSearchSuper..ts index 8407dd498..df2bb4ca3 100644 --- a/src/components/appSearchSuper..ts +++ b/src/components/appSearchSuper..ts @@ -4,7 +4,6 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import { copy, getObjectKeysAndSort, safeAssign } from "../helpers/object"; import { escapeRegExp, limitSymbols } from "../helpers/string"; import appChatsManager from "../lib/appManagers/appChatsManager"; import appDialogsManager from "../lib/appManagers/appDialogsManager"; @@ -51,6 +50,9 @@ import { attachClickEvent, simulateClickEvent } from "../helpers/dom/clickEvent" import { MyDocument } from "../lib/appManagers/appDocsManager"; import AppMediaViewer from "./appMediaViewer"; import lockTouchScroll from "../helpers/dom/lockTouchScroll"; +import copy from "../helpers/object/copy"; +import getObjectKeysAndSort from "../helpers/object/getObjectKeysAndSort"; +import safeAssign from "../helpers/object/safeAssign"; //const testScroll = false; diff --git a/src/components/appSelectPeers.ts b/src/components/appSelectPeers.ts index 9f6794151..0478d2970 100644 --- a/src/components/appSelectPeers.ts +++ b/src/components/appSelectPeers.ts @@ -13,18 +13,19 @@ import Scrollable from "./scrollable"; import { FocusDirection } from "../helpers/fastSmoothScroll"; import CheckboxField from "./checkboxField"; import appProfileManager from "../lib/appManagers/appProfileManager"; -import { safeAssign } from "../helpers/object"; import { i18n, LangPackKey, _i18n } from "../lib/langPack"; import findUpAttribute from "../helpers/dom/findUpAttribute"; import findUpClassName from "../helpers/dom/findUpClassName"; import PeerTitle from "./peerTitle"; import { cancelEvent } from "../helpers/dom/cancelEvent"; import replaceContent from "../helpers/dom/replaceContent"; -import { filterUnique, indexOfAndSplice } from "../helpers/array"; import debounce from "../helpers/schedulers/debounce"; import windowSize from "../helpers/windowSize"; import appPeersManager, { IsPeerType } from "../lib/appManagers/appPeersManager"; import { generateDelimiter, SettingSection } from "./sidebarLeft"; +import filterUnique from "../helpers/array/filterUnique"; +import indexOfAndSplice from "../helpers/array/indexOfAndSplice"; +import safeAssign from "../helpers/object/safeAssign"; type SelectSearchPeerType = 'contacts' | 'dialogs' | 'channelParticipants'; diff --git a/src/components/avatar.ts b/src/components/avatar.ts index 2bf55a316..b7b3b9300 100644 --- a/src/components/avatar.ts +++ b/src/components/avatar.ts @@ -17,7 +17,7 @@ import appAvatarsManager from "../lib/appManagers/appAvatarsManager"; import AppMediaViewer from "./appMediaViewer"; import AppMediaViewerAvatar from "./appMediaViewerAvatar"; import { NULL_PEER_ID } from "../lib/mtproto/mtproto_config"; -import { isObject } from "../helpers/object"; +import isObject from "../helpers/object/isObject"; const onAvatarUpdate = (peerId: PeerId) => { appAvatarsManager.removeFromAvatarsCache(peerId); diff --git a/src/components/call/index.ts b/src/components/call/index.ts new file mode 100644 index 000000000..ea57996cc --- /dev/null +++ b/src/components/call/index.ts @@ -0,0 +1,424 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import IS_SCREEN_SHARING_SUPPORTED from "../../environment/screenSharingSupport"; +import { attachClickEvent } from "../../helpers/dom/clickEvent"; +import ControlsHover from "../../helpers/dom/controlsHover"; +import findUpClassName from "../../helpers/dom/findUpClassName"; +import { addFullScreenListener, cancelFullScreen, isFullScreen, requestFullScreen } from "../../helpers/dom/fullScreen"; +import { MediaSize } from "../../helpers/mediaSizes"; +import MovablePanel from "../../helpers/movablePanel"; +import safeAssign from "../../helpers/object/safeAssign"; +import toggleClassName from "../../helpers/toggleClassName"; +import type { AppAvatarsManager } from "../../lib/appManagers/appAvatarsManager"; +import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; +import CallInstance from "../../lib/calls/callInstance"; +import CALL_STATE from "../../lib/calls/callState"; +import I18n, { i18n } from "../../lib/langPack"; +import RichTextProcessor from "../../lib/richtextprocessor"; +import rootScope from "../../lib/rootScope"; +import animationIntersector from "../animationIntersector"; +import ButtonIcon from "../buttonIcon"; +import GroupCallMicrophoneIconMini from "../groupCall/microphoneIconMini"; +import { MovableState } from "../movableElement"; +import PeerTitle from "../peerTitle"; +import PopupElement from "../popups"; +import SetTransition from "../singleTransition"; +import makeButton from "./button"; +import CallDescriptionElement from "./description"; +import callVideoCanvasBlur from "./videoCanvasBlur"; + +const className = 'call'; + +let previousState: MovableState = { + width: 400, + height: 580 +}; + +export default class PopupCall extends PopupElement { + private instance: CallInstance; + private appAvatarsManager: AppAvatarsManager; + private appPeersManager: AppPeersManager; + private peerId: PeerId; + + private description: CallDescriptionElement; + private emojisSubtitle: HTMLElement; + + private partyStates: HTMLElement; + private partyMutedState: HTMLElement; + + private firstButtonsRow: HTMLElement; + private secondButtonsRow: HTMLElement; + + private declineI18nElement: I18n.IntlElement; + + private makeButton: (options: Parameters[2]) => HTMLElement; + private btnAccept: HTMLElement; + private btnDecline: HTMLElement; + private btnVideo: HTMLElement; + private btnScreen: HTMLElement; + private btnMute: HTMLElement; + private btnFullScreen: HTMLButtonElement; + private btnExitFullScreen: HTMLButtonElement; + + private movablePanel: MovablePanel; + private microphoneIcon: GroupCallMicrophoneIconMini; + private muteI18nElement: I18n.IntlElement; + + private videoContainers: { + input?: HTMLElement, + output?: HTMLElement + }; + + private controlsHover: ControlsHover; + + constructor(options: { + appAvatarsManager: AppAvatarsManager, + appPeersManager: AppPeersManager, + instance: CallInstance + }) { + super('popup-call', undefined, { + withoutOverlay: true, + closable: true + }); + + safeAssign(this, options); + + this.videoContainers = {}; + + const {container, listenerSetter, instance} = this; + container.classList.add(className, 'night'); + + const avatarContainer = document.createElement('div'); + avatarContainer.classList.add(className + '-avatar'); + + const peerId = this.peerId = this.instance.interlocutorUserId.toPeerId(); + const photo = this.appPeersManager.getPeerPhoto(peerId); + this.appAvatarsManager.putAvatar(avatarContainer, peerId, photo, 'photo_big'); + + const title = new PeerTitle({ + peerId + }).element; + + title.classList.add(className + '-title'); + + const subtitle = document.createElement('div'); + subtitle.classList.add(className + '-subtitle'); + + const description = this.description = new CallDescriptionElement(subtitle); + + const emojisSubtitle = this.emojisSubtitle = document.createElement('div'); + emojisSubtitle.classList.add(className + '-emojis'); + + container.append(avatarContainer, title, subtitle, emojisSubtitle); + + this.btnFullScreen = ButtonIcon('fullscreen'); + this.btnExitFullScreen = ButtonIcon('smallscreen hide'); + attachClickEvent(this.btnFullScreen, this.onFullScreenClick, {listenerSetter}); + attachClickEvent(this.btnExitFullScreen, () => cancelFullScreen(), {listenerSetter}); + addFullScreenListener(this.container, this.onFullScreenChange, listenerSetter); + this.header.prepend(this.btnExitFullScreen); + this.header.append(this.btnFullScreen); + + this.partyStates = document.createElement('div'); + this.partyStates.classList.add(className + '-party-states'); + + this.partyMutedState = document.createElement('div'); + this.partyMutedState.classList.add(className + '-party-state'); + const stateText = i18n('VoipUserMicrophoneIsOff', [new PeerTitle({peerId, onlyFirstName: true}).element]); + stateText.classList.add(className + '-party-state-text'); + const mutedIcon = new GroupCallMicrophoneIconMini(false, true); + mutedIcon.setState(false, false); + this.partyMutedState.append( + mutedIcon.container, + stateText + ); + + this.partyStates.append(this.partyMutedState); + this.container.append(this.partyStates); + + this.makeButton = makeButton.bind(null, className, this.listenerSetter); + this.constructFirstButtons(); + this.constructSecondButtons(); + + listenerSetter.add(instance)('state', () => { + this.updateInstance(); + }); + + listenerSetter.add(instance)('mediaState', () => { + this.updateInstance(); + }); + + this.movablePanel = new MovablePanel({ + listenerSetter, + movableOptions: { + minWidth: 400, + minHeight: 580, + element: this.element, + verifyTouchTarget: (e) => { + const target = e.target; + if(findUpClassName(target, 'call-button') || + findUpClassName(target, 'btn-icon') || + isFullScreen()) { + return false; + } + + return true; + } + }, + // onResize: () => this.toggleBigLayout(), + previousState + }); + + const controlsHover = this.controlsHover = new ControlsHover(); + controlsHover.setup({ + element: this.container, + listenerSetter: this.listenerSetter, + showOnLeaveToClassName: 'call-buttons' + }); + controlsHover.showControls(false); + + this.addEventListener('close', () => { + const {movablePanel} = this; + previousState = movablePanel.state; + + this.microphoneIcon.destroy(); + + movablePanel.destroy(); + }); + + this.updateInstance(); + } + + private constructFirstButtons() { + const buttons = this.firstButtonsRow = document.createElement('div'); + buttons.classList.add(className + '-buttons', 'is-first'); + + const toggleDisability = toggleClassName.bind(null, 'btn-disabled'); + + const btnVideo = this.btnVideo = this.makeButton({ + text: 'Call.Camera', + icon: 'videocamera_filled', + callback: () => { + const toggle = toggleDisability([btnVideo, btnScreen], true); + this.instance.toggleVideoSharing().finally(toggle); + } + }); + + const btnScreen = this.btnScreen = this.makeButton({ + text: 'Call.Screen', + icon: 'sharescreen_filled', + callback: () => { + const toggle = toggleDisability([btnVideo, btnScreen], true); + this.instance.toggleScreenSharing().finally(toggle); + } + }); + + if(!IS_SCREEN_SHARING_SUPPORTED) { + btnScreen.classList.add('hide'); + this.container.classList.add('no-screen'); + } + + this.muteI18nElement = new I18n.IntlElement({ + key: 'Call.Mute' + }); + const btnMute = this.btnMute = this.makeButton({ + text: this.muteI18nElement.element, + callback: () => { + this.instance.toggleMuted(); + } + }); + + const microphoneIcon = this.microphoneIcon = new GroupCallMicrophoneIconMini(true, true); + btnMute.firstElementChild.append(microphoneIcon.container); + + // btnVideo.classList.add('disabled'); + // btnScreen.classList.add('disabled'); + + buttons.append(btnVideo, btnScreen, btnMute); + this.container.append(buttons); + } + + private constructSecondButtons() { + const buttons = this.secondButtonsRow = document.createElement('div'); + buttons.classList.add(className + '-buttons', 'is-second'); + + this.declineI18nElement = new I18n.IntlElement({ + key: 'Call.Decline' + }); + const btnDecline = this.btnDecline = this.makeButton({ + text: this.declineI18nElement.element, + icon: 'endcall_filled', + callback: () => { + this.instance.hangUp('phoneCallDiscardReasonHangup'); + }, + isDanger: true + }); + + const btnAccept = this.btnAccept = this.makeButton({ + text: 'Call.Accept', + icon: 'phone', + callback: () => { + this.instance.acceptCall(); + }, + isConfirm: true, + }); + + buttons.append(btnDecline, btnAccept); + this.container.append(buttons); + } + + private onFullScreenClick = () => { + requestFullScreen(this.container); + }; + + private onFullScreenChange = () => { + const isFull = isFullScreen(); + + const {btnFullScreen, btnExitFullScreen} = this; + + const wasFullScreen = this.container.classList.contains('is-full-screen'); + this.container.classList.toggle('is-full-screen', isFull); + btnFullScreen && btnFullScreen.classList.toggle('hide', isFull); + btnExitFullScreen && btnExitFullScreen.classList.toggle('hide', !isFull); + this.btnClose.classList.toggle('hide', isFull); + + if(isFull !== wasFullScreen) { + animationIntersector.checkAnimations(isFull); + + rootScope.setThemeColor(isFull ? '#000000' : undefined); + } + }; + + private createVideoContainer(video: HTMLVideoElement) { + const _className = className + '-video'; + const container = document.createElement('div'); + container.classList.add(_className + '-container'); + + video.classList.add(_className); + if(video.paused) { + video.play(); + } + + attachClickEvent(container, () => { + if(!container.classList.contains('small')) { + return; + } + + const big = Object.values(this.videoContainers).find(container => !container.classList.contains('small')); + big.classList.add('small'); + big.style.cssText = container.style.cssText; + container.classList.remove('small'); + container.style.cssText = ''; + }); + + const canvas = callVideoCanvasBlur(video); + canvas.classList.add(_className + '-blur'); + + container.append(canvas, video); + + return container; + } + + private updateInstance() { + const {instance} = this; + const {connectionState} = instance; + if(connectionState === CALL_STATE.CLOSED) { + if(this.container.classList.contains('is-full-screen')) { + cancelFullScreen(); + } + + this.btnVideo.classList.add('disabled'); + + this.hide(); + return; + } + + const isPendingIncoming = !instance.isOutgoing && connectionState === CALL_STATE.PENDING; + this.declineI18nElement.compareAndUpdate({ + key: connectionState === CALL_STATE.PENDING ? 'Call.Decline' : 'Call.End' + }); + this.btnAccept.classList.toggle('disable', !isPendingIncoming); + this.btnAccept.classList.toggle('hide-me', !isPendingIncoming); + this.container.classList.toggle('two-button-rows', isPendingIncoming); + + const isMuted = instance.isMuted; + const onFrame = () => { + this.btnMute.firstElementChild.classList.toggle('active', isMuted); + }; + + const player = this.microphoneIcon.getItem().player; + this.microphoneIcon.setState(!isMuted, !isMuted, onFrame); + if(!player) { + onFrame(); + } + + this.muteI18nElement.compareAndUpdate({ + key: isMuted ? 'VoipUnmute' : 'Call.Mute' + }); + + const isSharingVideo = instance.isSharingVideo; + this.btnVideo.firstElementChild.classList.toggle('active', isSharingVideo); + + const isSharingScreen = instance.isSharingScreen; + this.btnScreen.firstElementChild.classList.toggle('active', isSharingScreen); + + const outputState = instance.getMediaState('output'); + + SetTransition(this.partyMutedState, 'is-visible', !!outputState?.muted, 300); + + const containers = this.videoContainers; + ['input' as const, 'output' as const].forEach(type => { + const mediaState = instance.getMediaState(type); + const video = instance.getVideoElement(type) as HTMLVideoElement; + const isActive = !!video && !!(mediaState && (mediaState.videoState === 'active' || mediaState.screencastState === 'active')); + let videoContainer = containers[type]; + + if(isActive && video && !videoContainer) { + videoContainer = containers[type] = this.createVideoContainer(video); + this.container.append(videoContainer); + } + + if(!isActive && videoContainer) { + videoContainer.remove(); + delete containers[type]; + } + }); + + const inputVideoContainer = containers.input; + if(inputVideoContainer) { + const isSmall = !!containers.output; + inputVideoContainer.classList.toggle('small', isSmall); + + const video = instance.getVideoElement('input') as HTMLVideoElement; + if(isSmall) { + const mediaSize = new MediaSize(120, 80); + const aspected = mediaSize.aspectFitted(new MediaSize(video.videoWidth, video.videoHeight)); + // inputVideoContainer.style.width = aspected.width + 'px'; + // inputVideoContainer.style.height = aspected.height + 'px'; + // const ratio = 120 / 80; + inputVideoContainer.style.width = '120px'; + inputVideoContainer.style.height = '80px'; + } else { + inputVideoContainer.style.cssText = ''; + } + } + + this.container.classList.toggle('no-video', !Object.keys(containers).length); + + if(!this.emojisSubtitle.textContent && connectionState < CALL_STATE.EXCHANGING_KEYS) { + Promise.resolve(instance.getEmojisFingerprint()).then(emojis => { + this.emojisSubtitle.innerHTML = RichTextProcessor.wrapEmojiText(emojis.join('')); + }); + } + + this.setDescription(); + } + + private setDescription() { + this.description.update(this.instance); + } +} diff --git a/src/components/chat/autocompleteHelper.ts b/src/components/chat/autocompleteHelper.ts index ae4d691df..e71aef39a 100644 --- a/src/components/chat/autocompleteHelper.ts +++ b/src/components/chat/autocompleteHelper.ts @@ -6,12 +6,12 @@ import attachListNavigation from "../../helpers/dom/attachListNavigation"; import EventListenerBase from "../../helpers/eventListenerBase"; -import { safeAssign } from "../../helpers/object"; import { IS_MOBILE } from "../../environment/userAgent"; import rootScope from "../../lib/rootScope"; import appNavigationController, { NavigationItem } from "../appNavigationController"; import SetTransition from "../singleTransition"; import AutocompleteHelperController from "./autocompleteHelperController"; +import safeAssign from "../../helpers/object/safeAssign"; export default class AutocompleteHelper extends EventListenerBase<{ hidden: () => void, diff --git a/src/components/chat/bubbleGroups.ts b/src/components/chat/bubbleGroups.ts index 0a7a22bc8..7f6738bbf 100644 --- a/src/components/chat/bubbleGroups.ts +++ b/src/components/chat/bubbleGroups.ts @@ -8,7 +8,7 @@ import rootScope from "../../lib/rootScope"; //import { generatePathData } from "../../helpers/dom"; import { MyMessage } from "../../lib/appManagers/appMessagesManager"; import type Chat from "./chat"; -import { indexOfAndSplice } from "../../helpers/array"; +import indexOfAndSplice from "../../helpers/array/indexOfAndSplice"; type Group = {bubble: HTMLElement, mid: number, timestamp: number}[]; type BubbleGroup = {timestamp: number, fromId: PeerId, mid: number, group: Group}; diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 8ada50e0c..be8b8dce9 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -18,7 +18,6 @@ import type { AppDraftsManager } from "../../lib/appManagers/appDraftsManager"; import type { AppMessagesIdsManager } from "../../lib/appManagers/appMessagesIdsManager"; import type Chat from "./chat"; import { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager"; -import { getObjectKeysAndSort } from "../../helpers/object"; import { IS_TOUCH_SUPPORTED } from "../../environment/touchSupport"; import { logger } from "../../lib/logger"; import rootScope from "../../lib/rootScope"; @@ -42,7 +41,7 @@ import LazyLoadQueue from "../lazyLoadQueue"; import ListenerSetter from "../../helpers/listenerSetter"; import PollElement from "../poll"; import AudioElement from "../audio"; -import { Message, MessageEntity, MessageMedia, MessageReplyHeader, Photo, PhotoSize, ReplyMarkup, Update, WebPage } from "../../layer"; +import { Message, MessageEntity, MessageMedia, MessageReplyHeader, Photo, PhotoSize, WebPage } from "../../layer"; import { NULL_PEER_ID, REPLIES_PEER_ID } from "../../lib/mtproto/mtproto_config"; import { FocusDirection } from "../../helpers/fastSmoothScroll"; import useHeavyAnimationCheck, { getHeavyAnimationPromise, dispatchHeavyAnimationEvent, interruptHeavyAnimation } from "../../hooks/useHeavyAnimationCheck"; @@ -53,7 +52,6 @@ import DEBUG from "../../config/debug"; import { SliceEnd } from "../../helpers/slicedArray"; import serverTimeManager from "../../lib/mtproto/serverTimeManager"; import PeerTitle from "../peerTitle"; -import { forEachReverse } from "../../helpers/array"; import findUpClassName from "../../helpers/dom/findUpClassName"; import findUpTag from "../../helpers/dom/findUpTag"; import { toast } from "../toast"; @@ -86,6 +84,8 @@ import IS_CALL_SUPPORTED from "../../environment/callSupport"; import Button from "../button"; import { CallType } from "../../lib/calls/types"; import getVisibleRect from "../../helpers/dom/getVisibleRect"; +import getObjectKeysAndSort from "../../helpers/object/getObjectKeysAndSort"; +import forEachReverse from "../../helpers/array/forEachReverse"; const USE_MEDIA_TAILS = false; const IGNORE_ACTIONS: Set = new Set([ diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index 9abf95cdb..43200d498 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -76,7 +76,6 @@ import PeerTitle from '../peerTitle'; import { fastRaf } from '../../helpers/schedulers'; import PopupDeleteMessages from '../popups/deleteMessages'; import fixSafariStickyInputFocusing, { IS_STICKY_INPUT_BUGGED } from '../../helpers/dom/fixSafariStickyInputFocusing'; -import { copy } from '../../helpers/object'; import PopupPeer from '../popups/peer'; import MEDIA_MIME_TYPES_SUPPORTED from '../../environment/mediaMimeTypesSupport'; import appMediaPlaybackController from '../appMediaPlaybackController'; @@ -86,6 +85,7 @@ import CheckboxField from '../checkboxField'; import DropdownHover from '../../helpers/dropdownHover'; import RadioForm from '../radioForm'; import findUpTag from '../../helpers/dom/findUpTag'; +import copy from '../../helpers/object/copy'; const RECORD_MIN_TIME = 500; const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.'; diff --git a/src/components/chat/pinnedContainer.ts b/src/components/chat/pinnedContainer.ts index 8964c1e63..63a50ac60 100644 --- a/src/components/chat/pinnedContainer.ts +++ b/src/components/chat/pinnedContainer.ts @@ -12,8 +12,8 @@ import { ripple } from "../ripple"; import ListenerSetter from "../../helpers/listenerSetter"; import { cancelEvent } from "../../helpers/dom/cancelEvent"; import { attachClickEvent } from "../../helpers/dom/clickEvent"; -import { safeAssign } from "../../helpers/object"; import { Message } from "../../layer"; +import safeAssign from "../../helpers/object/safeAssign"; const classNames: string[] = ['is-pinned-message-shown', 'is-pinned-audio-shown']; const CLASSNAME_BASE = 'pinned-container'; diff --git a/src/components/chat/replyKeyboard.ts b/src/components/chat/replyKeyboard.ts index a8ef1b36f..a97a13d80 100644 --- a/src/components/chat/replyKeyboard.ts +++ b/src/components/chat/replyKeyboard.ts @@ -10,13 +10,13 @@ import DropdownHover from "../../helpers/dropdownHover"; import { ReplyMarkup } from "../../layer"; import RichTextProcessor from "../../lib/richtextprocessor"; import rootScope from "../../lib/rootScope"; -import { safeAssign } from "../../helpers/object"; import ListenerSetter, { Listener } from "../../helpers/listenerSetter"; import findUpClassName from "../../helpers/dom/findUpClassName"; import { IS_TOUCH_SUPPORTED } from "../../environment/touchSupport"; import findUpAsChild from "../../helpers/dom/findUpAsChild"; import { cancelEvent } from "../../helpers/dom/cancelEvent"; import { getHeavyAnimationPromise } from "../../hooks/useHeavyAnimationCheck"; +import safeAssign from "../../helpers/object/safeAssign"; export default class ReplyKeyboard extends DropdownHover { private static BASE_CLASS = 'reply-keyboard'; diff --git a/src/components/chat/selection.ts b/src/components/chat/selection.ts index 8b8108ee5..37b5b371b 100644 --- a/src/components/chat/selection.ts +++ b/src/components/chat/selection.ts @@ -27,7 +27,6 @@ import { cancelEvent } from "../../helpers/dom/cancelEvent"; import cancelSelection from "../../helpers/dom/cancelSelection"; import getSelectedText from "../../helpers/dom/getSelectedText"; import rootScope from "../../lib/rootScope"; -import { safeAssign } from "../../helpers/object"; import { fastRaf } from "../../helpers/schedulers"; import replaceContent from "../../helpers/dom/replaceContent"; import AppSearchSuper from "../appSearchSuper."; @@ -36,6 +35,7 @@ import { randomLong } from "../../helpers/random"; import { attachContextMenuListener } from "../misc"; import { attachClickEvent, AttachClickOptions } from "../../helpers/dom/clickEvent"; import findUpAsChild from "../../helpers/dom/findUpAsChild"; +import safeAssign from "../../helpers/object/safeAssign"; const accumulateMapSet = (map: Map>) => { return [...map.values()].reduce((acc, v) => acc + v.size, 0); diff --git a/src/components/checkboxField.ts b/src/components/checkboxField.ts index 4e9f03857..49b836e7b 100644 --- a/src/components/checkboxField.ts +++ b/src/components/checkboxField.ts @@ -5,9 +5,9 @@ */ import appStateManager from "../lib/appManagers/appStateManager"; -import { getDeepProperty } from "../helpers/object"; import { ripple } from "./ripple"; import { LangPackKey, _i18n } from "../lib/langPack"; +import getDeepProperty from "../helpers/object/getDeepProperty"; export type CheckboxFieldOptions = { text?: LangPackKey, diff --git a/src/components/editPeer.ts b/src/components/editPeer.ts index 6972d206c..272a09233 100644 --- a/src/components/editPeer.ts +++ b/src/components/editPeer.ts @@ -9,8 +9,8 @@ import AvatarEdit from "./avatarEdit"; import AvatarElement from "./avatar"; import InputField from "./inputField"; import ListenerSetter from "../helpers/listenerSetter"; -import { safeAssign } from "../helpers/object"; import ButtonCorner from "./buttonCorner"; +import safeAssign from "../helpers/object/safeAssign"; export default class EditPeer { public nextBtn: HTMLButtonElement; diff --git a/src/components/groupCall/index.ts b/src/components/groupCall/index.ts index 981d65a0b..c8b11a78d 100644 --- a/src/components/groupCall/index.ts +++ b/src/components/groupCall/index.ts @@ -8,7 +8,6 @@ import PopupElement from "../popups"; import { hexToRgb } from "../../helpers/color"; import { attachClickEvent } from "../../helpers/dom/clickEvent"; import customProperties from "../../helpers/dom/customProperties"; -import { safeAssign } from "../../helpers/object"; import { GroupCall, GroupCallParticipant } from "../../layer"; import type { AppChatsManager } from "../../lib/appManagers/appChatsManager"; import type { AppGroupCallsManager } from "../../lib/appManagers/appGroupCallsManager"; @@ -28,13 +27,14 @@ import Scrollable from "../scrollable"; import { MovableState } from "../movableElement"; import animationIntersector from "../animationIntersector"; import { IS_APPLE_MOBILE } from "../../environment/userAgent"; -import toggleDisability from "../../helpers/dom/toggleDisability"; import throttle from "../../helpers/schedulers/throttle"; import IS_SCREEN_SHARING_SUPPORTED from "../../environment/screenSharingSupport"; import GroupCallInstance from "../../lib/calls/groupCallInstance"; import makeButton from "../call/button"; import MovablePanel from "../../helpers/movablePanel"; import findUpClassName from "../../helpers/dom/findUpClassName"; +import safeAssign from "../../helpers/object/safeAssign"; +import toggleClassName from "../../helpers/toggleClassName"; export enum GROUP_CALL_PARTICIPANT_MUTED_STATE { UNMUTED, @@ -345,15 +345,17 @@ export default class PopupGroupCall extends PopupElement { this.buttonsContainer.classList.toggle('show-controls', show); }; + private toggleDisability = toggleClassName.bind(null, 'btn-disabled'); + private onVideoClick = () => { - const toggle = toggleDisability([this.btnVideo], true); + const toggle = this.toggleDisability([this.btnVideo], true); this.instance.toggleVideoSharing().finally(() => { toggle(); }); }; private onScreenClick = () => { - const toggle = toggleDisability([this.btnScreen], true); + const toggle = this.toggleDisability([this.btnScreen], true); this.instance.toggleScreenSharing().finally(() => { toggle(); }); diff --git a/src/components/groupCall/participantVideos.ts b/src/components/groupCall/participantVideos.ts index cee06ea21..52bb66411 100644 --- a/src/components/groupCall/participantVideos.ts +++ b/src/components/groupCall/participantVideos.ts @@ -8,7 +8,7 @@ import { attachClickEvent } from "../../helpers/dom/clickEvent"; import ControlsHover from "../../helpers/dom/controlsHover"; import findUpClassName from "../../helpers/dom/findUpClassName"; import ListenerSetter from "../../helpers/listenerSetter"; -import { safeAssign } from "../../helpers/object"; +import safeAssign from "../../helpers/object/safeAssign"; import { GroupCallParticipant } from "../../layer"; import { AppGroupCallsManager, GroupCallOutputSource } from "../../lib/appManagers/appGroupCallsManager"; import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; diff --git a/src/components/groupCall/participants.ts b/src/components/groupCall/participants.ts index 1115a4262..f092ef770 100644 --- a/src/components/groupCall/participants.ts +++ b/src/components/groupCall/participants.ts @@ -10,7 +10,7 @@ import findUpClassName from "../../helpers/dom/findUpClassName"; import { addFullScreenListener, isFullScreen } from "../../helpers/dom/fullScreen"; import ListenerSetter from "../../helpers/listenerSetter"; import noop from "../../helpers/noop"; -import { safeAssign } from "../../helpers/object"; +import safeAssign from "../../helpers/object/safeAssign"; import ScrollableLoader from "../../helpers/scrollableLoader"; import { GroupCallParticipant } from "../../layer"; import type { AppChatsManager } from "../../lib/appManagers/appChatsManager"; diff --git a/src/components/groupedLayout.ts b/src/components/groupedLayout.ts index 96147593e..e7c848a8e 100644 --- a/src/components/groupedLayout.ts +++ b/src/components/groupedLayout.ts @@ -5,7 +5,7 @@ For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ -import { accumulate } from "../helpers/array"; +import accumulate from "../helpers/array/accumulate"; import { clamp } from "../helpers/number"; type Size = {w: number, h: number}; diff --git a/src/components/lazyLoadQueue.ts b/src/components/lazyLoadQueue.ts index 263042bf9..a3b124f6e 100644 --- a/src/components/lazyLoadQueue.ts +++ b/src/components/lazyLoadQueue.ts @@ -6,8 +6,9 @@ import { logger, LogTypes } from "../lib/logger"; import VisibilityIntersector, { OnVisibilityChange } from "./visibilityIntersector"; -import { findAndSpliceAll, indexOfAndSplice } from "../helpers/array"; import throttle from "../helpers/schedulers/throttle"; +import findAndSpliceAll from "../helpers/array/findAndSpliceAll"; +import indexOfAndSplice from "../helpers/array/indexOfAndSplice"; type LazyLoadElementBase = { load: () => Promise diff --git a/src/components/movableElement.ts b/src/components/movableElement.ts index 038b9939f..7dfc46954 100644 --- a/src/components/movableElement.ts +++ b/src/components/movableElement.ts @@ -5,11 +5,10 @@ */ import findUpClassName from "../helpers/dom/findUpClassName"; -import { isFullScreen } from "../helpers/dom/fullScreen"; import EventListenerBase from "../helpers/eventListenerBase"; import mediaSizes from "../helpers/mediaSizes"; import { clamp } from "../helpers/number"; -import { safeAssign } from "../helpers/object"; +import safeAssign from "../helpers/object/safeAssign"; import windowSize from "../helpers/windowSize"; import SwipeHandler from "./swipeHandler"; diff --git a/src/components/popups/index.ts b/src/components/popups/index.ts index 09477cf8f..9d725c770 100644 --- a/src/components/popups/index.ts +++ b/src/components/popups/index.ts @@ -16,8 +16,8 @@ import { attachClickEvent, simulateClickEvent } from "../../helpers/dom/clickEve import isSendShortcutPressed from "../../helpers/dom/isSendShortcutPressed"; import { cancelEvent } from "../../helpers/dom/cancelEvent"; import EventListenerBase from "../../helpers/eventListenerBase"; -import { indexOfAndSplice } from "../../helpers/array"; -import { addFullScreenListener, getFullScreenElement, isFullScreen } from "../../helpers/dom/fullScreen"; +import { addFullScreenListener, getFullScreenElement } from "../../helpers/dom/fullScreen"; +import indexOfAndSplice from "../../helpers/array/indexOfAndSplice"; export type PopupButton = { text?: string, diff --git a/src/components/preloader.ts b/src/components/preloader.ts index 7a48b55ab..cff973d80 100644 --- a/src/components/preloader.ts +++ b/src/components/preloader.ts @@ -7,10 +7,10 @@ import { CancellablePromise } from "../helpers/cancellablePromise"; import SetTransition from "./singleTransition"; import { fastRaf } from "../helpers/schedulers"; -import { safeAssign } from "../helpers/object"; import { cancelEvent } from "../helpers/dom/cancelEvent"; import { attachClickEvent } from "../helpers/dom/clickEvent"; import isInDOM from "../helpers/dom/isInDOM"; +import safeAssign from "../helpers/object/safeAssign"; const TRANSITION_TIME = 200; diff --git a/src/components/radioField.ts b/src/components/radioField.ts index 1121f2d19..5c75f3a38 100644 --- a/src/components/radioField.ts +++ b/src/components/radioField.ts @@ -4,8 +4,8 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +import getDeepProperty from "../helpers/object/getDeepProperty"; import appStateManager from "../lib/appManagers/appStateManager"; -import { getDeepProperty } from "../helpers/object"; import { LangPackKey, _i18n } from "../lib/langPack"; export default class RadioField { diff --git a/src/components/rangeSelector.ts b/src/components/rangeSelector.ts index 93faa0343..ef869c00f 100644 --- a/src/components/rangeSelector.ts +++ b/src/components/rangeSelector.ts @@ -6,7 +6,7 @@ import { clamp } from "../helpers/number"; import attachGrabListeners, { GrabEvent } from "../helpers/dom/attachGrabListeners"; -import { safeAssign } from "../helpers/object"; +import safeAssign from "../helpers/object/safeAssign"; export default class RangeSelector { public container: HTMLDivElement; diff --git a/src/components/sidebarLeft/index.ts b/src/components/sidebarLeft/index.ts index 781fc9cb8..789ba21ce 100644 --- a/src/components/sidebarLeft/index.ts +++ b/src/components/sidebarLeft/index.ts @@ -39,9 +39,9 @@ import replaceContent from "../../helpers/dom/replaceContent"; import sessionStorage from "../../lib/sessionStorage"; import { CLICK_EVENT_NAME } from "../../helpers/dom/clickEvent"; import { closeBtnMenu } from "../misc"; -import { indexOfAndSplice } from "../../helpers/array"; import ButtonIcon from "../buttonIcon"; import confirmationPopup from "../confirmationPopup"; +import indexOfAndSplice from "../../helpers/array/indexOfAndSplice"; export const LEFT_COLUMN_ACTIVE_CLASSNAME = 'is-left-column-shown'; diff --git a/src/components/sidebarLeft/tabs/addMembers.ts b/src/components/sidebarLeft/tabs/addMembers.ts index e6b695b52..f2a353ea4 100644 --- a/src/components/sidebarLeft/tabs/addMembers.ts +++ b/src/components/sidebarLeft/tabs/addMembers.ts @@ -6,7 +6,7 @@ import { SliderSuperTab } from "../../slider"; import AppSelectPeers from "../../appSelectPeers"; -import { putPreloader, setButtonLoader } from "../../misc"; +import { setButtonLoader } from "../../misc"; import { LangPackKey, _i18n } from "../../../lib/langPack"; import ButtonCorner from "../../buttonCorner"; diff --git a/src/components/sidebarLeft/tabs/background.ts b/src/components/sidebarLeft/tabs/background.ts index 7a97863e8..ae5f10cff 100644 --- a/src/components/sidebarLeft/tabs/background.ts +++ b/src/components/sidebarLeft/tabs/background.ts @@ -12,7 +12,7 @@ import { attachClickEvent } from "../../../helpers/dom/clickEvent"; import findUpClassName from "../../../helpers/dom/findUpClassName"; import { requestFile } from "../../../helpers/files"; import highlightningColor from "../../../helpers/highlightningColor"; -import { copy } from "../../../helpers/object"; +import copy from "../../../helpers/object/copy"; import sequentialDom from "../../../helpers/sequentialDom"; import { AccountWallPapers, PhotoSize, WallPaper } from "../../../layer"; import appDocsManager, { MyDocument } from "../../../lib/appManagers/appDocsManager"; diff --git a/src/components/sidebarLeft/tabs/editFolder.ts b/src/components/sidebarLeft/tabs/editFolder.ts index cd2576e27..930d427b8 100644 --- a/src/components/sidebarLeft/tabs/editFolder.ts +++ b/src/components/sidebarLeft/tabs/editFolder.ts @@ -4,7 +4,6 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import { deepEqual, copy } from "../../../helpers/object"; import appDialogsManager from "../../../lib/appManagers/appDialogsManager"; import { MyDialogFilter as DialogFilter } from "../../../lib/storages/filters"; import lottieLoader, { LottieLoader } from "../../../lib/rlottie/lottieLoader"; @@ -22,6 +21,8 @@ import { i18n, i18n_, LangPackKey } from "../../../lib/langPack"; import { SettingSection } from ".."; import PopupPeer from "../../popups/peer"; import RLottiePlayer from "../../../lib/rlottie/rlottiePlayer"; +import copy from "../../../helpers/object/copy"; +import deepEqual from "../../../helpers/object/deepEqual"; const MAX_FOLDER_NAME_LENGTH = 12; diff --git a/src/components/sidebarLeft/tabs/includedChats.ts b/src/components/sidebarLeft/tabs/includedChats.ts index bf7751674..6aad10c1b 100644 --- a/src/components/sidebarLeft/tabs/includedChats.ts +++ b/src/components/sidebarLeft/tabs/includedChats.ts @@ -9,7 +9,6 @@ import AppSelectPeers from "../../appSelectPeers"; import appDialogsManager from "../../../lib/appManagers/appDialogsManager"; import appUsersManager from "../../../lib/appManagers/appUsersManager"; import { MyDialogFilter as DialogFilter } from "../../../lib/storages/filters"; -import { copy } from "../../../helpers/object"; import ButtonIcon from "../../buttonIcon"; import CheckboxField from "../../checkboxField"; import Button from "../../button"; @@ -19,8 +18,9 @@ import appMessagesManager from "../../../lib/appManagers/appMessagesManager"; import RichTextProcessor from "../../../lib/richtextprocessor"; import { SettingSection } from ".."; import { toast } from "../../toast"; -import { forEachReverse } from "../../../helpers/array"; import appPeersManager from "../../../lib/appManagers/appPeersManager"; +import copy from "../../../helpers/object/copy"; +import forEachReverse from "../../../helpers/array/forEachReverse"; export default class AppIncludedChatsTab extends SliderSuperTab { private editFolderTab: AppEditFolderTab; diff --git a/src/components/sidebarLeft/tabs/notifications.ts b/src/components/sidebarLeft/tabs/notifications.ts index 05eb9de6d..fff26603a 100644 --- a/src/components/sidebarLeft/tabs/notifications.ts +++ b/src/components/sidebarLeft/tabs/notifications.ts @@ -10,11 +10,11 @@ import CheckboxField from "../../checkboxField"; import { InputNotifyPeer, Update } from "../../../layer"; import appNotificationsManager from "../../../lib/appManagers/appNotificationsManager"; import { SliderSuperTabEventable } from "../../sliderTab"; -import { copy } from "../../../helpers/object"; import rootScope from "../../../lib/rootScope"; import { convertKeyToInputKey } from "../../../helpers/string"; import { LangPackKey } from "../../../lib/langPack"; import appStateManager from "../../../lib/appManagers/appStateManager"; +import copy from "../../../helpers/object/copy"; type InputNotifyKey = Exclude; diff --git a/src/components/sidebarRight/tabs/stickers.ts b/src/components/sidebarRight/tabs/stickers.ts index 54508abf4..26e41ffbd 100644 --- a/src/components/sidebarRight/tabs/stickers.ts +++ b/src/components/sidebarRight/tabs/stickers.ts @@ -15,10 +15,10 @@ import { RichTextProcessor } from "../../../lib/richtextprocessor"; import { wrapSticker } from "../../wrappers"; import appSidebarRight from ".."; import { StickerSet, StickerSetCovered } from "../../../layer"; -import { forEachReverse } from "../../../helpers/array"; import { i18n } from "../../../lib/langPack"; import findUpClassName from "../../../helpers/dom/findUpClassName"; import { attachClickEvent } from "../../../helpers/dom/clickEvent"; +import forEachReverse from "../../../helpers/array/forEachReverse"; export default class AppStickersTab extends SliderSuperTab { private inputSearch: InputSearch; diff --git a/src/components/sidebarRight/tabs/userPermissions.ts b/src/components/sidebarRight/tabs/userPermissions.ts index 1187cf261..e1103ac97 100644 --- a/src/components/sidebarRight/tabs/userPermissions.ts +++ b/src/components/sidebarRight/tabs/userPermissions.ts @@ -6,7 +6,7 @@ import { attachClickEvent } from "../../../helpers/dom/clickEvent"; import toggleDisability from "../../../helpers/dom/toggleDisability"; -import { deepEqual } from "../../../helpers/object"; +import deepEqual from "../../../helpers/object/deepEqual"; import { ChannelParticipant } from "../../../layer"; import appChatsManager from "../../../lib/appManagers/appChatsManager"; import appDialogsManager from "../../../lib/appManagers/appDialogsManager"; diff --git a/src/components/slider.ts b/src/components/slider.ts index d735903ca..6a4046822 100644 --- a/src/components/slider.ts +++ b/src/components/slider.ts @@ -8,9 +8,9 @@ import { horizontalMenu } from "./horizontalMenu"; import { TransitionSlider } from "./transition"; import appNavigationController, { NavigationItem } from "./appNavigationController"; import SliderSuperTab, { SliderSuperTabConstructable, SliderTab } from "./sliderTab"; -import { safeAssign } from "../helpers/object"; import { attachClickEvent } from "../helpers/dom/clickEvent"; -import { indexOfAndSplice } from "../helpers/array"; +import indexOfAndSplice from "../helpers/array/indexOfAndSplice"; +import safeAssign from "../helpers/object/safeAssign"; const TRANSITION_TIME = 250; diff --git a/src/components/sortedUserList.ts b/src/components/sortedUserList.ts index fd1316fb1..6cc6f0af0 100644 --- a/src/components/sortedUserList.ts +++ b/src/components/sortedUserList.ts @@ -11,9 +11,9 @@ import appUsersManager from "../lib/appManagers/appUsersManager"; import isInDOM from "../helpers/dom/isInDOM"; import positionElementByIndex from "../helpers/dom/positionElementByIndex"; import replaceContent from "../helpers/dom/replaceContent"; -import { safeAssign } from "../helpers/object"; import { fastRaf } from "../helpers/schedulers"; import SortedList, { SortedElementBase } from "../helpers/sortedList"; +import safeAssign from "../helpers/object/safeAssign"; interface SortedUser extends SortedElementBase { dom: DialogDom diff --git a/src/components/superIcon.ts b/src/components/superIcon.ts index 743f5dec4..6bd5f443b 100644 --- a/src/components/superIcon.ts +++ b/src/components/superIcon.ts @@ -5,7 +5,7 @@ */ import noop from "../helpers/noop"; -import { safeAssign } from "../helpers/object"; +import safeAssign from "../helpers/object/safeAssign"; import { LottieAssetName } from "../lib/rlottie/lottieLoader"; import RLottieIcon, { RLottieIconItemPartOptions, RLottieIconItemPart } from "../lib/rlottie/rlottieIcon"; import { RLottieColor } from "../lib/rlottie/rlottiePlayer"; diff --git a/src/components/swipeHandler.ts b/src/components/swipeHandler.ts index ea59c1fb5..e98389a63 100644 --- a/src/components/swipeHandler.ts +++ b/src/components/swipeHandler.ts @@ -5,9 +5,9 @@ */ import { cancelEvent } from "../helpers/dom/cancelEvent"; -import { safeAssign } from "../helpers/object"; import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport"; import rootScope from "../lib/rootScope"; +import safeAssign from "../helpers/object/safeAssign"; const getEvent = (e: TouchEvent | MouseEvent) => { return (e as TouchEvent).touches ? (e as TouchEvent).touches[0] : e as MouseEvent; diff --git a/src/components/topbarCall.ts b/src/components/topbarCall.ts index 010581ca0..75c3c4acd 100644 --- a/src/components/topbarCall.ts +++ b/src/components/topbarCall.ts @@ -25,9 +25,10 @@ import CALL_STATE from "../lib/calls/callState"; import replaceContent from "../helpers/dom/replaceContent"; import PeerTitle from "./peerTitle"; import CallDescriptionElement from "./call/description"; -// import PopupCall from "./call"; +import PopupCall from "./call"; import type { AppAvatarsManager } from "../lib/appManagers/appAvatarsManager"; import GroupCallMicrophoneIconMini from "./groupCall/microphoneIconMini"; +import CallInstance from "../lib/calls/callInstance"; function convertCallStateToGroupState(state: CALL_STATE, isMuted: boolean) { switch(state) { @@ -255,13 +256,13 @@ export default class TopbarCall { appPeersManager: this.appPeersManager, appChatsManager: this.appChatsManager }).show(); - }/* else if(this.instance instanceof CallInstance) { + } else if(this.instance instanceof CallInstance) { new PopupCall({ appAvatarsManager: this.appAvatarsManager, appPeersManager: this.appPeersManager, instance: this.instance }).show(); - } */ + } }, {listenerSetter}); container.append(left, center, right); diff --git a/src/environment/callSupport.ts b/src/environment/callSupport.ts index 636e0c895..37a28c6d6 100644 --- a/src/environment/callSupport.ts +++ b/src/environment/callSupport.ts @@ -1,5 +1,5 @@ import IS_WEBRTC_SUPPORTED from "./webrtcSupport"; -const IS_CALL_SUPPORTED = IS_WEBRTC_SUPPORTED && false; +const IS_CALL_SUPPORTED = IS_WEBRTC_SUPPORTED; export default IS_CALL_SUPPORTED; diff --git a/src/helpers/array.ts b/src/helpers/array.ts index b80f35257..7727a8db1 100644 --- a/src/helpers/array.ts +++ b/src/helpers/array.ts @@ -19,70 +19,16 @@ export function listMergeSorted(list1: any[] = [], list2: any[] = []) { return result; } */ -export const accumulate = (arr: number[], initialValue: number) => arr.reduce((acc, value) => acc + value, initialValue); - -export function indexOfAndSplice(array: Array, item: T) { - const idx = array.indexOf(item); - const spliced = idx !== -1 && array.splice(idx, 1); - return spliced && spliced[0]; -} - -export function findAndSpliceAll(array: Array, verify: (value: T, index: number, arr: typeof array) => boolean) { - const out: typeof array = []; - let idx = -1; - while((idx = array.findIndex(verify)) !== -1) { - out.push(array.splice(idx, 1)[0]); - } - return out; -} +export {}; + + + + + + -export function forEachReverse(array: Array, callback: (value: T, index?: number, array?: Array) => void) { - for(let length = array.length, i = length - 1; i >= 0; --i) { - callback(array[i], i, array); - } -}; - -export function insertInDescendSortedArray(array: Array, element: T, property: K, pos?: number) { - const sortProperty: number = element[property]; - - if(pos === undefined) { - pos = array.indexOf(element); - if(pos !== -1) { - const prev = array[pos - 1]; - const next = array[pos + 1]; - if((!prev || prev[property] >= sortProperty) && (!next || next[property] <= sortProperty)) { - // console.warn('same pos', pos, sortProperty, prev, next); - return pos; - } - - array.splice(pos, 1); - } - } - const len = array.length; - if(!len || sortProperty <= array[len - 1][property]) { - return array.push(element) - 1; - } else if(sortProperty >= array[0][property]) { - array.unshift(element); - return 0; - } else { - for(let i = 0; i < len; i++) { - if(sortProperty > array[i][property]) { - array.splice(i, 0, element); - return i; - } - } - } - console.error('wtf', array, element); - return array.indexOf(element); -} -export function filterUnique>(arr: T): T { - return [...new Set(arr)] as T; -} -export function flatten(arr: T[][]): T[] { - return arr.reduce((acc, val) => (acc.push(...val), acc), []); -} diff --git a/src/helpers/array/accumulate.ts b/src/helpers/array/accumulate.ts new file mode 100644 index 000000000..5884d4d32 --- /dev/null +++ b/src/helpers/array/accumulate.ts @@ -0,0 +1,3 @@ +export default function accumulate(arr: number[], initialValue: number) { + return arr.reduce((acc, value) => acc + value, initialValue); +} diff --git a/src/helpers/array/filterUnique.ts b/src/helpers/array/filterUnique.ts new file mode 100644 index 000000000..d359bd13f --- /dev/null +++ b/src/helpers/array/filterUnique.ts @@ -0,0 +1,3 @@ +export default function filterUnique>(arr: T): T { + return [...new Set(arr)] as T; +} diff --git a/src/helpers/array/findAndSpliceAll.ts b/src/helpers/array/findAndSpliceAll.ts new file mode 100644 index 000000000..136116781 --- /dev/null +++ b/src/helpers/array/findAndSpliceAll.ts @@ -0,0 +1,9 @@ +export default function findAndSpliceAll(array: Array, verify: (value: T, index: number, arr: typeof array) => boolean) { + const out: typeof array = []; + let idx = -1; + while((idx = array.findIndex(verify)) !== -1) { + out.push(array.splice(idx, 1)[0]); + } + + return out; +} diff --git a/src/helpers/array/flatten.ts b/src/helpers/array/flatten.ts new file mode 100644 index 000000000..d9f1a8929 --- /dev/null +++ b/src/helpers/array/flatten.ts @@ -0,0 +1,3 @@ +export default function flatten(arr: T[][]): T[] { + return arr.reduce((acc, val) => (acc.push(...val), acc), []); +} diff --git a/src/helpers/array/forEachReverse.ts b/src/helpers/array/forEachReverse.ts new file mode 100644 index 000000000..46381b153 --- /dev/null +++ b/src/helpers/array/forEachReverse.ts @@ -0,0 +1,5 @@ +export default function forEachReverse(array: Array, callback: (value: T, index?: number, array?: Array) => void) { + for(let length = array.length, i = length - 1; i >= 0; --i) { + callback(array[i], i, array); + } +}; diff --git a/src/helpers/array/indexOfAndSplice.ts b/src/helpers/array/indexOfAndSplice.ts new file mode 100644 index 000000000..c0c0aa4b0 --- /dev/null +++ b/src/helpers/array/indexOfAndSplice.ts @@ -0,0 +1,5 @@ +export default function indexOfAndSplice(array: Array, item: T) { + const idx = array.indexOf(item); + const spliced = idx !== -1 && array.splice(idx, 1); + return spliced && spliced[0]; +} diff --git a/src/helpers/array/insertInDescendSortedArray.ts b/src/helpers/array/insertInDescendSortedArray.ts new file mode 100644 index 000000000..da4720298 --- /dev/null +++ b/src/helpers/array/insertInDescendSortedArray.ts @@ -0,0 +1,35 @@ +export default function insertInDescendSortedArray(array: Array, element: T, property: K, pos?: number) { + const sortProperty: number = element[property]; + + if(pos === undefined) { + pos = array.indexOf(element); + if(pos !== -1) { + const prev = array[pos - 1]; + const next = array[pos + 1]; + if((!prev || prev[property] >= sortProperty) && (!next || next[property] <= sortProperty)) { + // console.warn('same pos', pos, sortProperty, prev, next); + return pos; + } + + array.splice(pos, 1); + } + } + + const len = array.length; + if(!len || sortProperty <= array[len - 1][property]) { + return array.push(element) - 1; + } else if(sortProperty >= array[0][property]) { + array.unshift(element); + return 0; + } else { + for(let i = 0; i < len; i++) { + if(sortProperty > array[i][property]) { + array.splice(i, 0, element); + return i; + } + } + } + + console.error('wtf', array, element); + return array.indexOf(element); +} diff --git a/src/helpers/audioAssetPlayer.ts b/src/helpers/audioAssetPlayer.ts index 37d16ec6e..19ca03ae3 100644 --- a/src/helpers/audioAssetPlayer.ts +++ b/src/helpers/audioAssetPlayer.ts @@ -9,6 +9,7 @@ const ASSETS_PATH = 'assets/audio/'; export default class AudioAssetPlayer { private audio: HTMLAudioElement; private tempId: number; + private assetName: AssetName; constructor(private assets: AssetName[]) { this.tempId = 0; @@ -16,7 +17,8 @@ export default class AudioAssetPlayer { public playSound(name: AssetName, loop = false) { ++this.tempId; - + this.assetName = name; + try { const audio = this.createAudio(); audio.src = ASSETS_PATH + name; @@ -27,6 +29,12 @@ export default class AudioAssetPlayer { } } + public playSoundIfDifferent(name: AssetName, loop?: boolean) { + if(this.assetName !== name) { + this.playSound(name, loop); + } + } + public createAudio() { let {audio} = this; if(audio) { @@ -39,6 +47,10 @@ export default class AudioAssetPlayer { } public stopSound() { + if(!this.audio) { + return; + } + this.audio.pause(); } diff --git a/src/helpers/bigInt/bigIntConversion.ts b/src/helpers/bigInt/bigIntConversion.ts new file mode 100644 index 000000000..088e7359c --- /dev/null +++ b/src/helpers/bigInt/bigIntConversion.ts @@ -0,0 +1,9 @@ +import bigInt from 'big-integer'; + +export function bigIntFromBytes(bytes: Uint8Array | number[], base = 256) { + return bigInt.fromArray(bytes instanceof Uint8Array ? [...bytes] : bytes, base); +} + +export function bigIntToBytes(bigInt: bigInt.BigInteger) { + return new Uint8Array(bigInt.toArray(256).value); +} diff --git a/src/helpers/bigInt/bigIntRandom.ts b/src/helpers/bigInt/bigIntRandom.ts new file mode 100644 index 000000000..1fb639350 --- /dev/null +++ b/src/helpers/bigInt/bigIntRandom.ts @@ -0,0 +1,13 @@ +import bigInt from "big-integer"; +import { nextRandomUint } from "../random"; + +export default function bigIntRandom(min: bigInt.BigNumber, max: bigInt.BigNumber) { + return bigInt.randBetween(min, max, () => { + return nextRandomUint(32) / 0xFFFFFFFF; + /* const bits = 32; + const randomBytes = new Uint8Array(bits / 8); + crypto.getRandomValues(randomBytes); + const r = bigIntFromBytes(randomBytes).mod(bigInt(2).pow(bits)); + return r.toJSNumber(); */ + }); +} diff --git a/src/helpers/bytes.ts b/src/helpers/bytes.ts index 93ad2a304..04acafdbd 100644 --- a/src/helpers/bytes.ts +++ b/src/helpers/bytes.ts @@ -9,91 +9,7 @@ * https://github.com/zhukov/webogram/blob/master/LICENSE */ -export function bytesToHex(bytes: ArrayLike) { - const length = bytes.length; - const arr: string[] = new Array(length); - for(let i = 0; i < length; ++i) { - arr[i] = (bytes[i] < 16 ? '0' : '') + (bytes[i] || 0).toString(16); - } - return arr.join(''); -} - -export function bytesFromHex(hexString: string) { - const len = hexString.length; - const bytes = new Uint8Array(Math.ceil(len / 2)); - let start = 0; - - if(len % 2) { // read 0x581 as 0x0581 - bytes[start++] = parseInt(hexString.charAt(0), 16); - } - - for(let i = start; i < len; i += 2) { - bytes[start++] = parseInt(hexString.substr(i, 2), 16); - } - - return bytes; -} - -export function bytesToBase64(bytes: number[] | Uint8Array) { - let mod3: number; - let result = ''; - - for(let nLen = bytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; ++nIdx) { - mod3 = nIdx % 3; - nUint24 |= bytes[nIdx] << (16 >>> mod3 & 24); - if(mod3 === 2 || nLen - nIdx === 1) { - result += String.fromCharCode( - uint6ToBase64(nUint24 >>> 18 & 63), - uint6ToBase64(nUint24 >>> 12 & 63), - uint6ToBase64(nUint24 >>> 6 & 63), - uint6ToBase64(nUint24 & 63) - ); - nUint24 = 0; - } - } - - return result.replace(/A(?=A$|$)/g, '='); -} - -export function uint6ToBase64(nUint6: number) { - return nUint6 < 26 - ? nUint6 + 65 - : nUint6 < 52 - ? nUint6 + 71 - : nUint6 < 62 - ? nUint6 - 4 - : nUint6 === 62 - ? 43 - : nUint6 === 63 - ? 47 - : 65; -} - -export function bytesCmp(bytes1: number[] | Uint8Array, bytes2: number[] | Uint8Array) { - const len = bytes1.length; - if(len !== bytes2.length) { - return false; - } - - for(let i = 0; i < len; ++i) { - if(bytes1[i] !== bytes2[i]) { - return false; - } - } - - return true; -} - -export function bytesXor(bytes1: Uint8Array, bytes2: Uint8Array) { - const len = bytes1.length; - const bytes = new Uint8Array(len); - - for(let i = 0; i < len; ++i) { - bytes[i] = bytes1[i] ^ bytes2[i]; - } - - return bytes; -} +export {}; /* export function bytesToArrayBuffer(b: number[]) { return (new Uint8Array(b)).buffer; @@ -111,16 +27,6 @@ export function convertToArrayBuffer(bytes: any | ArrayBuffer | Uint8Array) { return bytesToArrayBuffer(bytes); } */ -export function convertToUint8Array(bytes: Uint8Array | ArrayBuffer | number[] | string): Uint8Array { - if(bytes instanceof Uint8Array) { - return bytes; - } else if(typeof(bytes) === 'string') { - return new TextEncoder().encode(bytes); - } - - return new Uint8Array(bytes); -} - /* export function bytesFromArrayBuffer(buffer: ArrayBuffer) { const len = buffer.byteLength; const byteView = new Uint8Array(buffer); @@ -143,40 +49,6 @@ export function bufferConcat(buffer1: any, buffer2: any) { return tmp.buffer; } */ -export function bufferConcats(...args: (ArrayBuffer | Uint8Array | number[])[]) { - const length = args.reduce((acc, v) => acc + ((v as ArrayBuffer).byteLength || (v as Uint8Array).length), 0); - - const tmp = new Uint8Array(length); - - let lastLength = 0; - args.forEach(b => { - tmp.set(b instanceof ArrayBuffer ? new Uint8Array(b) : b, lastLength); - lastLength += (b as ArrayBuffer).byteLength || (b as Uint8Array).length; - }); - - return tmp/* .buffer */; -} - -export function bytesFromWordss(input: Uint32Array) { - const o = new Uint8Array(input.byteLength); - for(let i = 0, length = input.length * 4; i < length; ++i) { - o[i] = ((input[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff); - } - - return o; -} - -export function bytesToWordss(input: Parameters[0]) { - const bytes = convertToUint8Array(input); - - const words: number[] = []; - for(let i = 0, len = bytes.length; i < len; ++i) { - words[i >>> 2] |= bytes[i] << (24 - (i % 4) * 8); - } - - return new Uint32Array(words); -} - // * https://stackoverflow.com/a/52827031 /* export const isBigEndian = (() => { const array = new Uint8Array(4); diff --git a/src/helpers/bytes/addPadding.ts b/src/helpers/bytes/addPadding.ts new file mode 100644 index 000000000..db2b322e1 --- /dev/null +++ b/src/helpers/bytes/addPadding.ts @@ -0,0 +1,34 @@ +import bufferConcats from "./bufferConcats"; + +export default function addPadding( + bytes: T, + blockSize: number = 16, + zeroes?: boolean, + blockSizeAsTotalLength = false, + prepend = false +): T { + const len = (bytes as ArrayBuffer).byteLength || (bytes as Uint8Array).length; + const needPadding = blockSizeAsTotalLength ? blockSize - len : blockSize - (len % blockSize); + if(needPadding > 0 && needPadding < blockSize) { + ////console.log('addPadding()', len, blockSize, needPadding); + const padding = new Uint8Array(needPadding); + if(zeroes) { + for(let i = 0; i < needPadding; ++i) { + padding[i] = 0; + } + } else { + padding.randomize(); + } + + if(bytes instanceof ArrayBuffer) { + return (prepend ? bufferConcats(padding, bytes) : bufferConcats(bytes, padding)).buffer as T; + } else if(bytes instanceof Uint8Array) { + return (prepend ? bufferConcats(padding, bytes) : bufferConcats(bytes, padding)) as T; + } else { + // @ts-ignore + return (prepend ? [...padding].concat(bytes) : bytes.concat([...padding])) as T; + } + } + + return bytes; +} diff --git a/src/helpers/bytes/bufferConcats.ts b/src/helpers/bytes/bufferConcats.ts new file mode 100644 index 000000000..35d66d326 --- /dev/null +++ b/src/helpers/bytes/bufferConcats.ts @@ -0,0 +1,13 @@ +export default function bufferConcats(...args: (ArrayBuffer | Uint8Array | number[])[]) { + const length = args.reduce((acc, v) => acc + ((v as ArrayBuffer).byteLength || (v as Uint8Array).length), 0); + + const tmp = new Uint8Array(length); + + let lastLength = 0; + args.forEach(b => { + tmp.set(b instanceof ArrayBuffer ? new Uint8Array(b) : b, lastLength); + lastLength += (b as ArrayBuffer).byteLength || (b as Uint8Array).length; + }); + + return tmp/* .buffer */; +} diff --git a/src/helpers/bytes/bytesCmp.ts b/src/helpers/bytes/bytesCmp.ts new file mode 100644 index 000000000..ac4954482 --- /dev/null +++ b/src/helpers/bytes/bytesCmp.ts @@ -0,0 +1,14 @@ +export default function bytesCmp(bytes1: number[] | Uint8Array, bytes2: number[] | Uint8Array) { + const len = bytes1.length; + if(len !== bytes2.length) { + return false; + } + + for(let i = 0; i < len; ++i) { + if(bytes1[i] !== bytes2[i]) { + return false; + } + } + + return true; +} diff --git a/src/helpers/bytes/bytesFromHex.ts b/src/helpers/bytes/bytesFromHex.ts new file mode 100644 index 000000000..252ee84c1 --- /dev/null +++ b/src/helpers/bytes/bytesFromHex.ts @@ -0,0 +1,15 @@ +export default function bytesFromHex(hexString: string) { + const len = hexString.length; + const bytes = new Uint8Array(Math.ceil(len / 2)); + let start = 0; + + if(len % 2) { // read 0x581 as 0x0581 + bytes[start++] = parseInt(hexString.charAt(0), 16); + } + + for(let i = start; i < len; i += 2) { + bytes[start++] = parseInt(hexString.substr(i, 2), 16); + } + + return bytes; +} diff --git a/src/helpers/bytes/bytesFromWordss.ts b/src/helpers/bytes/bytesFromWordss.ts new file mode 100644 index 000000000..b9102b374 --- /dev/null +++ b/src/helpers/bytes/bytesFromWordss.ts @@ -0,0 +1,8 @@ +export default function bytesFromWordss(input: Uint32Array) { + const o = new Uint8Array(input.byteLength); + for(let i = 0, length = input.length * 4; i < length; ++i) { + o[i] = ((input[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff); + } + + return o; +} diff --git a/src/helpers/bytes/bytesModPow.ts b/src/helpers/bytes/bytesModPow.ts new file mode 100644 index 000000000..d3642e66c --- /dev/null +++ b/src/helpers/bytes/bytesModPow.ts @@ -0,0 +1,9 @@ +import { bigIntFromBytes, bigIntToBytes } from '../bigInt/bigIntConversion'; + +export default function bytesModPow(bytes: number[] | Uint8Array, exp: number[] | Uint8Array, mod: number[] | Uint8Array) { + const bytesBigInt = bigIntFromBytes(bytes); + const expBigInt = bigIntFromBytes(exp); + const modBigInt = bigIntFromBytes(mod); + const resBigInt = bytesBigInt.modPow(expBigInt, modBigInt); + return bigIntToBytes(resBigInt); +} diff --git a/src/helpers/bytes/bytesToBase64.ts b/src/helpers/bytes/bytesToBase64.ts new file mode 100644 index 000000000..10c86154a --- /dev/null +++ b/src/helpers/bytes/bytesToBase64.ts @@ -0,0 +1,34 @@ +export default function bytesToBase64(bytes: number[] | Uint8Array) { + let mod3: number; + let result = ''; + + for(let nLen = bytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; ++nIdx) { + mod3 = nIdx % 3; + nUint24 |= bytes[nIdx] << (16 >>> mod3 & 24); + if(mod3 === 2 || nLen - nIdx === 1) { + result += String.fromCharCode( + uint6ToBase64(nUint24 >>> 18 & 63), + uint6ToBase64(nUint24 >>> 12 & 63), + uint6ToBase64(nUint24 >>> 6 & 63), + uint6ToBase64(nUint24 & 63) + ); + nUint24 = 0; + } + } + + return result.replace(/A(?=A$|$)/g, '='); +} + +export function uint6ToBase64(nUint6: number) { + return nUint6 < 26 + ? nUint6 + 65 + : nUint6 < 52 + ? nUint6 + 71 + : nUint6 < 62 + ? nUint6 - 4 + : nUint6 === 62 + ? 43 + : nUint6 === 63 + ? 47 + : 65; +} diff --git a/src/helpers/bytes/bytesToHex.ts b/src/helpers/bytes/bytesToHex.ts new file mode 100644 index 000000000..efdaa9505 --- /dev/null +++ b/src/helpers/bytes/bytesToHex.ts @@ -0,0 +1,8 @@ +export default function bytesToHex(bytes: ArrayLike) { + const length = bytes.length; + const arr: string[] = new Array(length); + for(let i = 0; i < length; ++i) { + arr[i] = (bytes[i] < 16 ? '0' : '') + (bytes[i] || 0).toString(16); + } + return arr.join(''); +} diff --git a/src/helpers/bytes/bytesToWordss.ts b/src/helpers/bytes/bytesToWordss.ts new file mode 100644 index 000000000..1365b3229 --- /dev/null +++ b/src/helpers/bytes/bytesToWordss.ts @@ -0,0 +1,12 @@ +import convertToUint8Array from "./convertToUint8Array"; + +export default function bytesToWordss(input: Parameters[0]) { + const bytes = convertToUint8Array(input); + + const words: number[] = []; + for(let i = 0, len = bytes.length; i < len; ++i) { + words[i >>> 2] |= bytes[i] << (24 - (i % 4) * 8); + } + + return new Uint32Array(words); +} diff --git a/src/helpers/bytes/bytesXor.ts b/src/helpers/bytes/bytesXor.ts new file mode 100644 index 000000000..465eb3fab --- /dev/null +++ b/src/helpers/bytes/bytesXor.ts @@ -0,0 +1,10 @@ +export default function bytesXor(bytes1: Uint8Array, bytes2: Uint8Array) { + const len = bytes1.length; + const bytes = new Uint8Array(len); + + for(let i = 0; i < len; ++i) { + bytes[i] = bytes1[i] ^ bytes2[i]; + } + + return bytes; +} diff --git a/src/helpers/bytes/convertToUint8Array.ts b/src/helpers/bytes/convertToUint8Array.ts new file mode 100644 index 000000000..d095f0171 --- /dev/null +++ b/src/helpers/bytes/convertToUint8Array.ts @@ -0,0 +1,9 @@ +export default function convertToUint8Array(bytes: Uint8Array | ArrayBuffer | number[] | string): Uint8Array { + if(bytes instanceof Uint8Array) { + return bytes; + } else if(typeof(bytes) === 'string') { + return new TextEncoder().encode(bytes); + } + + return new Uint8Array(bytes); +} diff --git a/src/helpers/dom/controlsHover.ts b/src/helpers/dom/controlsHover.ts index b5886608a..ed83849be 100644 --- a/src/helpers/dom/controlsHover.ts +++ b/src/helpers/dom/controlsHover.ts @@ -7,7 +7,7 @@ import { IS_TOUCH_SUPPORTED } from "../../environment/touchSupport"; import EventListenerBase from "../eventListenerBase"; import ListenerSetter from "../listenerSetter"; -import { safeAssign } from "../object"; +import safeAssign from "../object/safeAssign"; import findUpClassName from "./findUpClassName"; export default class ControlsHover extends EventListenerBase<{ diff --git a/src/helpers/dropdownHover.ts b/src/helpers/dropdownHover.ts index 35a10c8e0..66bba089b 100644 --- a/src/helpers/dropdownHover.ts +++ b/src/helpers/dropdownHover.ts @@ -8,8 +8,8 @@ import { attachClickEvent } from "./dom/clickEvent"; import findUpAsChild from "./dom/findUpAsChild"; import EventListenerBase from "./eventListenerBase"; import ListenerSetter from "./listenerSetter"; -import { safeAssign } from "./object"; import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport"; +import safeAssign from "./object/safeAssign"; const KEEP_OPEN = false; const TOGGLE_TIMEOUT = 200; diff --git a/src/helpers/filterChatPhotosMessages.ts b/src/helpers/filterChatPhotosMessages.ts index cc3f0df95..8de0c6b29 100644 --- a/src/helpers/filterChatPhotosMessages.ts +++ b/src/helpers/filterChatPhotosMessages.ts @@ -6,7 +6,7 @@ import type { Message, MessageAction } from "../layer"; import type { MyMessage } from "../lib/appManagers/appMessagesManager"; -import { forEachReverse } from "./array"; +import forEachReverse from "./array/forEachReverse"; export default function filterChatPhotosMessages(value: { count: number; diff --git a/src/helpers/gzipUncompress.ts b/src/helpers/gzipUncompress.ts new file mode 100644 index 000000000..246ddd332 --- /dev/null +++ b/src/helpers/gzipUncompress.ts @@ -0,0 +1,12 @@ +//export function gzipUncompress(bytes: ArrayBuffer, toString: true): string; + +// @ts-ignore +import pako from 'pako/dist/pako_inflate.min.js'; + +//export function gzipUncompress(bytes: ArrayBuffer, toString?: false): Uint8Array; +export default function gzipUncompress(bytes: ArrayBuffer, toString?: boolean): string | Uint8Array { + //console.log(dT(), 'Gzip uncompress start'); + const result = pako.inflate(bytes, toString ? {to: 'string'} : undefined); + //console.log(dT(), 'Gzip uncompress finish'/* , result */); + return result; +} diff --git a/src/helpers/listLoader.ts b/src/helpers/listLoader.ts index dd2d22d71..2dfe31d60 100644 --- a/src/helpers/listLoader.ts +++ b/src/helpers/listLoader.ts @@ -4,8 +4,8 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import { forEachReverse } from "./array"; -import { safeAssign } from "./object"; +import forEachReverse from "./array/forEachReverse"; +import safeAssign from "./object/safeAssign"; export type ListLoaderOptions = { loadMore: ListLoader['loadMore'], diff --git a/src/helpers/long/longFromInts.ts b/src/helpers/long/longFromInts.ts new file mode 100644 index 000000000..be066edbe --- /dev/null +++ b/src/helpers/long/longFromInts.ts @@ -0,0 +1,7 @@ +import bigInt from "big-integer"; +import intToUint from "../number/intToUint"; + +export default function longFromInts(high: number, low: number): string { + high = intToUint(high), low = intToUint(low); + return bigInt(high).shiftLeft(32).add(bigInt(low)).toString(10); +} diff --git a/src/helpers/long/longToBytes.ts b/src/helpers/long/longToBytes.ts new file mode 100644 index 000000000..64684fb88 --- /dev/null +++ b/src/helpers/long/longToBytes.ts @@ -0,0 +1,11 @@ +import addPadding from '../bytes/addPadding'; +import bigInt from 'big-integer'; +import { bigIntToBytes } from '../bigInt/bigIntConversion'; + +export default function longToBytes(sLong: string) { + const bigIntBytes = bigIntToBytes(bigInt(sLong)).reverse(); + const bytes = addPadding(bigIntBytes, 8, true, false, false); + // console.log('longToBytes', bytes, bigIntBytes); + + return bytes; +} diff --git a/src/helpers/long/sortLongsArray.ts b/src/helpers/long/sortLongsArray.ts new file mode 100644 index 000000000..3dd4a5d4b --- /dev/null +++ b/src/helpers/long/sortLongsArray.ts @@ -0,0 +1,11 @@ +import bigInt from "big-integer"; + +export default function sortLongsArray(arr: string[]) { + return arr.map(long => { + return bigInt(long); + }).sort((a, b) => { + return a.compare(b); + }).map(bigInt => { + return bigInt.toString(10); + }); +} diff --git a/src/helpers/movablePanel.ts b/src/helpers/movablePanel.ts index 4ebe450e6..cce7f1a26 100644 --- a/src/helpers/movablePanel.ts +++ b/src/helpers/movablePanel.ts @@ -8,7 +8,7 @@ import MovableElement, { MovableElementOptions, MovableState } from "../componen import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport"; import ListenerSetter from "./listenerSetter"; import mediaSizes, { ScreenSize } from "./mediaSizes"; -import { safeAssign } from "./object"; +import safeAssign from "./object/safeAssign"; export default class MovablePanel { #movable: MovableElement; diff --git a/src/helpers/number/intToUint.ts b/src/helpers/number/intToUint.ts new file mode 100644 index 000000000..bc8e5e713 --- /dev/null +++ b/src/helpers/number/intToUint.ts @@ -0,0 +1,4 @@ +export default function intToUint(val: number) { + // return val < 0 ? val + 4294967296 : val; // 0 <= val <= Infinity + return val >>> 0; // (4294967296 >>> 0) === 0; 0 <= val <= 4294967295 +} diff --git a/src/helpers/object.ts b/src/helpers/object.ts deleted file mode 100644 index 9e9b66d10..000000000 --- a/src/helpers/object.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* - * https://github.com/morethanwords/tweb - * Copyright (C) 2019-2021 Eduard Kuzmenko - * https://github.com/morethanwords/tweb/blob/master/LICENSE - * - * Originally from: - * https://github.com/zhukov/webogram - * Copyright (C) 2014 Igor Zhukov - * https://github.com/zhukov/webogram/blob/master/LICENSE - */ - -export function copy(obj: T): T { - //in case of premitives - if(obj === null || typeof(obj) !== "object") { - return obj; - } - - //date objects should be - if(obj instanceof Date) { - return new Date(obj.getTime()) as any; - } - - //handle Array - if(Array.isArray(obj)) { - // @ts-ignore - const clonedArr: T = obj.map(el => copy(el)) as any as T; - return clonedArr; - } - - //lastly, handle objects - // @ts-ignore - let clonedObj = new obj.constructor(); - for(var prop in obj){ - if(obj.hasOwnProperty(prop)) { - clonedObj[prop] = copy(obj[prop]); - } - } - return clonedObj; -} - -export function deepEqual(x: any, y: any): boolean { - const ok = Object.keys, tx = typeof x, ty = typeof y; - return x && y && tx === 'object' && tx === ty ? ( - ok(x).length === ok(y).length && - ok(x).every(key => deepEqual(x[key], y[key])) - ) : (x === y); -} - -export function defineNotNumerableProperties(obj: T, names: (keyof T)[]) { - //const perf = performance.now(); - const props = {writable: true, configurable: true}; - const out: {[name in keyof T]?: typeof props} = {}; - names.forEach(name => { - if(!obj.hasOwnProperty(name)) { - out[name] = props; - } - }); - Object.defineProperties(obj, out); - //console.log('defineNotNumerableProperties time:', performance.now() - perf); -} - -export function getObjectKeysAndSort(object: {[key: string]: any}, sort: 'asc' | 'desc' = 'asc') { - if(!object) return []; - const ids = object instanceof Map ? [...object.keys()] : Object.keys(object).map(i => +i); - if(sort === 'asc') return ids.sort((a, b) => a - b); - else return ids.sort((a, b) => b - a); -} - -export function safeReplaceObject(wasObject: any, newObject: any) { - if(!wasObject) { - return newObject; - } - - for(var key in wasObject) { - if(!newObject.hasOwnProperty(key)) { - delete wasObject[key]; - } - } - - for(var key in newObject) { - //if (newObject.hasOwnProperty(key)) { // useless - wasObject[key] = newObject[key]; - //} - } - - return wasObject; -} - -/** - * Will be used for FILE_REFERENCE_EXPIRED - * @param key - * @param wasObject - * @param newObject - */ -export function safeReplaceArrayInObject(key: K, wasObject: any, newObject: any) { - if('byteLength' in newObject[key]) { // Uint8Array - newObject[key] = [...newObject[key]]; - } - - if(wasObject && wasObject[key] !== newObject[key]) { - wasObject[key].length = newObject[key].length; - (newObject[key] as any[]).forEach((v, i) => { - wasObject[key][i] = v; - }); - - /* wasObject[key].set(newObject[key]); */ - newObject[key] = wasObject[key]; - } -} - -export function isObject>(object: any): object is T { - return typeof(object) === 'object' && object !== null; -} - -export function getDeepProperty(object: any, key: string) { - const splitted = key.split('.'); - let o: any = object; - splitted.forEach(key => { - if(!key) { - return; - } - - // @ts-ignore - o = o[key]; - }); - - return o; -} - -export function setDeepProperty(object: any, key: string, value: any) { - const splitted = key.split('.'); - getDeepProperty(object, splitted.slice(0, -1).join('.'))[splitted.pop()] = value; -} - -export function validateInitObject(initObject: any, currentObject: any, onReplace?: (key: string) => void, previousKey?: string) { - for(const key in initObject) { - if(typeof(currentObject[key]) !== typeof(initObject[key])) { - currentObject[key] = copy(initObject[key]); - onReplace && onReplace(previousKey || key); - } else if(isObject(initObject[key])) { - validateInitObject(initObject[key], currentObject[key], onReplace, previousKey || key); - } - } -} - -export function safeAssign(object: T, fromObject: any) { - if(fromObject) { - for(let i in fromObject) { - if(fromObject[i] !== undefined) { - // @ts-ignore - object[i] = fromObject[i]; - } - } - } - - return object; -} diff --git a/src/helpers/object/copy.ts b/src/helpers/object/copy.ts new file mode 100644 index 000000000..c741088e8 --- /dev/null +++ b/src/helpers/object/copy.ts @@ -0,0 +1,28 @@ +export default function copy(obj: T): T { + //in case of premitives + if(obj === null || typeof(obj) !== "object") { + return obj; + } + + //date objects should be + if(obj instanceof Date) { + return new Date(obj.getTime()) as any; + } + + //handle Array + if(Array.isArray(obj)) { + // @ts-ignore + const clonedArr: T = obj.map(el => copy(el)) as any as T; + return clonedArr; + } + + //lastly, handle objects + // @ts-ignore + let clonedObj = new obj.constructor(); + for(var prop in obj){ + if(obj.hasOwnProperty(prop)) { + clonedObj[prop] = copy(obj[prop]); + } + } + return clonedObj; +} diff --git a/src/helpers/object/deepEqual.ts b/src/helpers/object/deepEqual.ts new file mode 100644 index 000000000..e30a74334 --- /dev/null +++ b/src/helpers/object/deepEqual.ts @@ -0,0 +1,7 @@ +export default function deepEqual(x: any, y: any): boolean { + const ok = Object.keys, tx = typeof x, ty = typeof y; + return x && y && tx === 'object' && tx === ty ? ( + ok(x).length === ok(y).length && + ok(x).every(key => deepEqual(x[key], y[key])) + ) : (x === y); +} diff --git a/src/helpers/object/defineNotNumerableProperties.ts b/src/helpers/object/defineNotNumerableProperties.ts new file mode 100644 index 000000000..60965375b --- /dev/null +++ b/src/helpers/object/defineNotNumerableProperties.ts @@ -0,0 +1,12 @@ +export default function defineNotNumerableProperties(obj: T, names: (keyof T)[]) { + //const perf = performance.now(); + const props = {writable: true, configurable: true}; + const out: {[name in keyof T]?: typeof props} = {}; + names.forEach(name => { + if(!obj.hasOwnProperty(name)) { + out[name] = props; + } + }); + Object.defineProperties(obj, out); + //console.log('defineNotNumerableProperties time:', performance.now() - perf); +} diff --git a/src/helpers/object/getDeepProperty.ts b/src/helpers/object/getDeepProperty.ts new file mode 100644 index 000000000..c14a01124 --- /dev/null +++ b/src/helpers/object/getDeepProperty.ts @@ -0,0 +1,14 @@ +export default function getDeepProperty(object: any, key: string) { + const splitted = key.split('.'); + let o: any = object; + splitted.forEach(key => { + if(!key) { + return; + } + + // @ts-ignore + o = o[key]; + }); + + return o; +} diff --git a/src/helpers/object/getObjectKeysAndSort.ts b/src/helpers/object/getObjectKeysAndSort.ts new file mode 100644 index 000000000..ed7924235 --- /dev/null +++ b/src/helpers/object/getObjectKeysAndSort.ts @@ -0,0 +1,6 @@ +export default function getObjectKeysAndSort(object: {[key: string]: any}, sort: 'asc' | 'desc' = 'asc') { + if(!object) return []; + const ids = object instanceof Map ? [...object.keys()] : Object.keys(object).map(i => +i); + if(sort === 'asc') return ids.sort((a, b) => a - b); + else return ids.sort((a, b) => b - a); +} diff --git a/src/helpers/object/isObject.ts b/src/helpers/object/isObject.ts new file mode 100644 index 000000000..9e38a64f8 --- /dev/null +++ b/src/helpers/object/isObject.ts @@ -0,0 +1,3 @@ +export default function isObject>(object: any): object is T { + return typeof(object) === 'object' && object !== null; +} diff --git a/src/helpers/object/safeAssign.ts b/src/helpers/object/safeAssign.ts new file mode 100644 index 000000000..623e592fe --- /dev/null +++ b/src/helpers/object/safeAssign.ts @@ -0,0 +1,12 @@ +export default function safeAssign(object: T, fromObject: any) { + if(fromObject) { + for(let i in fromObject) { + if(fromObject[i] !== undefined) { + // @ts-ignore + object[i] = fromObject[i]; + } + } + } + + return object; +} diff --git a/src/helpers/object/safeReplaceArrayInObject.ts b/src/helpers/object/safeReplaceArrayInObject.ts new file mode 100644 index 000000000..568eed0ba --- /dev/null +++ b/src/helpers/object/safeReplaceArrayInObject.ts @@ -0,0 +1,21 @@ +/** + * Will be used for FILE_REFERENCE_EXPIRED + * @param key + * @param wasObject + * @param newObject + */ + export default function safeReplaceArrayInObject(key: K, wasObject: any, newObject: any) { + if('byteLength' in newObject[key]) { // Uint8Array + newObject[key] = [...newObject[key]]; + } + + if(wasObject && wasObject[key] !== newObject[key]) { + wasObject[key].length = newObject[key].length; + (newObject[key] as any[]).forEach((v, i) => { + wasObject[key][i] = v; + }); + + /* wasObject[key].set(newObject[key]); */ + newObject[key] = wasObject[key]; + } +} diff --git a/src/helpers/object/safeReplaceObject.ts b/src/helpers/object/safeReplaceObject.ts new file mode 100644 index 000000000..200dacc6b --- /dev/null +++ b/src/helpers/object/safeReplaceObject.ts @@ -0,0 +1,19 @@ +export default function safeReplaceObject(wasObject: any, newObject: any) { + if(!wasObject) { + return newObject; + } + + for(var key in wasObject) { + if(!newObject.hasOwnProperty(key)) { + delete wasObject[key]; + } + } + + for(var key in newObject) { + //if (newObject.hasOwnProperty(key)) { // useless + wasObject[key] = newObject[key]; + //} + } + + return wasObject; +} diff --git a/src/helpers/object/setDeepProperty.ts b/src/helpers/object/setDeepProperty.ts new file mode 100644 index 000000000..7d9018692 --- /dev/null +++ b/src/helpers/object/setDeepProperty.ts @@ -0,0 +1,6 @@ +import getDeepProperty from "./getDeepProperty"; + +export default function setDeepProperty(object: any, key: string, value: any) { + const splitted = key.split('.'); + getDeepProperty(object, splitted.slice(0, -1).join('.'))[splitted.pop()] = value; +} diff --git a/src/helpers/object/validateInitObject.ts b/src/helpers/object/validateInitObject.ts new file mode 100644 index 000000000..513dd4486 --- /dev/null +++ b/src/helpers/object/validateInitObject.ts @@ -0,0 +1,13 @@ +import copy from "./copy"; +import isObject from "./isObject"; + +export default function validateInitObject(initObject: any, currentObject: any, onReplace?: (key: string) => void, previousKey?: string) { + for(const key in initObject) { + if(typeof(currentObject[key]) !== typeof(initObject[key])) { + currentObject[key] = copy(initObject[key]); + onReplace && onReplace(previousKey || key); + } else if(isObject(initObject[key])) { + validateInitObject(initObject[key], currentObject[key], onReplace, previousKey || key); + } + } +} diff --git a/src/helpers/scrollableLoader.ts b/src/helpers/scrollableLoader.ts index 8646210f4..62bebbe91 100644 --- a/src/helpers/scrollableLoader.ts +++ b/src/helpers/scrollableLoader.ts @@ -5,7 +5,7 @@ */ import Scrollable from "../components/scrollable"; -import { safeAssign } from "./object"; +import safeAssign from "./object/safeAssign"; export default class ScrollableLoader { public loading = false; diff --git a/src/helpers/searchListLoader.ts b/src/helpers/searchListLoader.ts index f80f07a0c..a87e58936 100644 --- a/src/helpers/searchListLoader.ts +++ b/src/helpers/searchListLoader.ts @@ -10,7 +10,7 @@ import type { Message } from "../layer"; import appMessagesIdsManager from "../lib/appManagers/appMessagesIdsManager"; import appMessagesManager, { MyMessage } from "../lib/appManagers/appMessagesManager"; import rootScope from "../lib/rootScope"; -import { forEachReverse } from "./array"; +import forEachReverse from "./array/forEachReverse"; import filterChatPhotosMessages from "./filterChatPhotosMessages"; import ListLoader, { ListLoaderOptions } from "./listLoader"; diff --git a/src/helpers/sortedList.ts b/src/helpers/sortedList.ts index b2717b14e..9b1962f37 100644 --- a/src/helpers/sortedList.ts +++ b/src/helpers/sortedList.ts @@ -4,9 +4,9 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import { insertInDescendSortedArray } from "./array"; +import insertInDescendSortedArray from "./array/insertInDescendSortedArray"; import { getMiddleware } from "./middleware"; -import { safeAssign } from "./object"; +import safeAssign from "./object/safeAssign"; export type SortedElementId = PeerId; export type SortedElementBase = { diff --git a/src/helpers/toggleClassName.ts b/src/helpers/toggleClassName.ts new file mode 100644 index 000000000..7687c6232 --- /dev/null +++ b/src/helpers/toggleClassName.ts @@ -0,0 +1,13 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +export default function toggleClassName(className: string, elements: HTMLElement[], disable: boolean) { + elements.forEach((element) => { + element.classList.toggle(className, disable); + }); + + return () => toggleClassName(className, elements, !disable); +} diff --git a/src/lang.ts b/src/lang.ts index ccc23b31b..9b5e3e5c5 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -614,6 +614,8 @@ const lang = { "BotUnblock": "RESTART", "BotStop": "Stop bot", "BotRestart": "Restart bot", + "VoipUserMicrophoneIsOff": "%s\'s microphone is off", + "VoipUserCameraIsOff": "%s\'s camera is off", // * macos "AccountSettings.Filters": "Chat Folders", diff --git a/src/lib/appManagers/appCallsManager.ts b/src/lib/appManagers/appCallsManager.ts index 60d4ae572..7502eb0c4 100644 --- a/src/lib/appManagers/appCallsManager.ts +++ b/src/lib/appManagers/appCallsManager.ts @@ -10,15 +10,338 @@ */ import { MOUNT_CLASS_TO } from "../../config/debug"; +import IS_CALL_SUPPORTED from "../../environment/callSupport"; +import AudioAssetPlayer from "../../helpers/audioAssetPlayer"; +import bytesCmp from "../../helpers/bytes/bytesCmp"; +import safeReplaceObject from "../../helpers/object/safeReplaceObject"; +import { nextRandomUint } from "../../helpers/random"; +import tsNow from "../../helpers/tsNow"; +import { InputPhoneCall, MessagesDhConfig, PhoneCall, PhoneCallDiscardReason, PhoneCallProtocol, PhonePhoneCall } from "../../layer"; +import CallInstance from "../calls/callInstance"; +import CALL_STATE from "../calls/callState"; import { logger } from "../logger"; +import apiManager from "../mtproto/mtprotoworker"; +import { NULL_PEER_ID } from "../mtproto/mtproto_config"; +import rootScope from "../rootScope"; +import apiUpdatesManager from "./apiUpdatesManager"; +import appProfileManager from "./appProfileManager"; +import appUsersManager from "./appUsersManager"; + +export type CallId = PhoneCall['id']; + +export type MyPhoneCall = Exclude; + +const CALL_REQUEST_TIMEOUT = 45e3; + +export type CallAudioAssetName = "call_busy.mp3" | "call_connect.mp3" | "call_end.mp3" | "call_incoming.mp3" | "call_outgoing.mp3" | "voip_failed.mp3" | "voip_connecting.mp3"; export class AppCallsManager { private log: ReturnType; - + private calls: Map; + private instances: Map; + private tempId: number; + private audioAsset: AudioAssetPlayer; + constructor() { this.log = logger('CALLS'); + + this.tempId = 0; + this.calls = new Map(); + this.instances = new Map(); + if(!IS_CALL_SUPPORTED) { + return; + } + rootScope.addMultipleEventsListeners({ + updatePhoneCall: async(update) => { + const call = this.saveCall(update.phone_call); + + let instance = this.instances.get(call.id); + + switch(call._) { + case 'phoneCallDiscarded': { + if(instance) { + instance.hangUp(call.reason?._, true); + } + + break; + } + + case 'phoneCallAccepted': { + if(instance) { + instance.confirmCall(); + } + + break; + } + + case 'phoneCallRequested': { + if(!instance) { + instance = this.createCallInstance({ + isOutgoing: false, + interlocutorUserId: call.admin_id + }); + + instance.overrideConnectionState(CALL_STATE.PENDING); + instance.setPhoneCall(call); + instance.setHangUpTimeout(CALL_REQUEST_TIMEOUT, 'phoneCallDiscardReasonMissed'); + } + + break; + } + + case 'phoneCall': { + if(!instance || instance.encryptionKey) { + break; + } + + const g_a = instance.dh.g_a = call.g_a_or_b; + const dh = instance.dh; + const g_a_hash = await apiManager.invokeCrypto('sha256', g_a); + if(!bytesCmp(dh.g_a_hash, g_a_hash)) { + this.log.error('Incorrect g_a_hash', dh.g_a_hash, g_a_hash); + break; + } + + const {key, key_fingerprint} = await this.computeKey(g_a, dh.b, dh.p); + if(call.key_fingerprint !== key_fingerprint) { + this.log.error('Incorrect key fingerprint', call.key_fingerprint, key_fingerprint); + break; + } + + instance.encryptionKey = key; + instance.joinCall(); + + break; + } + } + }, + + updatePhoneCallSignalingData: (update) => { + const instance = this.instances.get(update.phone_call_id); + if(instance?.id !== update.phone_call_id) { + return; + } + + instance.onUpdatePhoneCallSignalingData(update); + } + }); + + this.audioAsset = new AudioAssetPlayer([ + 'call_busy.mp3', + 'call_connect.mp3', + 'call_end.mp3', + 'call_incoming.mp3', + 'call_outgoing.mp3', + 'voip_failed.mp3' + ]); + } + + public get currentCall() { + let lastInstance: CallInstance; + for(const [callId, instance] of this.instances) { + lastInstance = instance; + if(instance.connectionState !== CALL_STATE.PENDING) { + break; + } + } + + return lastInstance; + } + + public getCallByUserId(userId: UserId) { + for(const [callId, instance] of this.instances) { + if(instance.interlocutorUserId === userId) { + return instance; + } + } + } + + public async computeKey(g_b: Uint8Array, a: Uint8Array, p: Uint8Array) { + return apiManager.invokeCrypto('compute-dh-key', g_b, a, p); + } + + public saveCall(call: PhoneCall) { + const isDiscarded = call._ === 'phoneCallDiscarded'; + const oldCall = this.calls.get(call.id); + if(oldCall) { + // if(shouldUpdate) { + safeReplaceObject(oldCall, call); + // } + + if(isDiscarded) { + this.calls.delete(call.id); + } + + call = oldCall; + } else if(!isDiscarded) { + this.calls.set(call.id, call as any); + } + + return call; + } + + public getCall(callId: CallId) { + return this.calls.get(callId); + } + + public getCallInput(id: CallId): InputPhoneCall { + const call = this.getCall(id); + return { + _: 'inputPhoneCall', + id: call.id, + access_hash: call.access_hash + }; + } + + private createCallInstance(options: { + isOutgoing: boolean, + interlocutorUserId: UserId, + protocol?: PhoneCallProtocol + }) { + const call = new CallInstance({ + appCallsManager: this, + apiManager, + apiUpdatesManager, + ...options, + }); + + let wasTryingToJoin = false; + call.addEventListener('state', (state) => { + const currentCall = this.currentCall; + if(state === CALL_STATE.CLOSED) { + this.instances.delete(call.id); + } + + if(state === CALL_STATE.EXCHANGING_KEYS) { + wasTryingToJoin = true; + } + + const hasConnected = call.connectedAt !== undefined; + if(state === CALL_STATE.EXCHANGING_KEYS || (state === CALL_STATE.CONNECTING && hasConnected)) { + call.setHangUpTimeout(CALL_REQUEST_TIMEOUT, 'phoneCallDiscardReasonDisconnect'); + } else { + call.clearHangUpTimeout(); + } + + if(currentCall === call || !currentCall) { + if(state === CALL_STATE.CLOSED) { + if(!call.isOutgoing && !wasTryingToJoin) { // incoming call has been accepted on other device or ended + this.audioAsset.stopSound(); + } else if(wasTryingToJoin && !hasConnected) { // something has happened during the key exchanging + this.audioAsset.playSound('voip_failed.mp3'); + } else { + this.audioAsset.playSound(call.discardReason === 'phoneCallDiscardReasonBusy' ? 'call_busy.mp3' : 'call_end.mp3'); + } + } else if(state === CALL_STATE.PENDING) { + this.audioAsset.playSound(call.isOutgoing ? 'call_outgoing.mp3' : 'call_incoming.mp3', true); + } else if(state === CALL_STATE.EXCHANGING_KEYS) { + this.audioAsset.playSoundIfDifferent('call_connect.mp3'); + } else if(state === CALL_STATE.CONNECTING) { + if(call.duration) { + this.audioAsset.playSound('voip_connecting.mp3', true); + } + } else { + this.audioAsset.stopSound(); + } + } + }); + + call.addEventListener('id', (id, prevId) => { + if(prevId !== undefined) { + this.instances.delete(prevId); + } + + const hasCurrent = !!this.currentCall; + this.instances.set(id, call); + + if(prevId === undefined) { + rootScope.dispatchEvent('call_instance', {instance: call, hasCurrent: hasCurrent}); + } + }); + + return call; + } + + public savePhonePhoneCall(phonePhoneCall: PhonePhoneCall) { + appUsersManager.saveApiUsers(phonePhoneCall.users); + return this.saveCall(phonePhoneCall.phone_call); + } + + public generateDh() { + return apiManager.invokeApi('messages.getDhConfig', { + version: 0, + random_length: 256 + }).then(async(dhConfig) => { + return apiManager.invokeCrypto('generate-dh', dhConfig as MessagesDhConfig.messagesDhConfig); + }); + } + + public startCallInternal(userId: UserId, isVideo: boolean) { + this.log('p2pStartCallInternal', userId, isVideo); + + const fullInfo = appProfileManager.getCachedFullUser(userId); + if(!fullInfo) return; + + const {video_calls_available} = fullInfo.pFlags; + + const call = this.createCallInstance({ + isOutgoing: true, + interlocutorUserId: userId + }); + + call.requestInputSource(true, !!(isVideo && video_calls_available), false); + + call.overrideConnectionState(CALL_STATE.REQUESTING); + call.setPhoneCall({ + _: 'phoneCallWaiting', + access_hash: '', + admin_id: NULL_PEER_ID, + date: tsNow(true), + id: --this.tempId, + participant_id: userId, + protocol: call.protocol, + pFlags: { + video: isVideo || undefined + } + }); + + // return; + this.generateDh().then(dh => { + call.dh = dh; + + return apiManager.invokeApi('phone.requestCall', { + user_id: appUsersManager.getUserInput(userId), + protocol: call.protocol, + video: isVideo && video_calls_available, + random_id: nextRandomUint(32), + g_a_hash: call.dh.g_a_hash + }); + }).then(result => { + const phoneCall = this.savePhonePhoneCall(result); + call.overrideConnectionState(CALL_STATE.PENDING); + call.setPhoneCall(phoneCall); + call.setHangUpTimeout(CALL_REQUEST_TIMEOUT, 'phoneCallDiscardReasonHangup'); + }); + } + + public async discardCall(callId: CallId, duration: number, reason: PhoneCallDiscardReason['_'], video?: boolean) { + if(!this.getCall(callId)) { + return; + } + + const updates = await apiManager.invokeApi('phone.discardCall', { + video, + peer: this.getCallInput(callId), + duration, + reason: { + _: reason + }, + connection_id: '0' + }); + + apiUpdatesManager.processUpdateMessage(updates); } } diff --git a/src/lib/appManagers/appChatsManager.ts b/src/lib/appManagers/appChatsManager.ts index 5f9444244..9e606c84d 100644 --- a/src/lib/appManagers/appChatsManager.ts +++ b/src/lib/appManagers/appChatsManager.ts @@ -10,7 +10,10 @@ */ import DEBUG, { MOUNT_CLASS_TO } from "../../config/debug"; -import { isObject, safeReplaceObject, copy, deepEqual } from "../../helpers/object"; +import copy from "../../helpers/object/copy"; +import deepEqual from "../../helpers/object/deepEqual"; +import isObject from "../../helpers/object/isObject"; +import safeReplaceObject from "../../helpers/object/safeReplaceObject"; import { ChannelParticipant, Chat, ChatAdminRights, ChatBannedRights, ChatParticipant, ChatPhoto, InputChannel, InputChatPhoto, InputFile, InputPeer, Update, Updates } from "../../layer"; import apiManagerProxy from "../mtproto/mtprotoworker"; import apiManager from '../mtproto/mtprotoworker'; diff --git a/src/lib/appManagers/appDocsManager.ts b/src/lib/appManagers/appDocsManager.ts index b3625e6d7..abeaa4067 100644 --- a/src/lib/appManagers/appDocsManager.ts +++ b/src/lib/appManagers/appDocsManager.ts @@ -10,7 +10,6 @@ */ import { FileURLType, getFileNameByLocation, getFileURL } from '../../helpers/fileName'; -import { safeReplaceArrayInObject, defineNotNumerableProperties, isObject } from '../../helpers/object'; import { Document, InputFileLocation, InputMedia, PhotoSize } from '../../layer'; import referenceDatabase, { ReferenceContext } from '../mtproto/referenceDatabase'; import opusDecodeController from '../opusDecodeController'; @@ -23,6 +22,9 @@ import { MOUNT_CLASS_TO } from '../../config/debug'; import { getFullDate } from '../../helpers/date'; import rootScope from '../rootScope'; import IS_WEBP_SUPPORTED from '../../environment/webpSupport'; +import defineNotNumerableProperties from '../../helpers/object/defineNotNumerableProperties'; +import isObject from '../../helpers/object/isObject'; +import safeReplaceArrayInObject from '../../helpers/object/safeReplaceArrayInObject'; export type MyDocument = Document.document; diff --git a/src/lib/appManagers/appDraftsManager.ts b/src/lib/appManagers/appDraftsManager.ts index 8940bf4da..f7b67069f 100644 --- a/src/lib/appManagers/appDraftsManager.ts +++ b/src/lib/appManagers/appDraftsManager.ts @@ -18,11 +18,11 @@ import serverTimeManager from "../mtproto/serverTimeManager"; import { MessageEntity, DraftMessage, MessagesSaveDraft } from "../../layer"; import apiManager from "../mtproto/mtprotoworker"; import { tsNow } from "../../helpers/date"; -import { deepEqual } from "../../helpers/object"; -import { isObject } from "../mtproto/bin_utils"; import { MOUNT_CLASS_TO } from "../../config/debug"; import stateStorage from "../stateStorage"; import appMessagesIdsManager from "./appMessagesIdsManager"; +import isObject from "../../helpers/object/isObject"; +import deepEqual from "../../helpers/object/deepEqual"; export type MyDraftMessage = DraftMessage.draftMessage; diff --git a/src/lib/appManagers/appEmojiManager.ts b/src/lib/appManagers/appEmojiManager.ts index e33284efe..f7e8e34a4 100644 --- a/src/lib/appManagers/appEmojiManager.ts +++ b/src/lib/appManagers/appEmojiManager.ts @@ -6,10 +6,10 @@ import App from "../../config/app"; import { MOUNT_CLASS_TO } from "../../config/debug"; -import { indexOfAndSplice } from "../../helpers/array"; -import { validateInitObject } from "../../helpers/object"; +import indexOfAndSplice from "../../helpers/array/indexOfAndSplice"; +import isObject from "../../helpers/object/isObject"; +import validateInitObject from "../../helpers/object/validateInitObject"; import I18n from "../langPack"; -import { isObject } from "../mtproto/bin_utils"; import apiManager from "../mtproto/mtprotoworker"; import RichTextProcessor from "../richtextprocessor"; import rootScope from "../rootScope"; diff --git a/src/lib/appManagers/appGroupCallsManager.ts b/src/lib/appManagers/appGroupCallsManager.ts index 1feac60b8..5e4b202a4 100644 --- a/src/lib/appManagers/appGroupCallsManager.ts +++ b/src/lib/appManagers/appGroupCallsManager.ts @@ -11,7 +11,7 @@ import { MOUNT_CLASS_TO } from "../../config/debug"; import AudioAssetPlayer from "../../helpers/audioAssetPlayer"; -import { safeReplaceObject } from "../../helpers/object"; +import safeReplaceObject from "../../helpers/object/safeReplaceObject"; import { nextRandomUint } from "../../helpers/random"; import tsNow from "../../helpers/tsNow"; import { GroupCall, GroupCallParticipant, GroupCallParticipantVideo, GroupCallParticipantVideoSourceGroup, InputGroupCall, Peer, PhoneJoinGroupCall, PhoneJoinGroupCallPresentation, Update, Updates } from "../../layer"; diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index 75ad6b680..76e8ce714 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -45,7 +45,6 @@ import AppPrivateSearchTab from '../../components/sidebarRight/tabs/search'; import I18n, { i18n, join, LangPackKey } from '../langPack'; import { ChatInvite, Dialog, SendMessageAction } from '../../layer'; import { hslaStringToHex } from '../../helpers/color'; -import { copy, getObjectKeysAndSort } from '../../helpers/object'; import { getFilesFromEvent } from '../../helpers/files'; import PeerTitle from '../../components/peerTitle'; import PopupPeer from '../../components/popups/peer'; @@ -77,8 +76,12 @@ import TopbarCall from '../../components/topbarCall'; import confirmationPopup from '../../components/confirmationPopup'; import IS_GROUP_CALL_SUPPORTED from '../../environment/groupCallSupport'; import appAvatarsManager from './appAvatarsManager'; +import appCallsManager from './appCallsManager'; import IS_CALL_SUPPORTED from '../../environment/callSupport'; import { CallType } from '../calls/types'; +import PopupCall from '../../components/call'; +import copy from '../../helpers/object/copy'; +import getObjectKeysAndSort from '../../helpers/object/getObjectKeysAndSort'; //console.log('appImManager included33!'); @@ -251,7 +254,7 @@ export class AppImManager { this.topbarCall = new TopbarCall(appGroupCallsManager, appPeersManager, appChatsManager, appAvatarsManager); } - /* if(IS_CALL_SUPPORTED) { + if(IS_CALL_SUPPORTED) { rootScope.addEventListener('call_instance', ({instance, hasCurrent}) => { if(hasCurrent) { return; @@ -259,12 +262,11 @@ export class AppImManager { new PopupCall({ appAvatarsManager, - appCallsManager, appPeersManager, instance }).show(); }); - } */ + } // ! do not remove this line // ! instance can be deactivated before the UI starts, because it waits in background for RAF that is delayed @@ -769,7 +771,7 @@ export class AppImManager { } public async callUser(userId: UserId, type: CallType) { - /* const call = appCallsManager.getCallByUserId(userId); + const call = appCallsManager.getCallByUserId(userId); if(call) { return; } @@ -790,17 +792,17 @@ export class AppImManager { await this.discardCurrentCall(userId.toPeerId()); - appCallsManager.startCallInternal(userId, type === 'video'); */ + appCallsManager.startCallInternal(userId, type === 'video'); } private discardCurrentCall(toPeerId: PeerId) { - /* if(appCallsManager.currentCall) return this.discardCallConfirmation(toPeerId); + if(appCallsManager.currentCall) return this.discardCallConfirmation(toPeerId); else if(appGroupCallsManager.groupCall) return this.discardGroupCallConfirmation(toPeerId); - else return Promise.resolve(); */ + else return Promise.resolve(); } private async discardCallConfirmation(toPeerId: PeerId) { - /* const currentCall = appCallsManager.currentCall; + const currentCall = appCallsManager.currentCall; if(currentCall) { await confirmationPopup({ titleLangKey: 'Call.Confirm.Discard.Call.Header', @@ -817,7 +819,7 @@ export class AppImManager { if(appCallsManager.currentCall === currentCall) { await currentCall.hangUp(); } - } */ + } } private async discardGroupCallConfirmation(toPeerId: PeerId) { diff --git a/src/lib/appManagers/appInlineBotsManager.ts b/src/lib/appManagers/appInlineBotsManager.ts index 33a29fca3..3f1bca52d 100644 --- a/src/lib/appManagers/appInlineBotsManager.ts +++ b/src/lib/appManagers/appInlineBotsManager.ts @@ -22,8 +22,8 @@ import { MOUNT_CLASS_TO } from "../../config/debug"; import rootScope from "../rootScope"; import appDraftsManager from "./appDraftsManager"; import appMessagesIdsManager from "./appMessagesIdsManager"; -import { insertInDescendSortedArray } from "../../helpers/array"; import appStateManager from "./appStateManager"; +import insertInDescendSortedArray from "../../helpers/array/insertInDescendSortedArray"; export class AppInlineBotsManager { private inlineResults: {[queryAndResultIds: string]: BotInlineResult} = {}; diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 7817e8577..d4265f7ca 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -14,7 +14,6 @@ import ProgressivePreloader from "../../components/preloader"; import { CancellablePromise, deferredPromise } from "../../helpers/cancellablePromise"; import { formatDateAccordingToTodayNew, formatTime, tsNow } from "../../helpers/date"; import { createPosterForVideo } from "../../helpers/files"; -import { copy, getObjectKeysAndSort } from "../../helpers/object"; import { randomLong } from "../../helpers/random"; import { splitStringByLength, limitSymbols, escapeRegExp } from "../../helpers/string"; import { Chat, ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageMedia, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update, Photo, Updates, ReplyMarkup, InputPeer, InputPhoto, InputDocument, InputGeoPoint, WebPage, GeoPoint, ReportReason, MessagesGetDialogs, InputChannel, InputDialogPeer } from "../../layer"; @@ -48,7 +47,6 @@ import DEBUG, { MOUNT_CLASS_TO } from "../../config/debug"; import SlicedArray, { Slice, SliceEnd } from "../../helpers/slicedArray"; import appNotificationsManager, { NotifyOptions } from "./appNotificationsManager"; import PeerTitle from "../../components/peerTitle"; -import { forEachReverse, indexOfAndSplice } from "../../helpers/array"; import htmlToDocumentFragment from "../../helpers/dom/htmlToDocumentFragment"; import htmlToSpan from "../../helpers/dom/htmlToSpan"; import { MUTE_UNTIL, NULL_PEER_ID, REPLIES_PEER_ID, SERVICE_PEER_ID } from "../mtproto/mtproto_config"; @@ -63,6 +61,10 @@ import IMAGE_MIME_TYPES_SUPPORTED from "../../environment/imageMimeTypesSupport" import VIDEO_MIME_TYPES_SUPPORTED from "../../environment/videoMimeTypesSupport"; import './appGroupCallsManager'; import appGroupCallsManager from "./appGroupCallsManager"; +import copy from "../../helpers/object/copy"; +import getObjectKeysAndSort from "../../helpers/object/getObjectKeysAndSort"; +import forEachReverse from "../../helpers/array/forEachReverse"; +import indexOfAndSplice from "../../helpers/array/indexOfAndSplice"; //console.trace('include'); // TODO: если удалить диалог находясь в папке, то он не удалится из папки и будет виден в настройках diff --git a/src/lib/appManagers/appNotificationsManager.ts b/src/lib/appManagers/appNotificationsManager.ts index 1e4da054a..b54b0a414 100644 --- a/src/lib/appManagers/appNotificationsManager.ts +++ b/src/lib/appManagers/appNotificationsManager.ts @@ -13,7 +13,6 @@ import { fontFamily } from "../../components/middleEllipsis"; import { MOUNT_CLASS_TO } from "../../config/debug"; import { CancellablePromise, deferredPromise } from "../../helpers/cancellablePromise"; import { tsNow } from "../../helpers/date"; -import { deepEqual } from "../../helpers/object"; import { convertInputKeyToKey } from "../../helpers/string"; import { IS_MOBILE } from "../../environment/userAgent"; import { InputNotifyPeer, InputPeerNotifySettings, NotifyPeer, PeerNotifySettings, Update } from "../../layer"; @@ -28,6 +27,7 @@ import appPeersManager from "./appPeersManager"; import appRuntimeManager from "./appRuntimeManager"; import appStateManager from "./appStateManager"; import appUsersManager from "./appUsersManager"; +import deepEqual from "../../helpers/object/deepEqual"; type MyNotification = Notification & { hidden?: boolean, diff --git a/src/lib/appManagers/appPeersManager.ts b/src/lib/appManagers/appPeersManager.ts index ecbe6c7f1..f2518bb23 100644 --- a/src/lib/appManagers/appPeersManager.ts +++ b/src/lib/appManagers/appPeersManager.ts @@ -12,13 +12,13 @@ import type { Chat, ChatPhoto, DialogPeer, InputChannel, InputDialogPeer, InputNotifyPeer, InputPeer, Peer, Update, User, UserProfilePhoto } from "../../layer"; import type { LangPackKey } from "../langPack"; import { MOUNT_CLASS_TO } from "../../config/debug"; -import { isObject } from "../../helpers/object"; import { RichTextProcessor } from "../richtextprocessor"; import rootScope from "../rootScope"; import appChatsManager from "./appChatsManager"; import appUsersManager from "./appUsersManager"; import I18n from '../langPack'; import { NULL_PEER_ID } from "../mtproto/mtproto_config"; +import isObject from "../../helpers/object/isObject"; // https://github.com/eelcohn/Telegram-API/wiki/Calculating-color-for-a-Telegram-user-on-IRC /* diff --git a/src/lib/appManagers/appPhotosManager.ts b/src/lib/appManagers/appPhotosManager.ts index 11bb84d04..5c744ad2d 100644 --- a/src/lib/appManagers/appPhotosManager.ts +++ b/src/lib/appManagers/appPhotosManager.ts @@ -10,10 +10,8 @@ */ import type { DownloadOptions } from "../mtproto/apiFileManager"; -import { bytesFromHex } from "../../helpers/bytes"; import { CancellablePromise } from "../../helpers/cancellablePromise"; import { getFileNameByLocation } from "../../helpers/fileName"; -import { safeReplaceArrayInObject, isObject } from "../../helpers/object"; import { IS_SAFARI } from "../../environment/userAgent"; import { InputFileLocation, InputMedia, InputPhoto, Photo, PhotoSize, PhotosPhotos } from "../../layer"; import apiManager from "../mtproto/mtprotoworker"; @@ -27,6 +25,9 @@ import { renderImageFromUrlPromise } from "../../helpers/dom/renderImageFromUrl" import calcImageInBox from "../../helpers/calcImageInBox"; import { makeMediaSize, MediaSize } from "../../helpers/mediaSizes"; import windowSize from "../../helpers/windowSize"; +import bytesFromHex from "../../helpers/bytes/bytesFromHex"; +import isObject from "../../helpers/object/isObject"; +import safeReplaceArrayInObject from "../../helpers/object/safeReplaceArrayInObject"; export type MyPhoto = Photo.photo; diff --git a/src/lib/appManagers/appPollsManager.ts b/src/lib/appManagers/appPollsManager.ts index 455d87d52..f028cbfc9 100644 --- a/src/lib/appManagers/appPollsManager.ts +++ b/src/lib/appManagers/appPollsManager.ts @@ -5,7 +5,7 @@ */ import { MOUNT_CLASS_TO } from "../../config/debug"; -import { copy } from "../../helpers/object"; +import copy from "../../helpers/object/copy"; import { InputMedia, Message, MessageEntity, MessageMedia, Poll, PollResults } from "../../layer"; import { logger, LogTypes } from "../logger"; import apiManager from "../mtproto/mtprotoworker"; diff --git a/src/lib/appManagers/appStateManager.ts b/src/lib/appManagers/appStateManager.ts index 6fd5870a2..36a79ed8d 100644 --- a/src/lib/appManagers/appStateManager.ts +++ b/src/lib/appManagers/appStateManager.ts @@ -14,7 +14,6 @@ import EventListenerBase from '../../helpers/eventListenerBase'; import rootScope from '../rootScope'; import stateStorage from '../stateStorage'; import { logger } from '../logger'; -import { copy, setDeepProperty, validateInitObject } from '../../helpers/object'; import App from '../../config/app'; import DEBUG, { MOUNT_CLASS_TO } from '../../config/debug'; import AppStorage from '../storage'; @@ -24,6 +23,9 @@ import DATABASE_STATE from '../../config/databases/state'; import sessionStorage from '../sessionStorage'; import { nextRandomUint } from '../../helpers/random'; import compareVersion from '../../helpers/compareVersion'; +import copy from '../../helpers/object/copy'; +import setDeepProperty from '../../helpers/object/setDeepProperty'; +import validateInitObject from '../../helpers/object/validateInitObject'; const REFRESH_EVERY = 24 * 60 * 60 * 1000; // 1 day // const REFRESH_EVERY = 1e3; diff --git a/src/lib/appManagers/appStickersManager.ts b/src/lib/appManagers/appStickersManager.ts index eb924a380..3a7ccd9ff 100644 --- a/src/lib/appManagers/appStickersManager.ts +++ b/src/lib/appManagers/appStickersManager.ts @@ -11,13 +11,13 @@ import rootScope from '../rootScope'; import appDocsManager, { MyDocument } from './appDocsManager'; import AppStorage from '../storage'; import { MOUNT_CLASS_TO } from '../../config/debug'; -import { forEachReverse } from '../../helpers/array'; import DATABASE_STATE from '../../config/databases/state'; import { readBlobAsText } from '../../helpers/blob'; import lottieLoader from '../rlottie/lottieLoader'; import mediaSizes from '../../helpers/mediaSizes'; import { getEmojiToneIndex } from '../../vendor/emoji'; import RichTextProcessor from '../richtextprocessor'; +import forEachReverse from '../../helpers/array/forEachReverse'; const CACHE_TIME = 3600e3; diff --git a/src/lib/appManagers/appUsersManager.ts b/src/lib/appManagers/appUsersManager.ts index 9167cf38a..9a2d50191 100644 --- a/src/lib/appManagers/appUsersManager.ts +++ b/src/lib/appManagers/appUsersManager.ts @@ -10,13 +10,15 @@ */ import { MOUNT_CLASS_TO } from "../../config/debug"; -import { filterUnique, indexOfAndSplice } from "../../helpers/array"; +import filterUnique from "../../helpers/array/filterUnique"; +import indexOfAndSplice from "../../helpers/array/indexOfAndSplice"; import { CancellablePromise, deferredPromise } from "../../helpers/cancellablePromise"; import cleanSearchText from "../../helpers/cleanSearchText"; import cleanUsername from "../../helpers/cleanUsername"; import { formatFullSentTimeRaw, tsNow } from "../../helpers/date"; import { formatPhoneNumber } from "../../helpers/formatPhoneNumber"; -import { safeReplaceObject, isObject } from "../../helpers/object"; +import isObject from "../../helpers/object/isObject"; +import safeReplaceObject from "../../helpers/object/safeReplaceObject"; import { Chat, InputContact, InputMedia, InputPeer, InputUser, User as MTUser, UserProfilePhoto, UserStatus } from "../../layer"; import I18n, { i18n, LangPackKey } from "../langPack"; //import apiManager from '../mtproto/apiManager'; diff --git a/src/lib/appManagers/appWebPagesManager.ts b/src/lib/appManagers/appWebPagesManager.ts index 0bdaa8abd..acce6a39f 100644 --- a/src/lib/appManagers/appWebPagesManager.ts +++ b/src/lib/appManagers/appWebPagesManager.ts @@ -14,10 +14,10 @@ import appDocsManager from "./appDocsManager"; import { RichTextProcessor } from "../richtextprocessor"; import { ReferenceContext } from "../mtproto/referenceDatabase"; import rootScope from "../rootScope"; -import { safeReplaceObject } from "../../helpers/object"; import { limitSymbols } from "../../helpers/string"; import { WebPage } from "../../layer"; import { MOUNT_CLASS_TO } from "../../config/debug"; +import safeReplaceObject from "../../helpers/object/safeReplaceObject"; const photoTypeSet = new Set(['photo', 'video', 'gif', 'document']); diff --git a/src/lib/calls/callConnectionInstance.ts b/src/lib/calls/callConnectionInstance.ts new file mode 100644 index 000000000..c700586ec --- /dev/null +++ b/src/lib/calls/callConnectionInstance.ts @@ -0,0 +1,52 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import CallConnectionInstanceBase, { CallConnectionInstanceOptions } from "./callConnectionInstanceBase"; +import CallInstance from "./callInstance"; +import parseSignalingData from "./helpers/parseSignalingData"; +import { parseSdp } from "./sdp/utils"; + +export default class CallConnectionInstance extends CallConnectionInstanceBase { + private call: CallInstance; + + constructor(options: CallConnectionInstanceOptions & { + call: CallConnectionInstance['call'] + }) { + super(options); + } + + protected async negotiateInternal() { + const {connection, call} = this; + + if(!connection.localDescription && !connection.remoteDescription && !call.isOutgoing) { + return; + } + + let descriptionInit: RTCSessionDescriptionInit; + if(call.offerReceived) { + call.offerReceived = false; + + const answer = descriptionInit = await connection.createAnswer(); + + this.log('[sdp] local', answer.type, answer.sdp); + await connection.setLocalDescription(answer); + + this.log('[InitialSetup] send 2'); + } else { + const offer = descriptionInit = await connection.createOffer(); + + this.log('[sdp] local', offer.sdp); + await connection.setLocalDescription(offer); + + call.offerSent = true; + + this.log('[InitialSetup] send 0'); + } + + const initialSetup = parseSignalingData(parseSdp(descriptionInit.sdp)); + call.sendCallSignalingData(initialSetup); + } +} diff --git a/src/lib/calls/callConnectionInstanceBase.ts b/src/lib/calls/callConnectionInstanceBase.ts index 2ab09390a..9f202206e 100644 --- a/src/lib/calls/callConnectionInstanceBase.ts +++ b/src/lib/calls/callConnectionInstanceBase.ts @@ -4,7 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import { safeAssign } from "../../helpers/object"; +import safeAssign from "../../helpers/object/safeAssign"; import { logger } from "../logger"; import createDataChannel from "./helpers/createDataChannel"; import createPeerConnection from "./helpers/createPeerConnection"; diff --git a/src/lib/calls/callInstance.ts b/src/lib/calls/callInstance.ts new file mode 100644 index 000000000..cd5d37ac9 --- /dev/null +++ b/src/lib/calls/callInstance.ts @@ -0,0 +1,843 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import ctx from "../../environment/ctx"; +import { IS_SAFARI } from "../../environment/userAgent"; +import safeAssign from "../../helpers/object/safeAssign"; +import debounce from "../../helpers/schedulers/debounce"; +import { GroupCallParticipantVideoSourceGroup, PhoneCall, PhoneCallDiscardReason, PhoneCallProtocol, Update } from "../../layer"; +import { emojiFromCodePoints } from "../../vendor/emoji"; +import type { ApiUpdatesManager } from "../appManagers/apiUpdatesManager"; +import type { AppCallsManager, CallId } from "../appManagers/appCallsManager"; +import { logger } from "../logger"; +import type { ApiManagerProxy } from "../mtproto/mtprotoworker"; +import CallConnectionInstance from "./callConnectionInstance"; +import CallInstanceBase from "./callInstanceBase"; +import CALL_STATE from "./callState"; +import { GROUP_CALL_AMPLITUDE_ANALYSE_INTERVAL_MS } from "./constants"; +import parseSignalingData from "./helpers/parseSignalingData"; +import stopTrack from "./helpers/stopTrack"; +import localConferenceDescription, { ConferenceEntry, generateSsrc } from "./localConferenceDescription"; +import getCallProtocol from "./p2P/getCallProtocol"; +import getRtcConfiguration from "./p2P/getRtcConfiguration"; +import P2PEncryptor from "./p2P/p2PEncryptor"; +import { p2pParseCandidate, P2PSdpBuilder } from "./p2P/p2PSdpBuilder"; +import { parseSdp } from "./sdp/utils"; +import { WebRTCLineType } from "./sdpBuilder"; +import StreamManager from "./streamManager"; +import { AudioCodec, CallMediaState, CallSignalingData, DiffieHellmanInfo, P2PAudioCodec, P2PVideoCodec, VideoCodec } from "./types"; + +export default class CallInstance extends CallInstanceBase<{ + state: (state: CALL_STATE) => void, + id: (id: CallId, prevId: CallId) => void, + muted: (muted: boolean) => void, + mediaState: (mediaState: CallMediaState) => void +}> { + public dh: Partial; + public id: CallId; + public call: PhoneCall; + public interlocutorUserId: UserId; + public protocol: PhoneCallProtocol; + public isOutgoing: boolean; + public encryptionKey: Uint8Array; + public connectionInstance: CallConnectionInstance; + public encryptor: P2PEncryptor; + public decryptor: P2PEncryptor; + public candidates: RTCIceCandidate[]; + + public offerReceived: boolean; + public offerSent: boolean; + + public createdParticipantEntries: boolean; + public release: () => Promise; + public _connectionState: CALL_STATE; + + public connectedAt: number; + public discardReason: string; + + private appCallsManager: AppCallsManager; + private apiManager: ApiManagerProxy; + private apiUpdatesManager: ApiUpdatesManager; + + private hangUpTimeout: number; + + private mediaStates: { + input: CallMediaState, + output?: CallMediaState + }; + + private sendMediaState: () => Promise; + + private decryptQueue: Uint8Array[]; + + private getEmojisFingerprintPromise: Promise; + private emojisFingerprint: [string, string, string, string]; + + private wasStartingScreen: boolean; + private wasStartingVideo: boolean; + + constructor(options: { + isOutgoing: boolean, + interlocutorUserId: UserId, + appCallsManager: CallInstance['appCallsManager'], + apiManager: CallInstance['apiManager'], + apiUpdatesManager: CallInstance['apiUpdatesManager'], + protocol?: PhoneCallProtocol + }) { + super(); + + this.log = logger('CALL'); + + if(!this.protocol) { + this.protocol = getCallProtocol(); + } + + safeAssign(this, options); + + this.offerReceived = false; + this.offerSent = false; + this.decryptQueue = []; + this.candidates = []; + + this.addEventListener('state', (state) => { + this.log('state', CALL_STATE[state]); + + if(state === CALL_STATE.CLOSED) { + this.cleanup(); + } + }); + + const streamManager = new StreamManager(GROUP_CALL_AMPLITUDE_ANALYSE_INTERVAL_MS); + streamManager.direction = 'sendrecv'; + streamManager.types.push('screencast'); + if(!this.isOutgoing) { + streamManager.locked = true; + streamManager.canCreateConferenceEntry = false; + } + + let mediaState: CallMediaState = { + '@type': 'MediaState', + type: 'input', + lowBattery: false, + muted: true, + screencastState: 'inactive', + videoRotation: 0, + videoState: 'inactive' + }; + + const self = this; + mediaState = new Proxy(mediaState, { + set: function(target, key, value) { + // @ts-ignore + target[key] = value; + self.setMediaState(mediaState); + self.sendMediaState(); + return true; + } + }); + + this.mediaStates = { + input: mediaState + }; + + this.sendMediaState = debounce(this._sendMediaState.bind(this), 0, false, true); + } + + get connectionState() { + const {_connectionState, connectionInstance} = this; + if(_connectionState !== undefined) { + return _connectionState; + } else if(!connectionInstance) { + return CALL_STATE.CONNECTING; + } else { + const {iceConnectionState} = connectionInstance.connection; + if(iceConnectionState === 'closed') { + return CALL_STATE.CLOSED; + } else if(iceConnectionState !== 'connected' && (!IS_SAFARI || iceConnectionState !== 'completed')) { + return CALL_STATE.CONNECTING; + } else { + return CALL_STATE.CONNECTED; + } + } + } + + public getVideoElement(type: CallMediaState['type']) { + if(type === 'input') return this.elements.get('main'); + else { + const mediaState = this.getMediaState('output'); + if(!mediaState) { + return; + } + + const type: WebRTCLineType = mediaState.videoState === 'active' ? 'video' : (mediaState.screencastState === 'active' ? 'screencast' : undefined); + if(!type) { + return; + } + + const entry = this.description.findEntry((entry) => entry.type === type); + if(!entry) { + return; + } + + return this.elements.get('' + entry.recvEntry.source); + } + } + + public async startScreenSharingInternal() { + try { + this.wasStartingScreen = true; + this.wasStartingVideo = false; + this.streamManager.types = ['audio', 'screencast']; + await this.requestScreen(); + } catch(err) { + this.log.error('startScreenSharing error', err); + } + } + + public async toggleScreenSharing() { + if(this.isSharingVideo) { + await this.stopVideoSharing(); + } + + if(this.isSharingScreen) { + return this.stopVideoSharing(); + } else { + return this.startScreenSharingInternal(); + } + } + + public async startVideoSharingInternal() { + try { + this.wasStartingScreen = false; + this.wasStartingVideo = true; + this.streamManager.types = ['audio', 'video']; + await this.requestInputSource(false, true, false); + } catch(err) { + this.log.error('startVideoSharing error', err); + } + } + + public async stopVideoSharing() { + const mediaState = this.getMediaState('input'); + mediaState.videoState = mediaState.screencastState = 'inactive'; + + const {streamManager, description} = this; + const track = streamManager.inputStream.getVideoTracks()[0]; + if(track) { + stopTrack(track); + streamManager.appendToConference(description); // clear sender track + } + } + + public async toggleVideoSharing() { + if(this.isSharingScreen) { + await this.stopVideoSharing(); + } + + if(this.isSharingVideo) { + return this.stopVideoSharing(); + } else { + return this.startVideoSharingInternal(); + } + } + + public getMediaState(type: CallMediaState['type']) { + return this.mediaStates[type]; + } + + public setMediaState(mediaState: CallMediaState) { + this.mediaStates[mediaState.type] = mediaState; + this.dispatchEvent('mediaState', mediaState); + } + + public isSharingVideoType(type: 'video' | 'screencast') { + try { + const hasVideoTrack = super.isSharingVideo; + return hasVideoTrack && !!((this.wasStartingScreen && type === 'screencast') || (this.wasStartingVideo && type === 'video')); + + // ! it will be used before the track appears + // return !!this.description.entries.find(entry => entry.type === type && entry.transceiver.sender.track.enabled); + } catch(err) { + return false; + } + } + + public get isSharingVideo() { + return this.isSharingVideoType('video'); + } + + public get isSharingScreen() { + return this.isSharingVideoType('screencast'); + } + + public get isMuted() { + const audioTrack = this.streamManager.inputStream.getAudioTracks()[0]; + return !audioTrack?.enabled; + } + + public get isClosing() { + const {connectionState} = this; + return connectionState === CALL_STATE.CLOSING || connectionState === CALL_STATE.CLOSED; + } + + public get streamManager(): StreamManager { + return this.connectionInstance?.streamManager; + } + + public get description(): localConferenceDescription { + return this.connectionInstance?.description; + } + + public setHangUpTimeout(timeout: number, reason: PhoneCallDiscardReason['_']) { + this.clearHangUpTimeout(); + this.hangUpTimeout = ctx.setTimeout(() => { + this.hangUpTimeout = undefined; + this.hangUp(reason); + }, timeout); + } + + public clearHangUpTimeout() { + if(this.hangUpTimeout !== undefined) { + clearTimeout(this.hangUpTimeout); + this.hangUpTimeout = undefined; + } + } + + public setPhoneCall(phoneCall: PhoneCall) { + this.call = phoneCall; + + const {id} = phoneCall; + if(this.id !== id) { + const prevId = this.id; + this.id = id; + this.dispatchEvent('id', id, prevId); + } + } + + public async acceptCall() { + // this.clearHangUpTimeout(); + this.overrideConnectionState(CALL_STATE.EXCHANGING_KEYS); + + const call = this.call as PhoneCall.phoneCallRequested; + this.requestInputSource(true, !!call.pFlags.video, false); + + const g_a_hash = call.g_a_hash; + this.appCallsManager.generateDh().then(dh => { + this.dh = { // ! it is correct + g_a_hash, + b: dh.a, + g_b: dh.g_a, + g_b_hash: dh.g_a_hash, + p: dh.p, + }; + + return this.apiManager.invokeApi('phone.acceptCall', { + peer: this.appCallsManager.getCallInput(this.id), + protocol: this.protocol, + g_b: this.dh.g_b + }); + }).then(phonePhoneCall => { + this.appCallsManager.savePhonePhoneCall(phonePhoneCall); + }); + } + + public joinCall() { + this.log('joinCall'); + + this.getEmojisFingerprint(); + + this.overrideConnectionState(); + + const {isOutgoing, encryptionKey, streamManager} = this; + + const configuration = getRtcConfiguration(this.call as PhoneCall.phoneCall); + this.log('joinCall configuration', configuration); + if(!configuration) return; + + const connectionInstance = this.connectionInstance = new CallConnectionInstance({ + call: this, + streamManager, + log: this.log.bindPrefix('connection'), + }); + + const connection = connectionInstance.createPeerConnection(configuration); + connection.addEventListener('iceconnectionstatechange', () => { + const state = this.connectionState; + if(this.connectedAt === undefined && state === CALL_STATE.CONNECTED) { + this.connectedAt = Date.now(); + } + + this.dispatchEvent('state', state); + }); + connection.addEventListener('negotiationneeded', () => { + connectionInstance.negotiate(); + }); + connection.addEventListener('icecandidate', (event) => { + const {candidate} = event; + connection.log('onicecandidate', candidate); + if(candidate?.candidate) { + this.sendIceCandidate(candidate); + } + }); + connection.addEventListener('track', (event) => { + const {track} = event; + connection.log('ontrack', track); + this.onTrack(event); + }); + + const description = connectionInstance.createDescription(); + + this.encryptor = new P2PEncryptor(isOutgoing, encryptionKey); + this.decryptor = new P2PEncryptor(!isOutgoing, encryptionKey); + + this.log('currentCall', this); + + if(isOutgoing) { + connectionInstance.appendStreamToConference(); + } + + this.createDataChannel(); + + this.processDecryptQueue(); + } + + private createDataChannelEntry() { + const dataChannelEntry = this.description.createEntry('application'); + dataChannelEntry.setDirection('sendrecv'); + dataChannelEntry.sendEntry = dataChannelEntry.recvEntry = dataChannelEntry; + } + + private createDataChannel() { + if(this.connectionInstance.dataChannel) { + return; + } + + const channel = this.connectionInstance.createDataChannel({ + id: 0, + negotiated: true + }); + channel.addEventListener('message', (e) => { + this.applyDataChannelData(JSON.parse(e.data)); + }); + channel.addEventListener('open', () => { + this.sendMediaState(); + }); + } + + private applyDataChannelData(data: CallMediaState) { + switch(data['@type']) { + case 'MediaState': { + data.type = 'output'; + this.log('got output media state', data); + this.setMediaState(data); + break; + } + + default: + this.log.error('unknown data channel data:', data); + break; + } + } + + private _sendMediaState() { + const {connectionInstance} = this; + if(!connectionInstance) return; + + const mediaState = {...this.getMediaState('input')}; + // mediaState.videoRotation = 90; + delete mediaState.type; + this.log('sendMediaState', mediaState); + + connectionInstance.sendDataChannelData(mediaState); + } + + public async sendCallSignalingData(data: CallSignalingData) { + /* if(data['@type'] === 'InitialSetup') { + this.filterNotVP8(data); + } */ + + const json = JSON.stringify(data); + const arr = new TextEncoder().encode(json); + const {bytes} = await this.encryptor.encryptRawPacket(arr); + + this.log('sendCallSignalingData', this.id, json); + await this.apiManager.invokeApi('phone.sendSignalingData', { + peer: this.appCallsManager.getCallInput(this.id), + data: bytes + }); + } + + public sendIceCandidate(iceCandidate: RTCIceCandidate) { + this.log('sendIceCandidate', iceCandidate); + const {candidate, sdpMLineIndex} = iceCandidate; + if(sdpMLineIndex !== 0) { + return; + } + + const parsed = p2pParseCandidate(candidate); + // const parsed = {sdpString: candidate}; + /* if(parsed.address.ip !== '') { + return; + } */ + + this.sendCallSignalingData({ + '@type': 'Candidates', + candidates: [parsed] + }); + } + + public async confirmCall() { + const {appCallsManager, apiManager, protocol, id, call} = this; + const dh = this.dh as DiffieHellmanInfo.a; + + // this.clearHangUpTimeout(); + this.overrideConnectionState(CALL_STATE.EXCHANGING_KEYS); + const {key, key_fingerprint} = await appCallsManager.computeKey((call as PhoneCall.phoneCallAccepted).g_b, dh.a, dh.p); + + const phonePhoneCall = await apiManager.invokeApi('phone.confirmCall', { + peer: appCallsManager.getCallInput(id), + protocol: protocol, + g_a: dh.g_a, + key_fingerprint: key_fingerprint + }); + + this.encryptionKey = key; + appCallsManager.savePhonePhoneCall(phonePhoneCall); + this.joinCall(); + } + + public getEmojisFingerprint() { + if(this.emojisFingerprint) return this.emojisFingerprint; + if(this.getEmojisFingerprintPromise) return this.getEmojisFingerprintPromise; + return this.getEmojisFingerprintPromise = this.apiManager.invokeCrypto('get-emojis-fingerprint', this.encryptionKey, this.dh.g_a).then(codePoints => { + this.getEmojisFingerprintPromise = undefined; + return this.emojisFingerprint = codePoints.map(codePoints => emojiFromCodePoints(codePoints)) as [string, string, string, string]; + }); + } + + private unlockStreamManager() { + this.connectionInstance.streamManager.locked = false; + this.connectionInstance.appendStreamToConference(); + } + + private async doTheMagic() { + this.connectionInstance.appendStreamToConference(); + + const connection = this.connectionInstance.connection; + + let answer = await connection.createAnswer(); + + this.log('[sdp] local', answer.type, answer.sdp); + await connection.setLocalDescription(answer); + + connection.getTransceivers().filter(transceiver => transceiver.direction === 'recvonly').forEach(transceiver => { + const entry = this.connectionInstance.description.getEntryByMid(transceiver.mid); + entry.transceiver = entry.recvEntry.transceiver = transceiver; + transceiver.direction = 'sendrecv'; + }); + + const isAnswer = false; + + const description = this.description; + let bundle = description.entries.map(entry => entry.mid); + const sdpDescription: RTCSessionDescriptionInit = { + type: isAnswer ? 'answer' : 'offer', + sdp: description.generateSdp({ + bundle, + entries: description.entries.filter(entry => bundle.includes(entry.mid)), + // isAnswer: isAnswer + isAnswer: !isAnswer + }) + }; + + await connection.setRemoteDescription(sdpDescription); + + answer = await connection.createAnswer(); + + await connection.setLocalDescription(answer); + + const initialSetup = parseSignalingData(parseSdp(answer.sdp)); + this.log('[InitialSetup] send 1'); + this.sendCallSignalingData(initialSetup); + + this.unlockStreamManager(); + } + + public overrideConnectionState(state?: CALL_STATE) { + this._connectionState = state; + this.dispatchEvent('state', this.connectionState); + } + + public get duration() { + return this.connectedAt !== undefined ? (Date.now() - this.connectedAt) / 1000 | 0 : 0; + } + + protected onInputStream(stream: MediaStream): void { + super.onInputStream(stream); + + const videoTrack = stream.getVideoTracks()[0]; + if(videoTrack) { + const state = this.getMediaState('input'); + + // handle starting camera + if(!this.wasStartingScreen && !this.wasStartingVideo) { + this.wasStartingVideo = true; + } + + if(this.isSharingVideo) { + state.videoState = 'active'; + } else if(this.isSharingScreen) { + state.screencastState = 'active'; + } + + videoTrack.addEventListener('ended', () => { + this.stopVideoSharing(); + }, {once: true}); + } + + if(stream.getAudioTracks().length) { + this.onMutedChange(); + } + } + + private onMutedChange() { + const isMuted = this.isMuted; + this.dispatchEvent('muted', isMuted); + + const state = this.getMediaState('input'); + state.muted = isMuted; + } + + public toggleMuted(): Promise { + return this.requestAudioSource(true).then(() => { + this.setMuted(); + this.onMutedChange(); + }); + } + + public async hangUp(discardReason?: PhoneCallDiscardReason['_'], discardedByOtherParty?: boolean) { + if(this.connectionState === CALL_STATE.CLOSED) { + return; + } + + this.discardReason = discardReason; + this.log('hangUp', discardReason); + this.overrideConnectionState(CALL_STATE.CLOSED); + + if(this.connectionInstance) { + this.connectionInstance.closeConnectionAndStream(true); + } + + if(discardReason && !discardedByOtherParty) { + let hasVideo = false; + for(const type in this.mediaStates) { + const mediaState = this.mediaStates[type as 'input' | 'output']; + hasVideo = mediaState.videoState === 'active' || mediaState.screencastState === 'active' || hasVideo; + } + + await this.appCallsManager.discardCall(this.id, this.duration, discardReason, hasVideo); + } + } + + private performCodec(_codec: P2PAudioCodec | P2PVideoCodec) { + const payloadTypes: AudioCodec['payload-types'] = _codec.payloadTypes.map(payloadType => { + return { + ...payloadType, + 'rtcp-fbs': payloadType.feedbackTypes + } + }); + + const codec: AudioCodec = { + 'rtp-hdrexts': _codec.rtpExtensions, + 'payload-types': payloadTypes + }; + + return codec; + } + + private setDataToDescription(data: CallSignalingData.initialSetup) { + this.description.setData({ + transport: { + pwd: data.pwd, + ufrag: data.ufrag, + fingerprints: data.fingerprints, + 'rtcp-mux': true + }, + audio: this.performCodec(data.audio), + video: data.video ? this.performCodec(data.video) as VideoCodec : undefined, + screencast: data.screencast ? this.performCodec(data.screencast) as VideoCodec : undefined + }); + } + + private filterNotVP8(initialSetup: CallSignalingData.initialSetup) { + if(!this.isOutgoing) { // only VP8 works now + [initialSetup.video, initialSetup.screencast].filter(Boolean).forEach(codec => { + const payloadTypes = codec.payloadTypes; + const idx = payloadTypes.findIndex(payloadType => payloadType.name === 'VP8'); + const vp8PayloadType = payloadTypes[idx]; + const rtxIdx = payloadTypes.findIndex(payloadType => +payloadType.parameters?.apt === vp8PayloadType.id); + codec.payloadTypes = [payloadTypes[idx], payloadTypes[rtxIdx]]; + }); + } + } + + public async applyCallSignalingData(data: CallSignalingData) { + this.log('applyCallSignalingData', this, data); + + const {connection, description} = this.connectionInstance; + + switch(data['@type']) { + case 'InitialSetup': { + this.log('[sdp] InitialSetup', data); + + this.filterNotVP8(data); + this.setDataToDescription(data); + + const performSsrcGroups = (ssrcGroups: P2PVideoCodec['ssrcGroups']): GroupCallParticipantVideoSourceGroup[] => { + return ssrcGroups.map(ssrcGroup => { + return { + _: 'groupCallParticipantVideoSourceGroup', + semantics: ssrcGroup.semantics, + sources: ssrcGroup.ssrcs.map(source => +source) + }; + }); + }; + + const ssrcs = [ + generateSsrc('audio', +data.audio.ssrc), + data.video ? generateSsrc('video', performSsrcGroups(data.video.ssrcGroups)) : undefined, + data.screencast ? generateSsrc('screencast', performSsrcGroups(data.screencast.ssrcGroups)) : undefined + ].filter(Boolean); + + ssrcs.forEach(ssrc => { + let entry = description.getEntryBySource(ssrc.source); + if(entry) { + return; + } + + const sendRecvEntry = description.findFreeSendRecvEntry(ssrc.type, false); + entry = new ConferenceEntry(sendRecvEntry.mid, ssrc.type); + entry.setDirection('sendrecv'); + sendRecvEntry.recvEntry = entry; + + description.setEntrySource(entry, ssrc.sourceGroups || ssrc.source); + }); + + this.createDataChannelEntry(); + + const isAnswer = this.offerSent; + this.offerSent = false; + + let bundle = description.entries.map(entry => entry.mid); + const sdpDescription: RTCSessionDescriptionInit = { + type: isAnswer ? 'answer' : 'offer', + sdp: description.generateSdp({ + bundle, + entries: description.entries.filter(entry => bundle.includes(entry.mid)), + // isAnswer: isAnswer + isAnswer: !isAnswer + }) + }; + + this.log('[sdp] remote', sdpDescription.sdp); + + await connection.setRemoteDescription(sdpDescription); + + await this.tryToReleaseCandidates(); + + if(!isAnswer) { + await this.doTheMagic(); + } + + break; + } + + case 'Candidates': { + for(const candidate of data.candidates) { + const init: RTCIceCandidateInit = P2PSdpBuilder.generateCandidate(candidate); + init.sdpMLineIndex = 0; + const iceCandidate = new RTCIceCandidate(init); + this.candidates.push(iceCandidate); + } + + await this.tryToReleaseCandidates(); + break; + } + + default: { + this.log.error('unrecognized signaling data', data); + } + } + } + + public async tryToReleaseCandidates() { + const {connectionInstance} = this; + if(!connectionInstance) { + return; + } + + const {connection} = connectionInstance; + if(connection.remoteDescription) { + const promises: Promise[] = this.candidates.map(candidate => this.addIceCandidate(connection, candidate)); + this.candidates.length = 0; + + await Promise.all(promises); + } else { + this.log('[candidates] postpone'); + } + } + + private async addIceCandidate(connection: RTCPeerConnection, candidate: RTCIceCandidate) { + this.log('[candidate] start', candidate); + try { + // if(!candidate.address) return; + await connection.addIceCandidate(candidate); + this.log('[candidate] add', candidate); + } catch(e) { + this.log.error('[candidate] error', candidate, e); + } + } + + private async processDecryptQueue() { + const {encryptor} = this; + if(!encryptor) { + this.log.warn('got encrypted signaling data before the encryption key'); + return; + } + + const length = this.decryptQueue.length; + if(!length) { + return; + } + + const queue = this.decryptQueue.slice(); + this.decryptQueue.length = 0; + + for(const data of queue) { + const decryptedData = await encryptor.decryptRawPacket(data); + if(!decryptedData) { + continue; + } + + // this.log('[update] updateNewCallSignalingData', update, decryptedData); + + const str = new TextDecoder().decode(decryptedData); + try { + const signalingData: CallSignalingData = JSON.parse(str); + this.log('[update] updateNewCallSignalingData', signalingData); + this.applyCallSignalingData(signalingData); + } catch(err) { + this.log.error('wrong signaling data', str); + this.hangUp('phoneCallDiscardReasonDisconnect'); + } + } + } + + public onUpdatePhoneCallSignalingData(update: Update.updatePhoneCallSignalingData) { + this.decryptQueue.push(update.data); + this.processDecryptQueue(); + } +} diff --git a/src/lib/calls/callInstanceBase.ts b/src/lib/calls/callInstanceBase.ts index 880e570ea..bf9cbf366 100644 --- a/src/lib/calls/callInstanceBase.ts +++ b/src/lib/calls/callInstanceBase.ts @@ -8,6 +8,7 @@ import EventListenerBase, { EventListenerListeners } from "../../helpers/eventLi import noop from "../../helpers/noop"; import { logger } from "../logger"; import getAudioConstraints from "./helpers/getAudioConstraints"; +import getScreenConstraints from "./helpers/getScreenConstraints"; import getStreamCached from "./helpers/getStreamCached"; import getVideoConstraints from "./helpers/getVideoConstraints"; import LocalConferenceDescription from "./localConferenceDescription"; @@ -93,11 +94,16 @@ export default abstract class CallInstanceBase return this.getStream({ constraints, muted - }).then(stream => { - if(stream.getVideoTracks().length) { - this.saveInputVideoStream(stream, 'main'); - } - + }).then((stream) => { + this.onInputStream(stream); + }); + } + + public requestScreen() { + return this.getStream({ + isScreen: true, + constraints: getScreenConstraints(true) + }).then((stream) => { this.onInputStream(stream); }); } @@ -211,6 +217,11 @@ export default abstract class CallInstanceBase protected onInputStream(stream: MediaStream): void { if(!this.isClosing) { + const videoTracks = stream.getVideoTracks(); + if(videoTracks.length) { + this.saveInputVideoStream(stream, 'main'); + } + const {streamManager, description} = this; streamManager.addStream(stream, 'input'); diff --git a/src/lib/calls/groupCallConnectionInstance.ts b/src/lib/calls/groupCallConnectionInstance.ts index 7ddd6c3a9..956f1ed3b 100644 --- a/src/lib/calls/groupCallConnectionInstance.ts +++ b/src/lib/calls/groupCallConnectionInstance.ts @@ -4,12 +4,12 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import { forEachReverse } from "../../helpers/array"; +import forEachReverse from "../../helpers/array/forEachReverse"; import throttle from "../../helpers/schedulers/throttle"; import { Updates, PhoneJoinGroupCall, PhoneJoinGroupCallPresentation, Update } from "../../layer"; import apiUpdatesManager from "../appManagers/apiUpdatesManager"; import appGroupCallsManager, { GroupCallConnectionType, JoinGroupCallJsonPayload } from "../appManagers/appGroupCallsManager"; -import apiManager from "../mtproto/apiManager"; +import apiManager from "../mtproto/mtprotoworker"; import rootScope from "../rootScope"; import CallConnectionInstanceBase, { CallConnectionInstanceOptions } from "./callConnectionInstanceBase"; import GroupCallInstance from "./groupCallInstance"; diff --git a/src/lib/calls/groupCallInstance.ts b/src/lib/calls/groupCallInstance.ts index b631e8171..1956c2e1f 100644 --- a/src/lib/calls/groupCallInstance.ts +++ b/src/lib/calls/groupCallInstance.ts @@ -5,15 +5,15 @@ */ import { IS_SAFARI } from "../../environment/userAgent"; -import { indexOfAndSplice } from "../../helpers/array"; -import { safeAssign } from "../../helpers/object"; +import indexOfAndSplice from "../../helpers/array/indexOfAndSplice"; +import safeAssign from "../../helpers/object/safeAssign"; import throttle from "../../helpers/schedulers/throttle"; import { GroupCall, GroupCallParticipant, Updates } from "../../layer"; import apiUpdatesManager from "../appManagers/apiUpdatesManager"; import appGroupCallsManager, { GroupCallConnectionType, GroupCallId, GroupCallOutputSource } from "../appManagers/appGroupCallsManager"; import appPeersManager from "../appManagers/appPeersManager"; import { logger } from "../logger"; -import apiManager from "../mtproto/apiManager"; +import apiManager from "../mtproto/mtprotoworker"; import { NULL_PEER_ID } from "../mtproto/mtproto_config"; import rootScope from "../rootScope"; import CallInstanceBase, { TryAddTrackOptions } from "./callInstanceBase"; diff --git a/src/lib/calls/helpers/filterServerCodecs.ts b/src/lib/calls/helpers/filterServerCodecs.ts index 84b76a24f..f42b33903 100644 --- a/src/lib/calls/helpers/filterServerCodecs.ts +++ b/src/lib/calls/helpers/filterServerCodecs.ts @@ -4,7 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import { forEachReverse } from "../../../helpers/array"; +import forEachReverse from "../../../helpers/array/forEachReverse"; import SDPMediaSection from "../sdp/mediaSection"; import { UpdateGroupCallConnectionData, Codec } from "../types"; diff --git a/src/lib/calls/helpers/fixLocalOffer.ts b/src/lib/calls/helpers/fixLocalOffer.ts index 0df2097e8..2ad9941d3 100644 --- a/src/lib/calls/helpers/fixLocalOffer.ts +++ b/src/lib/calls/helpers/fixLocalOffer.ts @@ -4,8 +4,8 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import { forEachReverse } from "../../../helpers/array"; -import { copy } from "../../../helpers/object"; +import forEachReverse from "../../../helpers/array/forEachReverse"; +import copy from "../../../helpers/object/copy"; import { ConferenceEntry } from "../localConferenceDescription"; import { parseSdp, addSimulcast } from "../sdp/utils"; import { generateMediaFirstLine, SDPBuilder } from "../sdpBuilder"; diff --git a/src/lib/calls/helpers/getEmojisFingerprint.ts b/src/lib/calls/helpers/getEmojisFingerprint.ts new file mode 100644 index 000000000..0caeafd91 --- /dev/null +++ b/src/lib/calls/helpers/getEmojisFingerprint.ts @@ -0,0 +1,95 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import cryptoWorker from '../../crypto/cryptoworker'; +import bigInt from 'big-integer'; + +function readBigIntFromBytesBE(bytes: Uint8Array) { + const length = bytes.length; + const bits = length * 8; + let value = bigInt(bytes[0]).and(0x7F).shiftLeft(bits - 8); + for(let i = 1; i < length; ++i) { + const _bits = bits - (i + 1) * 8; + const b = bigInt(bytes[i]); + value = value.or(_bits ? b.shiftLeft(_bits) : b); + } + + return value; +} + +// Emojis were taken from tdlib +const emojis = [ + '1f609', '1f60d', '1f61b', '1f62d', '1f631', '1f621', '1f60e', + '1f634', '1f635', '1f608', '1f62c', '1f607', '1f60f', '1f46e', + '1f477', '1f482', '1f476', '1f468', '1f469', '1f474', '1f475', + '1f63b', '1f63d', '1f640', '1f47a', '1f648', '1f649', '1f64a', + '1f480', '1f47d', '1f4a9', '1f525', '1f4a5', '1f4a4', '1f442', + '1f440', '1f443', '1f445', '1f444', '1f44d', '1f44e', '1f44c', + '1f44a', '270c', '270b', '1f450', '1f446', '1f447', '1f449', + '1f448', '1f64f', '1f44f', '1f4aa', '1f6b6', '1f3c3', '1f483', + '1f46b', '1f46a', '1f46c', '1f46d', '1f485', '1f3a9', '1f451', + '1f452', '1f45f', '1f45e', '1f460', '1f455', '1f457', '1f456', + '1f459', '1f45c', '1f453', '1f380', '1f484', '1f49b', '1f499', + '1f49c', '1f49a', '1f48d', '1f48e', '1f436', '1f43a', '1f431', + '1f42d', '1f439', '1f430', '1f438', '1f42f', '1f428', '1f43b', + '1f437', '1f42e', '1f417', '1f434', '1f411', '1f418', '1f43c', + '1f427', '1f425', '1f414', '1f40d', '1f422', '1f41b', '1f41d', + '1f41c', '1f41e', '1f40c', '1f419', '1f41a', '1f41f', '1f42c', + '1f40b', '1f410', '1f40a', '1f42b', '1f340', '1f339', '1f33b', + '1f341', '1f33e', '1f344', '1f335', '1f334', '1f333', '1f31e', + '1f31a', '1f319', '1f30e', '1f30b', '26a1', '2614', '2744', '26c4', + '1f300', '1f308', '1f30a', '1f393', '1f386', '1f383', '1f47b', + '1f385', '1f384', '1f381', '1f388', '1f52e', '1f3a5', '1f4f7', + '1f4bf', '1f4bb', '260e', '1f4e1', '1f4fa', '1f4fb', '1f509', + '1f514', '23f3', '23f0', '231a', '1f512', '1f511', '1f50e', + '1f4a1', '1f526', '1f50c', '1f50b', '1f6bf', '1f6bd', '1f527', + '1f528', '1f6aa', '1f6ac', '1f4a3', '1f52b', '1f52a', '1f48a', + '1f489', '1f4b0', '1f4b5', '1f4b3', '2709', '1f4eb', '1f4e6', + '1f4c5', '1f4c1', '2702', '1f4cc', '1f4ce', '2712', '270f', + '1f4d0', '1f4da', '1f52c', '1f52d', '1f3a8', '1f3ac', '1f3a4', + '1f3a7', '1f3b5', '1f3b9', '1f3bb', '1f3ba', '1f3b8', '1f47e', + '1f3ae', '1f0cf', '1f3b2', '1f3af', '1f3c8', '1f3c0', '26bd', + '26be', '1f3be', '1f3b1', '1f3c9', '1f3b3', '1f3c1', '1f3c7', + '1f3c6', '1f3ca', '1f3c4', '2615', '1f37c', '1f37a', '1f377', + '1f374', '1f355', '1f354', '1f35f', '1f357', '1f371', '1f35a', + '1f35c', '1f361', '1f373', '1f35e', '1f369', '1f366', '1f382', + '1f370', '1f36a', '1f36b', '1f36d', '1f36f', '1f34e', '1f34f', + '1f34a', '1f34b', '1f352', '1f347', '1f349', '1f353', '1f351', + '1f34c', '1f350', '1f34d', '1f346', '1f345', '1f33d', '1f3e1', + '1f3e5', '1f3e6', '26ea', '1f3f0', '26fa', '1f3ed', '1f5fb', + '1f5fd', '1f3a0', '1f3a1', '26f2', '1f3a2', '1f6a2', '1f6a4', + '2693', '1f680', '2708', '1f681', '1f682', '1f68b', '1f68e', + '1f68c', '1f699', '1f697', '1f695', '1f69b', '1f6a8', '1f694', + '1f692', '1f691', '1f6b2', '1f6a0', '1f69c', '1f6a6', '26a0', + '1f6a7', '26fd', '1f3b0', '1f5ff', '1f3aa', '1f3ad', + '1f1ef-1f1f5', '1f1f0-1f1f7', '1f1e9-1f1ea', '1f1e8-1f1f3', + '1f1fa-1f1f8', '1f1eb-1f1f7', '1f1ea-1f1f8', '1f1ee-1f1f9', + '1f1f7-1f1fa', '1f1ec-1f1e7', '0031-20e3', '0032-20e3', '0033-20e3', + '0034-20e3', '0035-20e3', '0036-20e3', '0037-20e3', '0038-20e3', '0039-20e3', + '0030-20e3', '1f51f', '2757', '2753', '2665', '2666', '1f4af', '1f517', + '1f531', '1f534', '1f535', '1f536', '1f537' +]; + +export default async function getEmojisFingerprint(key: Uint8Array, g_a: Uint8Array) { + const arr = key.concat(g_a); + const hash = await cryptoWorker.invokeCrypto('sha256', arr); + + const result: [string, string, string, string] = [] as any; + const emojisLength = emojis.length; + + const kPartSize = 8; + for(let partOffset = 0; partOffset != hash.length; partOffset += kPartSize) { + const bytes = hash.slice(partOffset, partOffset + kPartSize); + const value = readBigIntFromBytesBE(bytes); + const index = value.mod(emojisLength).toJSNumber(); + + // const emoji = emojiFromCodePoints(emojis[index]); + const codePoints = emojis[index]; + result.push(codePoints); + } + + return result; +} diff --git a/src/lib/calls/helpers/getScreenConstraints.ts b/src/lib/calls/helpers/getScreenConstraints.ts index 8f04ddeca..380b31426 100644 --- a/src/lib/calls/helpers/getScreenConstraints.ts +++ b/src/lib/calls/helpers/getScreenConstraints.ts @@ -1,12 +1,17 @@ -export default function getScreenConstraints(): DisplayMediaStreamConstraints { - return { +export default function getScreenConstraints(skipAudio?: boolean) { + const constraints: DisplayMediaStreamConstraints = { video: { // @ts-ignore // cursor: 'always', width: {max: 1920}, height: {max: 1080}, frameRate: {max: 30} - }, - audio: true + } }; + + if(!skipAudio) { + constraints.audio = true; + } + + return constraints; } diff --git a/src/lib/calls/helpers/parseSignalingData.ts b/src/lib/calls/helpers/parseSignalingData.ts new file mode 100644 index 000000000..6d83c4ef4 --- /dev/null +++ b/src/lib/calls/helpers/parseSignalingData.ts @@ -0,0 +1,103 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import SDP from "../sdp"; +import { CallSignalingData, P2PVideoCodec } from "../types"; +import parseMediaSectionInfo from "./parseMediaSectionInfo"; + +export default function parseSignalingData(sdp: SDP) { + const info = parseMediaSectionInfo(sdp, sdp.media[0]); + + const data: CallSignalingData.initialSetup = { + '@type': 'InitialSetup', + fingerprints: [info.fingerprint], + ufrag: info.ufrag, + pwd: info.pwd, + audio: undefined, + video: undefined, + screencast: undefined + }; + + const convertNumber = (number: number) => '' + number; + + for(const section of sdp.media) { + const mediaType = section.mediaType; + if(mediaType === 'application' || !section.isSending) { + continue; + } + + const codec: P2PVideoCodec = data[mediaType === 'video' && data['video'] ? 'screencast' : mediaType] = {} as any; + const info = parseMediaSectionInfo(sdp, section); + codec.ssrc = convertNumber(info.source); + + if(info.sourceGroups) { + codec.ssrcGroups = info.sourceGroups.map(sourceGroup => ({semantics: sourceGroup.semantics, ssrcs: sourceGroup.sources.map(convertNumber)})); + } + + const rtpExtensions: P2PVideoCodec['rtpExtensions'] = codec.rtpExtensions = []; + section.attributes.get('extmap').forEach((attribute) => { + rtpExtensions.push({ + id: +attribute.key, + uri: attribute.value + }); + }); + + const payloadTypesMap: Map = new Map(); + + const getPayloadType = (id: number) => { + let payloadType = payloadTypesMap.get(id); + if(!payloadType) { + payloadTypesMap.set(id, payloadType = { + id + } as any); + } + + return payloadType; + }; + + section.attributes.get('rtpmap').forEach((attribute) => { + const id = +attribute.key; + const payloadType = getPayloadType(id); + const splitted = attribute.value.split('/'); + const [name, clockrate, channels] = splitted; + payloadType.name = name; + payloadType.clockrate = +clockrate; + payloadType.channels = channels ? +channels : 0; + }); + + section.attributes.get('rtcp-fb').forEach((attribute) => { + const id = +attribute.key; + const payloadType = getPayloadType(id); + payloadType.feedbackTypes = attribute.lines.map((line) => { + const splitted = line.split(' '); + const [type, subtype] = splitted; + return { + type, + subtype: subtype || '' + }; + }); + }); + + section.attributes.get('fmtp').forEach((attribute) => { + const id = +attribute.key; + const payloadType = getPayloadType(id); + const parameters: P2PVideoCodec['payloadTypes'][0]['parameters'] = payloadType.parameters = {}; + const splitted = attribute.value.split(';'); + for(const str of splitted) { + const [key, value] = str.split('='); + parameters[key] = value; + } + }); + + codec.payloadTypes = Array.from(payloadTypesMap.values()); + + /* if(codec.payloadTypes.length > 5) { + codec.payloadTypes.length = Math.min(codec.payloadTypes.length, 5); + } */ + } + + return data; +} diff --git a/src/lib/calls/localConferenceDescription.ts b/src/lib/calls/localConferenceDescription.ts index 2a13a6fc7..3c9f1d1d9 100644 --- a/src/lib/calls/localConferenceDescription.ts +++ b/src/lib/calls/localConferenceDescription.ts @@ -9,10 +9,10 @@ * https://github.com/evgeny-nadymov/telegram-react/blob/master/LICENSE */ -import { indexOfAndSplice } from '../../helpers/array'; -import { safeAssign } from '../../helpers/object'; +import indexOfAndSplice from '../../helpers/array/indexOfAndSplice'; +import safeAssign from '../../helpers/object/safeAssign'; import { GroupCallParticipantVideoSourceGroup } from '../../layer'; -import { SDPBuilder, WebRTCLineType, WEBRTC_MEDIA_PORT } from './sdpBuilder'; +import { fixMediaLineType, SDPBuilder, WebRTCLineType, WEBRTC_MEDIA_PORT } from './sdpBuilder'; import { AudioCodec, GroupCallConnectionTransport, Ssrc, UpdateGroupCallConnectionData, VideoCodec } from './types'; export class ConferenceEntry { @@ -57,7 +57,7 @@ export class ConferenceEntry { this.setDirection(init.direction); } - return this.transceiver = connection.addTransceiver(this.type, init); + return this.transceiver = connection.addTransceiver(fixMediaLineType(this.type), init); } public setSource(source: number | GroupCallParticipantVideoSourceGroup[]) { @@ -99,6 +99,7 @@ export default class LocalConferenceDescription implements UpdateGroupCallConnec public readonly transport: GroupCallConnectionTransport; public readonly audio?: AudioCodec; public readonly video: VideoCodec; + public readonly screencast?: VideoCodec; private maxSeenId: number; public readonly entries: ConferenceEntry[]; diff --git a/src/lib/calls/p2P/getCallProtocol.ts b/src/lib/calls/p2P/getCallProtocol.ts new file mode 100644 index 000000000..84e88bf4b --- /dev/null +++ b/src/lib/calls/p2P/getCallProtocol.ts @@ -0,0 +1,25 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + * + * Originally from: + * https://github.com/evgeny-nadymov/telegram-react + * Copyright (C) 2018 Evgeny Nadymov + * https://github.com/evgeny-nadymov/telegram-react/blob/master/LICENSE + */ + +import { PhoneCallProtocol } from "../../../layer"; + +export default function getCallProtocol(): PhoneCallProtocol { + return { + _: 'phoneCallProtocol', + pFlags: { + udp_p2p: true, + udp_reflector: true + }, + min_layer: 92, + max_layer: 92, + library_versions: ['4.0.0'] + }; +} diff --git a/src/lib/calls/p2P/getRtcConfiguration.ts b/src/lib/calls/p2P/getRtcConfiguration.ts new file mode 100644 index 000000000..b611bbfeb --- /dev/null +++ b/src/lib/calls/p2P/getRtcConfiguration.ts @@ -0,0 +1,56 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + * + * Originally from: + * https://github.com/evgeny-nadymov/telegram-react + * Copyright (C) 2018 Evgeny Nadymov + * https://github.com/evgeny-nadymov/telegram-react/blob/master/LICENSE + */ + +import { PhoneCall } from "../../../layer"; + +export default function getRtcConfiguration(call: PhoneCall.phoneCall): RTCConfiguration { + const iceServers: RTCIceServer[] = []; + call.connections.forEach((connection) => { + switch(connection._) { + /* case 'callServerTypeTelegramReflector': { + break; + } */ + case 'phoneConnectionWebrtc': { + const {ip, ipv6, port, username, password} = connection; + const urls: string[] = []; + if(connection.pFlags.turn) { + if(ip) { + urls.push(`turn:${ip}:${port}`); + } + if(ipv6) { + urls.push(`turn:[${ipv6}]:${port}`); + } + } else if(connection.pFlags.stun) { + if(ip) { + urls.push(`stun:${ip}:${port}`); + } + if(ipv6) { + urls.push(`stun:[${ipv6}]:${port}`); + } + } + + if(urls.length > 0) { + iceServers.push({ + urls, + username, + credential: password + }); + } + break; + } + } + }); + + return { + iceServers, + iceTransportPolicy: call.pFlags.p2p_allowed ? 'all' : 'relay' + }; +} diff --git a/src/lib/calls/p2P/p2PEncryptor.js b/src/lib/calls/p2P/p2PEncryptor.js deleted file mode 100644 index db54e847c..000000000 --- a/src/lib/calls/p2P/p2PEncryptor.js +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright (c) 2018-present, Evgeny Nadymov - * - * This source code is licensed under the GPL v.3.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -import CryptoJS from 'crypto-js'; - -const P2P_ENCRYPTION = true; - -const kMaxIncomingPacketSize = 128 * 1024 * 1024; - -function uint8ArrayToWordArray(u8arr) { - let len = u8arr.length; - let words = []; - for (let i = 0; i < len; i++) { - words[i >>> 2] |= (u8arr[i] & 0xff) << (24 - (i % 4) * 8); - } - - return CryptoJS.lib.WordArray.create(words, len); -} - -function wordArrayToUint8Array(wordArray) { - const l = wordArray.sigBytes; - const words = wordArray.words; - const result = new Uint8Array(l); - - let i = 0 /*dst*/, j = 0 /*src*/; - while(true) { - // here i is a multiple of 4 - if (i===l) - break; - let w = words[j++]; - result[i++] = (w & 0xff000000) >>> 24; - if (i===l) - break; - result[i++] = (w & 0x00ff0000) >>> 16; - if (i===l) - break; - result[i++] = (w & 0x0000ff00) >>> 8; - if (i===l) - break; - result[i++] = (w & 0x000000ff); - } - - return result; -} - -export default class P2PEncryptor { - constructor(isOutgoing, keyBase64) { - this.keyBase64 = keyBase64; - this.isOutgoing = isOutgoing; - this.type = 'Signaling'; - this.counter = 0; - this.seqMap = new Map(); - - const p2pKeyWA = CryptoJS.enc.Base64.parse(keyBase64); - this.p2pKey = wordArrayToUint8Array(p2pKeyWA); - this.mode = CryptoJS.mode.CTR; - this.padding = CryptoJS.pad.NoPadding; - } - - encryptToBase64(str) { - if (P2P_ENCRYPTION) { - const enc = new TextEncoder(); - const arr = enc.encode(str); - - const packet = this.encryptRawPacket(new Uint8Array(arr)); - - const { bytes } = packet; - const wa = uint8ArrayToWordArray(bytes); - - return CryptoJS.enc.Base64.stringify(wa); - } else { - return btoa(str); - } - } - - decryptFromBase64(base64) { - if (P2P_ENCRYPTION) { - const wa = CryptoJS.enc.Base64.parse(base64); - - const buffer = wordArrayToUint8Array(wa); - const decrypted = this.decryptRawPacket(buffer); - - const dec = new TextDecoder('utf-8'); - return dec.decode(decrypted); - } else { - return atob(base64); - } - } - - concatSHA256(parts) { - const sha256 = CryptoJS.algo.SHA256.create(); - for (let i = 0; i < parts.length; i++) { - const str = uint8ArrayToWordArray(parts[i]); - sha256.update(str); - } - - const result = sha256.finalize(); - - return wordArrayToUint8Array(result); - } - - encryptPrepared(buffer) { - const result = { - counter: 0, //this.counterFromSeq(this.readSeq(buffer)), - bytes: new Uint8Array(16 + buffer.length) - } - - const x = (this.isOutgoing ? 0 : 8) + (this.type === 'Signaling' ? 128 : 0); - const key = this.p2pKey; - - const msgKeyLarge = this.concatSHA256([key.subarray(x + 88, x + 88 + 32), buffer]); - const msgKey = result.bytes; - for (let i = 0; i < 16; i++) { - msgKey[i] = msgKeyLarge[i + 8]; - } - - const aesKeyIv = this.prepareAesKeyIv(key, msgKey, x); - - const bytes = this.aesProcessCtr(buffer, buffer.length, aesKeyIv, true); - - result.bytes = new Uint8Array([...result.bytes.subarray(0, 16), ...bytes]); - - return result; - } - - encryptObjToBase64(obj) { - const str = JSON.stringify(obj); - - const enc = new TextEncoder(); - const arr = enc.encode(str); - - const packet = this.encryptRawPacket(new Uint8Array(arr)); - - const { bytes } = packet; - const wa = uint8ArrayToWordArray(bytes); - - return CryptoJS.enc.Base64.stringify(wa); - } - - encryptRawPacket(buffer) { - const seq = ++this.counter; - const arr = new ArrayBuffer(4); - const view = new DataView(arr); - view.setUint32(0, seq >>> 0, false); // byteOffset = 0; litteEndian = false - - const result = new Uint8Array([...new Uint8Array(arr), ...buffer]); - - return this.encryptPrepared(result); - } - - prepareAesKeyIv(key, msgKey, x) { - const sha256a = this.concatSHA256([ - msgKey.subarray(0, 16), - key.subarray(x, x + 36) - ]); - - const sha256b = this.concatSHA256([ - key.subarray(40 + x, 40 + x + 36), - msgKey.subarray(0, 16) - ]); - - return { - key: new Uint8Array([ - ...sha256a.subarray(0, 8), - ...sha256b.subarray(8, 8 + 16), - ...sha256a.subarray(24, 24 + 8) - ]), - iv: new Uint8Array([ - ...sha256b.subarray(0, 4), - ...sha256a.subarray(8, 8 + 8), - ...sha256b.subarray(24, 24 + 4) - ]) - }; - } - - aesProcessCtr(encryptedData, dataSize, aesKeyIv, encrypt = true) { - const key = uint8ArrayToWordArray(aesKeyIv.key); - const iv = uint8ArrayToWordArray(aesKeyIv.iv); - const str = uint8ArrayToWordArray(encryptedData); - - const { mode, padding } = this; - - if (encrypt) { - const encrypted = CryptoJS.AES.encrypt(str, key, { - mode, - iv, - padding - }); - - return wordArrayToUint8Array(encrypted.ciphertext); - } else { - const decrypted = CryptoJS.AES.decrypt({ ciphertext: str }, key, { - mode, - iv, - padding - }); - - return wordArrayToUint8Array(decrypted); - } - } - - decryptObjFromBase64(base64) { - const wa = CryptoJS.enc.Base64.parse(base64); - - const buffer = wordArrayToUint8Array(wa); - const decrypted = this.decryptRawPacket(buffer); - - const dec = new TextDecoder('utf-8'); - return JSON.parse(dec.decode(decrypted)) - } - - constTimeIsDifferent(a, b, count) { - let msgKeyEquals = true; - for (let i = 0; i < count; i++) { - if (a[i] !== b[i]) { - msgKeyEquals = false; - } - } - - return !msgKeyEquals; - } - - decryptRawPacket(buffer) { - if (buffer.length < 21 || buffer.length > kMaxIncomingPacketSize) { - return null; - } - - const { isOutgoing, type } = this; - - const x = (isOutgoing ? 8 : 0) + (type === 'Signaling' ? 128 : 0); - const key = this.p2pKey; - - const msgKey = buffer.subarray(0, 16); - const encryptedData = buffer.subarray(16); - const encryptedDataSize = buffer.length - 16; - - const aesKeyIv = this.prepareAesKeyIv(key, msgKey, x); - - const decryptionBuffer = this.aesProcessCtr(encryptedData, encryptedDataSize, aesKeyIv, false); - - const msgKeyLarge = this.concatSHA256([ - key.subarray(88 + x, 88 + x + 32), - decryptionBuffer - ]); - - if (this.constTimeIsDifferent(msgKeyLarge.subarray(8), msgKey, 16)) { - return null; - } - - const dataView = new DataView(decryptionBuffer.buffer); - const seq = dataView.getUint32(0); - if (this.seqMap.has(seq)) { - return null; - } - this.seqMap.set(seq, seq); - - return decryptionBuffer.slice(4); - } -}; \ No newline at end of file diff --git a/src/lib/calls/p2P/p2PEncryptor.ts b/src/lib/calls/p2P/p2PEncryptor.ts new file mode 100644 index 000000000..f55f85783 --- /dev/null +++ b/src/lib/calls/p2P/p2PEncryptor.ts @@ -0,0 +1,163 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + * + * Originally from: + * https://github.com/evgeny-nadymov/telegram-react + * Copyright (C) 2018 Evgeny Nadymov + * https://github.com/evgeny-nadymov/telegram-react/blob/master/LICENSE + */ + +import bufferConcats from '../../../helpers/bytes/bufferConcats'; +import subtle from '../../crypto/subtle'; +import sha256 from '../../crypto/utils/sha256'; + +const kMaxIncomingPacketSize = 128 * 1024 * 1024; + +export default class P2PEncryptor { + private type: 'Signaling'; + private counter: number; + private seqMap: Map; + + constructor(private isOutgoing: boolean, private p2pKey: Uint8Array) { + this.type = 'Signaling'; + this.counter = 0; + this.seqMap = new Map(); + } + + private concatSHA256(parts: Uint8Array[]) { + return sha256(bufferConcats(...parts)); + } + + private async encryptPrepared(buffer: Uint8Array) { + const result = { + counter: 0, //this.counterFromSeq(this.readSeq(buffer)), + bytes: new Uint8Array(16 + buffer.length) + }; + + const x = (this.isOutgoing ? 0 : 8) + (this.type === 'Signaling' ? 128 : 0); + const key = this.p2pKey; + + const msgKeyLarge = await this.concatSHA256([key.subarray(x + 88, x + 88 + 32), buffer]); + const msgKey = result.bytes; + for(let i = 0; i < 16; ++i) { + msgKey[i] = msgKeyLarge[i + 8]; + } + + const aesKeyIv = await this.prepareAesKeyIv(key, msgKey, x); + + const bytes = await this.aesProcessCtr(buffer, buffer.length, aesKeyIv, true); + + result.bytes = new Uint8Array([...result.bytes.subarray(0, 16), ...bytes]); + + return result; + } + + public encryptRawPacket(buffer: Uint8Array) { + const seq = ++this.counter; + const arr = new ArrayBuffer(4); + const view = new DataView(arr); + view.setUint32(0, seq >>> 0, false); // byteOffset = 0; litteEndian = false + + const result = new Uint8Array([...new Uint8Array(arr), ...buffer]); + + return this.encryptPrepared(result); + } + + private async prepareAesKeyIv(key: Uint8Array, msgKey: Uint8Array, x: number) { + const [sha256a, sha256b] = await Promise.all([ + this.concatSHA256([ + msgKey.subarray(0, 16), + key.subarray(x, x + 36) + ]), + + this.concatSHA256([ + key.subarray(40 + x, 40 + x + 36), + msgKey.subarray(0, 16) + ]) + ]); + + return { + key: new Uint8Array([ + ...sha256a.subarray(0, 8), + ...sha256b.subarray(8, 8 + 16), + ...sha256a.subarray(24, 24 + 8) + ]), + iv: new Uint8Array([ + ...sha256b.subarray(0, 4), + ...sha256a.subarray(8, 8 + 8), + ...sha256b.subarray(24, 24 + 4) + ]) + }; + } + + private async aesProcessCtr(encryptedData: Uint8Array, dataSize: number, aesKeyIv: {key: Uint8Array, iv: Uint8Array}, encrypt = true) { + const cryptoKey = await subtle.importKey( + 'raw', + aesKeyIv.key, + {name: 'AES-CTR'}, + false, + [encrypt ? 'encrypt' : 'decrypt'] + ); + + const buffer: ArrayBuffer = await subtle[encrypt ? 'encrypt' : 'decrypt']({ + name: 'AES-CTR', + counter: aesKeyIv.iv, + length: aesKeyIv.iv.length * 8 + }, + cryptoKey, + encryptedData + ); + + return new Uint8Array(buffer); + } + + private constTimeIsDifferent(a: Uint8Array, b: Uint8Array, count: number) { + let msgKeyEquals = true; + for(let i = 0; i < count; ++i) { + if(a[i] !== b[i]) { + msgKeyEquals = false; + } + } + + return !msgKeyEquals; + } + + public async decryptRawPacket(buffer: Uint8Array) { + if(buffer.length < 21 || buffer.length > kMaxIncomingPacketSize) { + return; + } + + const {isOutgoing, type} = this; + + const x = (isOutgoing ? 8 : 0) + (type === 'Signaling' ? 128 : 0); + const key = this.p2pKey; + + const msgKey = buffer.subarray(0, 16); + const encryptedData = buffer.subarray(16); + const encryptedDataSize = buffer.length - 16; + + const aesKeyIv = await this.prepareAesKeyIv(key, msgKey, x); + + const decryptionBuffer = await this.aesProcessCtr(encryptedData, encryptedDataSize, aesKeyIv, false); + + const msgKeyLarge = await this.concatSHA256([ + key.subarray(88 + x, 88 + x + 32), + decryptionBuffer + ]); + + if(this.constTimeIsDifferent(msgKeyLarge.subarray(8), msgKey, 16)) { + return; + } + + const dataView = new DataView(decryptionBuffer.buffer); + const seq = dataView.getUint32(0); + if(this.seqMap.has(seq)) { + return; + } + this.seqMap.set(seq, seq); + + return decryptionBuffer.slice(4); + } +} diff --git a/src/lib/calls/p2P/p2PSdpBuilder.js b/src/lib/calls/p2P/p2PSdpBuilder.js index ae2d5b63c..58bc007c3 100644 --- a/src/lib/calls/p2P/p2PSdpBuilder.js +++ b/src/lib/calls/p2P/p2PSdpBuilder.js @@ -1,325 +1,324 @@ /* - * Copyright (c) 2018-present, Evgeny Nadymov - * - * This source code is licensed under the GPL v.3.0 license found in the - * LICENSE file in the root directory of this source tree. - */ +* Copyright (c) 2018-present, Evgeny Nadymov +* +* This source code is licensed under the GPL v.3.0 license found in the +* LICENSE file in the root directory of this source tree. +*/ -import { ChromeP2PSdpBuilder } from './ChromeP2PSdpBuilder'; +import ChromeP2PSdpBuilder from './chromeP2PSdpBuilder'; import { FirefoxP2PSdpBuilder } from './firefoxP2PSdpBuilder'; import { SafariP2PSdpBuilder } from './safariP2PSdpBuilder'; -import { TG_CALLS_SDP_STRING } from '../../Stores/CallStore'; +// import { TG_CALLS_SDP_STRING } from '../../Stores/CallStore'; export function p2pParseCandidate(candidate) { - if (!candidate) { - return null; - } - if (!candidate.startsWith('candidate:')) { - return null; - } - - const sdpString = candidate; - candidate = candidate.substr('candidate:'.length); - - const [ foundation, component, protocol, priority, ip, port, ...other ] = candidate.split(' '); - const c = { - sdpString, - foundation, - component, - protocol, - priority, - address: { ip, port } - }; - - for (let i = 0; i < other.length; i += 2) { - switch (other[i]) { - case 'typ': { - c.type = other[i + 1]; - break; - } - case 'raddr': { - if (!c.relAddress) { - c.relAddress = { }; - } - - c.relAddress.ip = other[i + 1]; - break; - } - case 'rport': { - if (!c.relAddress) { - c.relAddress = { }; - } - - c.relAddress.port = other[i + 1]; - break; - } - case 'generation': { - c.generation = other[i + 1]; - break; - } - case 'tcptype': { - c.tcpType = other[i + 1]; - break; - } - case 'network-id': { - c.networkId = other[i + 1]; - break; - } - case 'network-cost': { - c.networkCost = other[i + 1]; - break; - } - case 'ufrag': { - c.username = other[i + 1]; - break; - } + if(!candidate || !candidate.startsWith('candidate:')) { + return; + } + + const sdpString = candidate; + candidate = candidate.substr('candidate:'.length); + + const [foundation, component, protocol, priority, ip, port, ...other] = candidate.split(' '); + const c = { + sdpString, + foundation, + component, + protocol, + priority, + address: { ip, port } + }; + + for(let i = 0; i < other.length; i += 2) { + switch(other[i]) { + case 'typ': { + c.type = other[i + 1]; + break; + } + case 'raddr': { + if(!c.relAddress) { + c.relAddress = {}; + } + + c.relAddress.ip = other[i + 1]; + break; + } + case 'rport': { + if(!c.relAddress) { + c.relAddress = {}; } + + c.relAddress.port = other[i + 1]; + break; + } + case 'generation': { + c.generation = other[i + 1]; + break; + } + case 'tcptype': { + c.tcpType = other[i + 1]; + break; + } + case 'network-id': { + c.networkId = other[i + 1]; + break; + } + case 'network-cost': { + c.networkCost = other[i + 1]; + break; + } + case 'ufrag': { + c.username = other[i + 1]; + break; + } } - - return c; + } + + return c; } export function p2pParseSdp(sdp) { - const lines = sdp.split('\r\n'); - const lookup = (prefix, force = true, lineFrom = 0, lineTo = Number.MAX_VALUE) => { - if (lineTo === -1) { - lineTo = Number.MAX_VALUE; - } - for (let i = lineFrom; i < lines.length && i < lineTo; i++) { - const line = lines[i]; - if (line.startsWith(prefix)) { - return line.substr(prefix.length); - } - } - - if (force) { - console.error("Can't find prefix", prefix); - } - - return null; - }; - const findIndex = (prefix, lineFrom = 0, lineTo = Number.MAX_VALUE) => { - if (lineTo === -1) { - lineTo = Number.MAX_VALUE; - } - for (let i = lineFrom; i < lines.length && i < lineTo; i++) { - const line = lines[i]; - if (line.startsWith(prefix)) { - return i; - } - } - - return -1; - }; - - const pwdIndex = findIndex('a=ice-pwd:'); - const ufragIndex = findIndex('a=ice-ufrag:'); - if (pwdIndex === -1 && ufragIndex === -1) { - return { - // sessionId: lookup('o=').split(' ')[1], - ufrag: null, - pwd: null, - fingerprints: [] - }; + const lines = sdp.split('\r\n'); + const lookup = (prefix, force = true, lineFrom = 0, lineTo = Number.MAX_VALUE) => { + if (lineTo === -1) { + lineTo = Number.MAX_VALUE; } - - const info = { - // sessionId: lookup('o=').split(' ')[1], - ufrag: null, - pwd: null, - fingerprints: [] + for (let i = lineFrom; i < lines.length && i < lineTo; i++) { + const line = lines[i]; + if (line.startsWith(prefix)) { + return line.substr(prefix.length); + } + } + + if (force) { + console.error("Can't find prefix", prefix); + } + + return null; + }; + const findIndex = (prefix, lineFrom = 0, lineTo = Number.MAX_VALUE) => { + if (lineTo === -1) { + lineTo = Number.MAX_VALUE; + } + for (let i = lineFrom; i < lines.length && i < lineTo; i++) { + const line = lines[i]; + if (line.startsWith(prefix)) { + return i; + } + } + + return -1; + }; + + const pwdIndex = findIndex('a=ice-pwd:'); + const ufragIndex = findIndex('a=ice-ufrag:'); + if (pwdIndex === -1 && ufragIndex === -1) { + return { + // sessionId: lookup('o=').split(' ')[1], + ufrag: null, + pwd: null, + fingerprints: [] }; - - let mediaIndex = findIndex('m='); - const fingerprint = lookup('a=fingerprint:', false); - const setup = lookup('a=setup:', false); - if (fingerprint && setup) { - info.fingerprints.push({ - hash: fingerprint.split(' ')[0], - fingerprint: fingerprint.split(' ')[1], - setup + } + + const info = { + // sessionId: lookup('o=').split(' ')[1], + ufrag: null, + pwd: null, + fingerprints: [] + }; + + let mediaIndex = findIndex('m='); + const fingerprint = lookup('a=fingerprint:', false); + const setup = lookup('a=setup:', false); + if (fingerprint && setup) { + info.fingerprints.push({ + hash: fingerprint.split(' ')[0], + fingerprint: fingerprint.split(' ')[1], + setup + }); + } + + const ufrag = lookup('a=ice-ufrag:', false); + const pwd = lookup('a=ice-pwd:', false); + if (ufrag && pwd) { + info.ufrag = ufrag; + info.pwd = pwd; + } + + while (mediaIndex !== -1) { + let nextMediaIndex = findIndex('m=', mediaIndex + 1); + + const extmap = []; + const types = []; + const mediaType = lookup('m=', true, mediaIndex, nextMediaIndex).split(' ')[0]; + const media = { + // type: lookup('m=', true, mediaIndex, nextMediaIndex).split(' ')[0], + // mid: lookup('a=mid:', true, mediaIndex, nextMediaIndex), + // dir: findDirection(mediaIndex, nextMediaIndex), + rtpExtensions: extmap, + payloadTypes: types + } + + const lineTo = nextMediaIndex === -1 ? lines.length : nextMediaIndex; + const fmtp = new Map(); + const rtcpFb = new Map(); + for (let i = mediaIndex; i < lineTo; i++) { + const line = lines[i]; + if (line.startsWith('a=extmap:')) { + const [ id, uri ] = line.substr('a=extmap:'.length).split(' '); + extmap.push({ id: parseInt(id), uri }); + } else if (line.startsWith('a=fmtp:')) { + const [ id, str ] = line.substr('a=fmtp:'.length).split(' '); + const obj = { }; + const arr = str.split(';').map(x => { + const [ key, value ] = x.split('='); + obj[key] = value; + return { [key]: value }; }); + fmtp.set(parseInt(id), obj); + } else if (line.startsWith('a=rtcp-fb:')) { + const [ id, type = '', subtype = '' ] = line.substr('a=rtcp-fb:'.length).split(' '); + if (rtcpFb.has(parseInt(id))) { + rtcpFb.get(parseInt(id)).push({ type, subtype }); + } else { + rtcpFb.set(parseInt(id), [{ type, subtype }]) + } + } else if (line.startsWith('a=rtpmap')) { + const [ id, str ] = line.substr('a=rtpmap:'.length).split(' '); + const [ name, clockrate, channels = '0' ] = str.split('/'); + const obj = { id: parseInt(id), name, clockrate: parseInt(clockrate), channels: parseInt(channels) }; + + types.push(obj); + } } - - const ufrag = lookup('a=ice-ufrag:', false); - const pwd = lookup('a=ice-pwd:', false); - if (ufrag && pwd) { - info.ufrag = ufrag; - info.pwd = pwd; + + for (let i = 0; i < types.length; i++) { + const { id } = types[i]; + if (rtcpFb.has(id)) { + types[i].feedbackTypes = rtcpFb.get(id); + } + if (fmtp.has(id)) { + types[i].parameters = fmtp.get(id); + } } - - while (mediaIndex !== -1) { - let nextMediaIndex = findIndex('m=', mediaIndex + 1); - - const extmap = []; - const types = []; - const mediaType = lookup('m=', true, mediaIndex, nextMediaIndex).split(' ')[0]; - const media = { - // type: lookup('m=', true, mediaIndex, nextMediaIndex).split(' ')[0], - // mid: lookup('a=mid:', true, mediaIndex, nextMediaIndex), - // dir: findDirection(mediaIndex, nextMediaIndex), - rtpExtensions: extmap, - payloadTypes: types - } - - const lineTo = nextMediaIndex === -1 ? lines.length : nextMediaIndex; - const fmtp = new Map(); - const rtcpFb = new Map(); - for (let i = mediaIndex; i < lineTo; i++) { - const line = lines[i]; - if (line.startsWith('a=extmap:')) { - const [ id, uri ] = line.substr('a=extmap:'.length).split(' '); - extmap.push({ id: parseInt(id), uri }); - } else if (line.startsWith('a=fmtp:')) { - const [ id, str ] = line.substr('a=fmtp:'.length).split(' '); - const obj = { }; - const arr = str.split(';').map(x => { - const [ key, value ] = x.split('='); - obj[key] = value; - return { [key]: value }; - }); - fmtp.set(parseInt(id), obj); - } else if (line.startsWith('a=rtcp-fb:')) { - const [ id, type = '', subtype = '' ] = line.substr('a=rtcp-fb:'.length).split(' '); - if (rtcpFb.has(parseInt(id))) { - rtcpFb.get(parseInt(id)).push({ type, subtype }); - } else { - rtcpFb.set(parseInt(id), [{ type, subtype }]) - } - } else if (line.startsWith('a=rtpmap')) { - const [ id, str ] = line.substr('a=rtpmap:'.length).split(' '); - const [ name, clockrate, channels = '0' ] = str.split('/'); - const obj = { id: parseInt(id), name, clockrate: parseInt(clockrate), channels: parseInt(channels) }; - - types.push(obj); - } - } - - for (let i = 0; i < types.length; i++) { - const { id } = types[i]; - if (rtcpFb.has(id)) { - types[i].feedbackTypes = rtcpFb.get(id); - } - if (fmtp.has(id)) { - types[i].parameters = fmtp.get(id); - } - } - - const ssrc = lookup('a=ssrc:', false, mediaIndex, nextMediaIndex); - if (ssrc) { - media.ssrc = ssrc.split(' ')[0]; - } - - const ssrcGroup = lookup('a=ssrc-group:', false, mediaIndex, nextMediaIndex); - if (ssrcGroup) { - const [ semantics, ...ssrcs ] = ssrcGroup.split(' '); - media.ssrcGroups = [{ - semantics, - ssrcs - }] - } - - switch (mediaType) { - case 'audio': { - info.audio = media; - break; - } - case 'video': { - info.video = media; - break; - } - } - - mediaIndex = nextMediaIndex; + + const ssrc = lookup('a=ssrc:', false, mediaIndex, nextMediaIndex); + if (ssrc) { + media.ssrc = ssrc.split(' ')[0]; } - - // console.log('[p2pParseSdp]', sdp, info); - return info; + + const ssrcGroup = lookup('a=ssrc-group:', false, mediaIndex, nextMediaIndex); + if (ssrcGroup) { + const [ semantics, ...ssrcs ] = ssrcGroup.split(' '); + media.ssrcGroups = [{ + semantics, + ssrcs + }] + } + + switch (mediaType) { + case 'audio': { + info.audio = media; + break; + } + case 'video': { + info.video = media; + break; + } + } + + mediaIndex = nextMediaIndex; + } + + if(!info.video.ssrcGroups) { + info.video.ssrcGroups = []; + } + + info['@type'] = 'InitialSetup'; + + // console.log('[p2pParseSdp]', sdp, info); + return info; } export function isFirefox() { - return navigator.userAgent.toLowerCase().indexOf('firefox') > -1; + return navigator.userAgent.toLowerCase().indexOf('firefox') > -1; } function isSafari() { - return navigator.userAgent.toLowerCase().indexOf('safari') > -1 && navigator.userAgent.toLowerCase().indexOf('chrome') === -1; + return navigator.userAgent.toLowerCase().indexOf('safari') > -1 && navigator.userAgent.toLowerCase().indexOf('chrome') === -1; } export function addExtmap(extmap) { - let sdp = ''; - // return sdp; - for (let j = 0; j < extmap.length; j++) { - const ext = extmap[j]; - const { id, uri } = ext; - // if (isFirefox() && uri.indexOf('')) - console.log('[extmap] add', id, uri); - sdp += ` -a=extmap:${id} ${uri}`; - } - - return sdp; + let sdp = []; + // return sdp; + for (let j = 0; j < extmap.length; j++) { + const ext = extmap[j]; + const { id, uri } = ext; + // if (isFirefox() && uri.indexOf('')) + console.log('[extmap] add', id, uri); + sdp.push(`a=extmap:${id} ${uri}`); + } + + return sdp.join('\n'); } export function addPayloadTypes(types) { - let sdp = ''; - console.log('[SDP] addPayloadTypes', types); - for (let i = 0; i < types.length; i++) { - const type = types[i]; - const { id, name, clockrate, channels, feedbackTypes, parameters } = type; - sdp += ` -a=rtpmap:${id} ${name}/${clockrate}${channels ? '/' + channels : ''}`; - if (feedbackTypes) { - feedbackTypes.forEach(x => { - const { type, subtype } = x; - sdp += ` -a=rtcp-fb:${id} ${[type, subtype].join(' ')}`; - }); - } - if (parameters) { - const fmtp = []; - Object.getOwnPropertyNames(parameters).forEach(pName => { - fmtp.push(`${pName}=${parameters[pName]}`); - }); - - sdp += ` -a=fmtp:${id} ${fmtp.join(';')}`; - } + let sdp = []; + console.log('[SDP] addPayloadTypes', types); + for (let i = 0; i < types.length; i++) { + const type = types[i]; + const { id, name, clockrate, channels, feedbackTypes, parameters } = type; + sdp.push(`a=rtpmap:${id} ${name}/${clockrate}${channels ? '/' + channels : ''}`); + if (feedbackTypes) { + feedbackTypes.forEach(x => { + const { type, subtype } = x; + sdp.push(`a=rtcp-fb:${id} ${[type, subtype].join(' ')}`); + }); } - - return sdp; + if (parameters) { + const fmtp = []; + Object.getOwnPropertyNames(parameters).forEach(pName => { + fmtp.push(`${pName}=${parameters[pName]}`); + }); + + sdp.push(`a=fmtp:${id} ${fmtp.join(';')}`); + } + } + + return sdp.join('\n'); } export function addSsrc(type, ssrc, ssrcGroups, streamName) { - let sdp = ''; - - if (ssrcGroups && ssrcGroups.length > 0) { - ssrcGroups.forEach(ssrcGroup => { - if (ssrcGroup && ssrcGroup.ssrcs.length > 0) { - sdp += ` -a=ssrc-group:${ssrcGroup.semantics} ${ssrcGroup.ssrcs.join(' ')}`; - ssrcGroup.ssrcs.forEach(ssrc => { - sdp += ` -a=ssrc:${ssrc} cname:stream${ssrc} -a=ssrc:${ssrc} msid:${streamName} ${type}${ssrc} -a=ssrc:${ssrc} mslabel:${type}${ssrc} -a=ssrc:${ssrc} label:${type}${ssrc}`; - }); - } + let sdp = []; + + if (ssrcGroups && ssrcGroups.length > 0) { + ssrcGroups.forEach(ssrcGroup => { + if (ssrcGroup && ssrcGroup.ssrcs.length > 0) { + sdp.push(`a=ssrc-group:${ssrcGroup.semantics} ${ssrcGroup.ssrcs.join(' ')}`); + ssrcGroup.ssrcs.forEach(ssrc => { + sdp.push( + `a=ssrc:${ssrc} cname:stream${ssrc}`, + `a=ssrc:${ssrc} msid:${streamName} ${type}${ssrc}`, + `a=ssrc:${ssrc} mslabel:${type}${ssrc}`, + `a=ssrc:${ssrc} label:${type}${ssrc}` + ); }); - } else if (ssrc) { - sdp += ` -a=ssrc:${ssrc} cname:stream${ssrc} -a=ssrc:${ssrc} msid:${streamName} ${type}${ssrc} -a=ssrc:${ssrc} mslabel:${type}${ssrc} -a=ssrc:${ssrc} label:${type}${ssrc}`; - } - - return sdp; + } + }); + } else if (ssrc) { + sdp.push( + `a=ssrc:${ssrc} cname:stream${ssrc}`, + `a=ssrc:${ssrc} msid:${streamName} ${type}${ssrc}`, + `a=ssrc:${ssrc} mslabel:${type}${ssrc}`, + `a=ssrc:${ssrc} label:${type}${ssrc}` + ); + } + + return sdp.join('\n'); } export function addDataChannel(mid) { - return ` -m=application 9 UDP/DTLS/SCTP webrtc-datachannel + return `m=application 9 UDP/DTLS/SCTP webrtc-datachannel c=IN IP4 0.0.0.0 a=ice-options:trickle a=mid:2 @@ -328,69 +327,69 @@ a=max-message-size:262144`; } export class P2PSdpBuilder { - static generateCandidate(info) { - if (!info) return null; - - const { sdpString, sdpMLineIndex, sdpMid, foundation, component, protocol, priority, address, type, relAddress, generation, tcpType, networkId, networkCost, username } = info; - if (TG_CALLS_SDP_STRING) { - if (sdpString) { - return { - candidate: sdpString, - sdpMLineIndex, - sdpMid - }; - } - } - throw 'no sdpString'; - - let candidate = `candidate:${foundation} ${component} ${protocol} ${priority} ${address.ip} ${address.port}`; - const attrs = [] - if (type) { - attrs.push(`typ ${type}`); - } - if (relAddress) { - attrs.push(`raddr ${relAddress.ip}`); - attrs.push(`rport ${relAddress.port}`); - } - if (tcpType) { - attrs.push(`tcptype ${tcpType}`); - } - if (generation) { - attrs.push(`generation ${generation}`); - } - if (username) { - attrs.push(`ufrag ${username}`); - } - if (networkId) { - attrs.push(`network-id ${networkId}`); - } - if (networkCost) { - attrs.push(`network-cost ${networkCost}`); - } - if (attrs.length > 0) { - candidate += ` ${attrs.join(' ')}`; - } - - return { candidate, sdpMid, sdpMLineIndex }; + static generateCandidate(info) { + if (!info) return null; + + const { sdpString, sdpMLineIndex, sdpMid, foundation, component, protocol, priority, address, type, relAddress, generation, tcpType, networkId, networkCost, username } = info; + if (/* TG_CALLS_SDP_STRING */true) { + if (sdpString) { + return { + candidate: sdpString, + sdpMLineIndex, + sdpMid + }; + } } - - static generateOffer(info) { - if (isFirefox()) { - return FirefoxP2PSdpBuilder.generateOffer(info); - } else if (isSafari()) { - return SafariP2PSdpBuilder.generateOffer(info); - } - - return ChromeP2PSdpBuilder.generateOffer(info); + throw 'no sdpString'; + + let candidate = `candidate:${foundation} ${component} ${protocol} ${priority} ${address.ip} ${address.port}`; + const attrs = [] + if (type) { + attrs.push(`typ ${type}`); } - - static generateAnswer(info) { - if (isFirefox()) { - return FirefoxP2PSdpBuilder.generateAnswer(info); - } else if (isSafari()) { - return SafariP2PSdpBuilder.generateAnswer(info); - } - - return ChromeP2PSdpBuilder.generateAnswer(info); + if (relAddress) { + attrs.push(`raddr ${relAddress.ip}`); + attrs.push(`rport ${relAddress.port}`); + } + if (tcpType) { + attrs.push(`tcptype ${tcpType}`); + } + if (generation) { + attrs.push(`generation ${generation}`); + } + if (username) { + attrs.push(`ufrag ${username}`); + } + if (networkId) { + attrs.push(`network-id ${networkId}`); + } + if (networkCost) { + attrs.push(`network-cost ${networkCost}`); + } + if (attrs.length > 0) { + candidate += ` ${attrs.join(' ')}`; + } + + return { candidate, sdpMid, sdpMLineIndex }; + } + + static generateOffer(info) { + if (isFirefox()) { + return FirefoxP2PSdpBuilder.generateOffer(info); + } else if (isSafari()) { + return SafariP2PSdpBuilder.generateOffer(info); + } + + return ChromeP2PSdpBuilder.generateOffer(info); + } + + static generateAnswer(info) { + if (isFirefox()) { + return FirefoxP2PSdpBuilder.generateAnswer(info); + } else if (isSafari()) { + return SafariP2PSdpBuilder.generateAnswer(info); } + + return ChromeP2PSdpBuilder.generateAnswer(info); + } } \ No newline at end of file diff --git a/src/lib/calls/sdpBuilder.ts b/src/lib/calls/sdpBuilder.ts index fe6279937..c3a944314 100644 --- a/src/lib/calls/sdpBuilder.ts +++ b/src/lib/calls/sdpBuilder.ts @@ -15,10 +15,16 @@ import StringFromLineBuilder from './stringFromLineBuilder'; import { GroupCallConnectionTransport, PayloadType, UpdateGroupCallConnectionData } from './types'; import { fromTelegramSource } from './utils'; -export type WebRTCLineType = 'video' | 'audio' | 'application'; +// screencast is for Peer-to-Peer only +export type WebRTCLineTypeTrue = 'video' | 'audio' | 'application'; +export type WebRTCLineType = WebRTCLineTypeTrue | 'screencast'; export const WEBRTC_MEDIA_PORT = '9'; +export function fixMediaLineType(mediaType: WebRTCLineType) { + return mediaType === 'screencast' ? 'video' : mediaType; +} + export function performCandidate(c: GroupCallConnectionTransport['candidates'][0]) { const arr: string[] = []; arr.push('a=candidate:'); @@ -31,15 +37,16 @@ export function performCandidate(c: GroupCallConnectionTransport['candidates'][0 } export function getConnectionTypeForMediaType(mediaType: WebRTCLineType) { - return mediaType === 'application' ? 'DTLS/SCTP' : 'RTP/SAVPF'; + // return mediaType === 'application' ? 'DTLS/SCTP' : 'RTP/SAVPF'; + return mediaType === 'application' ? 'DTLS/SCTP' : 'UDP/TLS/RTP/SAVPF'; } export function generateMediaFirstLine(mediaType: WebRTCLineType, port = WEBRTC_MEDIA_PORT, payloadIds: (string | number)[]) { const connectionType = getConnectionTypeForMediaType(mediaType); - return `m=${mediaType} ${port} ${connectionType} ${payloadIds.join(' ')}`; + return `m=${fixMediaLineType(mediaType)} ${port} ${connectionType} ${payloadIds.join(' ')}`; } -type ConferenceData = UpdateGroupCallConnectionData; +type ConferenceData = UpdateGroupCallConnectionData | LocalConferenceDescription; // https://tools.ietf.org/id/draft-ietf-rtcweb-sdp-08.html // https://datatracker.ietf.org/doc/html/draft-roach-mmusic-unified-plan-00 @@ -78,7 +85,7 @@ export class SDPBuilder extends StringFromLineBuilder { 'a=extmap-allow-mixed', `a=group:BUNDLE ${bundle}`, 'a=ice-options:trickle', - 'a=ice-lite', // ice-lite: is a minimal version of the ICE specification, intended for servers running on a public IP address. + // 'a=ice-lite', // ice-lite: is a minimal version of the ICE specification, intended for servers running on a public IP address. 'a=msid-semantic:WMS *' ); } @@ -167,7 +174,7 @@ export class SDPBuilder extends StringFromLineBuilder { const isInactive = direction === 'inactive'; if(entry.shouldBeSkipped(isAnswer)) { return add( - `m=${type} 0 ${getConnectionTypeForMediaType(type)} 0`, + `m=${fixMediaLineType(type)} 0 ${getConnectionTypeForMediaType(type)} 0`, `c=IN IP4 0.0.0.0`, `a=inactive`, `a=mid:${mid}` diff --git a/src/lib/calls/streamManager.ts b/src/lib/calls/streamManager.ts index c03ac195c..80d05b3d7 100644 --- a/src/lib/calls/streamManager.ts +++ b/src/lib/calls/streamManager.ts @@ -14,6 +14,7 @@ import rootScope from '../rootScope'; import { GROUP_CALL_AMPLITUDE_ANALYSE_COUNT_MAX } from './constants'; import stopTrack from './helpers/stopTrack'; import LocalConferenceDescription from './localConferenceDescription'; +import { fixMediaLineType, WebRTCLineType } from './sdpBuilder'; import { getAmplitude, toTelegramSource } from './utils'; export type StreamItemBase = { @@ -72,7 +73,8 @@ export default class StreamManager { public direction: RTCRtpTransceiver['direction']; public canCreateConferenceEntry: boolean; - public lol: boolean; + public locked: boolean; + public types: WebRTCLineType[]; constructor(private interval?: number) { this.context = new (window.AudioContext || (window as any).webkitAudioContext)(); @@ -84,6 +86,7 @@ export default class StreamManager { this.direction = 'sendonly'; this.canCreateConferenceEntry = true; // this.lol = true; + this.types = ['audio', 'video']; } public addStream(stream: MediaStream, type: StreamItem['type']) { @@ -258,18 +261,20 @@ export default class StreamManager { } */ public appendToConference(conference: LocalConferenceDescription) { - if(this.lol) { + if(this.locked) { return; } - // return; + const {inputStream, direction, canCreateConferenceEntry} = this; - // const direction: RTCRtpTransceiverInit['direction'] = 'sendrecv'; - // const direction: RTCRtpTransceiverInit['direction'] = 'sendonly'; const transceiverInit: RTCRtpTransceiverInit = {direction, streams: [inputStream]}; - const types: ['audio' | 'video', RTCRtpTransceiverInit][] = [ - ['audio' as const, transceiverInit], - ['video' as const, transceiverInit/* {sendEncodings: [{maxBitrate: 2500000}], ...transceiverInit} */] - ]; + const types = this.types.map(type => { + return [ + type, + /* type === 'video' || type === 'screencast' ? + {sendEncodings: [{maxBitrate: 2500000}], ...transceiverInit} : */ + transceiverInit + ] as const; + }); const tracks = inputStream.getTracks(); // const transceivers = conference.connection.getTransceivers(); @@ -296,7 +301,9 @@ export default class StreamManager { transceiver.direction = entry.direction; } - const track = tracks.find(track => track.kind === type); + const mediaTrackType = fixMediaLineType(type); + const trackIdx = tracks.findIndex(track => track.kind === mediaTrackType); + const track = trackIdx !== -1 ? tracks.splice(trackIdx, 1)[0] : undefined; const sender = transceiver.sender; if(sender.track === track) { continue; diff --git a/src/lib/calls/stringFromLineBuilder.ts b/src/lib/calls/stringFromLineBuilder.ts index bef72a1df..e5e3466ec 100644 --- a/src/lib/calls/stringFromLineBuilder.ts +++ b/src/lib/calls/stringFromLineBuilder.ts @@ -10,11 +10,12 @@ */ export default class StringFromLineBuilder { - private lines: string[] = []; - private newLine: string[] = []; + private lines: string[]; + private newLine: string[]; constructor(private joiner = '\r\n') { - + this.lines = []; + this.newLine = []; } public add(...strs: string[]) { diff --git a/src/lib/calls/types.d.ts b/src/lib/calls/types.d.ts index 89c6fac1c..571b1e0c5 100644 --- a/src/lib/calls/types.d.ts +++ b/src/lib/calls/types.d.ts @@ -73,7 +73,8 @@ export type VideoCodec = { export type UpdateGroupCallConnectionData = { transport: GroupCallConnectionTransport, audio?: AudioCodec, - video: VideoCodec + video: VideoCodec, + screencast?: VideoCodec }; export type UpgradeGroupCallConnectionPresentationData = Omit; diff --git a/src/lib/crypto/computeDhKey.ts b/src/lib/crypto/computeDhKey.ts new file mode 100644 index 000000000..83f7c1916 --- /dev/null +++ b/src/lib/crypto/computeDhKey.ts @@ -0,0 +1,17 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import { bigIntFromBytes } from "../../helpers/bigInt/bigIntConversion"; +import cryptoWorker from "./cryptoworker"; + +export default async function computeDhKey(g_b: Uint8Array, a: Uint8Array, p: Uint8Array) { + const key = await cryptoWorker.invokeCrypto('mod-pow', g_b, a, p); + const keySha1Hashed = await cryptoWorker.invokeCrypto('sha1', key); + const key_fingerprint = keySha1Hashed.slice(-8).reverse(); // key_fingerprint: key_fingerprint as any // ! it doesn't work + const key_fingerprint_long = bigIntFromBytes(key_fingerprint).toString(10); // bigInt2str(str2bigInt(bytesToHex(key_fingerprint), 16), 10); + + return {key, key_fingerprint: key_fingerprint_long}; +} diff --git a/src/lib/crypto/crypto.worker.js b/src/lib/crypto/crypto.worker.js index 917b85be0..cb8737169 100644 --- a/src/lib/crypto/crypto.worker.js +++ b/src/lib/crypto/crypto.worker.js @@ -16,9 +16,6 @@ const ctx = self; import {secureRandom} from '../polyfill'; secureRandom; -import {pqPrimeFactorization, bytesModPow, sha1HashSync, - aesEncryptSync, aesDecryptSync, hash_pbkdf2, sha256HashSync, rsaEncrypt} from './crypto_utils'; - import {gzipUncompress} from '../mtproto/bin_utils'; ctx.onmessage = function(e) { @@ -38,11 +35,11 @@ ctx.onmessage = function(e) { result = bytesModPow.apply(null, e.data.args); break; - case 'sha1-hash': + case 'sha1': result = sha1HashSync.apply(null, e.data.args); break; - case 'sha256-hash': + case 'sha256': result = sha256HashSync.apply(null, e.data.args); break; diff --git a/src/lib/crypto/crypto_methods.ts b/src/lib/crypto/crypto_methods.ts index 43be63fb4..54d706323 100644 --- a/src/lib/crypto/crypto_methods.ts +++ b/src/lib/crypto/crypto_methods.ts @@ -4,27 +4,45 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +import type bytesModPow from "../../helpers/bytes/bytesModPow"; +import type gzipUncompress from "../../helpers/gzipUncompress"; import type { Awaited } from "../../types"; -import type { aesEncryptSync, aesDecryptSync, sha256HashSync, sha1HashSync, bytesModPow, hash_pbkdf2, rsaEncrypt, pqPrimeFactorization, gzipUncompress } from "./crypto_utils"; -import type { computeSRP } from "./srp"; +import type getEmojisFingerprint from "../calls/helpers/getEmojisFingerprint"; +import type computeDhKey from "./computeDhKey"; +import type generateDh from "./generateDh"; +import type computeSRP from "./srp"; +import type { aesEncryptSync, aesDecryptSync } from "./utils/aesIGE"; +import type factorizeBrentPollardPQ from "./utils/factorize/BrentPollard"; +// import type factorizeTdlibPQ from "./utils/factorize/tdlib"; +import type pbkdf2 from "./utils/pbkdf2"; +import type rsaEncrypt from "./utils/rsa"; +import type sha1 from "./utils/sha1"; +import type sha256 from "./utils/sha256"; export type CryptoMethods = { - 'sha1-hash': typeof sha1HashSync, - 'sha256-hash': typeof sha256HashSync, - 'pbkdf2': typeof hash_pbkdf2, + 'sha1': typeof sha1, + 'sha256': typeof sha256, + 'pbkdf2': typeof pbkdf2, 'aes-encrypt': typeof aesEncryptSync, 'aes-decrypt': typeof aesDecryptSync, 'rsa-encrypt': typeof rsaEncrypt, - 'factorize': typeof pqPrimeFactorization, + 'factorize': typeof factorizeBrentPollardPQ, + // 'factorize-tdlib': typeof factorizeTdlibPQ, 'mod-pow': typeof bytesModPow, 'gzipUncompress': typeof gzipUncompress, - 'computeSRP': typeof computeSRP + 'computeSRP': typeof computeSRP, + 'generate-dh': typeof generateDh, + 'compute-dh-key': typeof computeDhKey, + 'get-emojis-fingerprint': typeof getEmojisFingerprint }; export default abstract class CryptoWorkerMethods { abstract performTaskWorker(task: string, ...args: any[]): Promise; - public invokeCrypto(method: Method, ...args: Parameters): Promise>> { + public invokeCrypto( + method: Method, + ...args: Parameters + ): Promise>> { return this.performTaskWorker>>(method, ...args as any[]); } } diff --git a/src/lib/crypto/crypto_utils.ts b/src/lib/crypto/crypto_utils.ts deleted file mode 100644 index de57c6696..000000000 --- a/src/lib/crypto/crypto_utils.ts +++ /dev/null @@ -1,286 +0,0 @@ -/* - * https://github.com/morethanwords/tweb - * Copyright (C) 2019-2021 Eduard Kuzmenko - * https://github.com/morethanwords/tweb/blob/master/LICENSE - * - * Originally from: - * https://github.com/zhukov/webogram - * Copyright (C) 2014 Igor Zhukov - * https://github.com/zhukov/webogram/blob/master/LICENSE - */ - -//import sha1 from '@cryptography/sha1'; -//import sha256 from '@cryptography/sha256'; -import {IGE} from '@cryptography/aes'; - -// @ts-ignore -import pako from 'pako/dist/pako_inflate.min.js'; - -import {str2bigInt, bpe, equalsInt, greater, - copy_, eGCD_, add_, rightShift_, sub_, copyInt_, isZero, - divide_, one, bigInt2str, powMod, bigInt2bytes, int2bigInt, mod} from '../../vendor/leemon';//from 'leemon'; - -import { addPadding } from '../mtproto/bin_utils'; -import { bytesToWordss, bytesFromWordss, bytesToHex, bytesFromHex, convertToUint8Array } from '../../helpers/bytes'; -import { nextRandomUint } from '../../helpers/random'; -import type { RSAPublicKeyHex } from '../mtproto/rsaKeysManager'; - -const subtle = typeof(window) !== 'undefined' && 'crypto' in window ? window.crypto.subtle : self.crypto.subtle; - -export function longToBytes(sLong: string) { - /* let perf = performance.now(); - for(let i = 0; i < 1000000; ++i) { - bytesFromWords({words: longToInts(sLong), sigBytes: 8}).reverse(); - } - console.log('longToBytes JSBN', sLong, performance.now() - perf); - - //const bytes = bytesFromWords({words: longToInts(sLong), sigBytes: 8}).reverse(); - - perf = performance.now(); - for(let i = 0; i < 1000000; ++i) { - bigInt2bytes(str2bigInt(sLong, 10)); - } - console.log('longToBytes LEEMON', sLong, performance.now() - perf); */ - - const bigIntBytes = new Uint8Array(bigInt2bytes(str2bigInt(sLong, 10), false)); - const bytes = addPadding(bigIntBytes, 8, true, false, false); - //console.log('longToBytes', bytes, b); - - return bytes; -} - -export function sha1HashSync(bytes: Parameters[0]) { - return subtle.digest('SHA-1', convertToUint8Array(bytes)).then(b => { - return new Uint8Array(b); - }); - /* //console.trace(dT(), 'SHA-1 hash start', bytes); - - const hashBytes: number[] = []; - - let hash = sha1(String.fromCharCode.apply(null, - bytes instanceof Uint8Array ? [...bytes] : [...new Uint8Array(bytes)])); - for(let i = 0; i < hash.length; ++i) { - hashBytes.push(hash.charCodeAt(i)); - } - - //console.log(dT(), 'SHA-1 hash finish', hashBytes, bytesToHex(hashBytes)); - - return new Uint8Array(hashBytes); */ -} - -export function sha256HashSync(bytes: Parameters[0]) { - return subtle.digest('SHA-256', convertToUint8Array(bytes)).then(b => { - //console.log('legacy', performance.now() - perfS); - return new Uint8Array(b); - }); - /* //console.log('SHA-256 hash start'); - - let perfS = performance.now(); - - - let perfD = performance.now(); - let words = typeof(bytes) === 'string' ? bytes : bytesToWordss(bytes as any); - let hash = sha256(words); - console.log('darutkin', performance.now() - perfD); - - //console.log('SHA-256 hash finish', hash, sha256(words, 'hex')); - - return bytesFromWordss(hash); */ -} - -export function aesEncryptSync(bytes: Uint8Array, keyBytes: Uint8Array, ivBytes: Uint8Array) { - //console.log(dT(), 'AES encrypt start', bytes, keyBytes, ivBytes); - // console.log('aes before padding bytes:', bytesToHex(bytes)); - bytes = addPadding(bytes); - // console.log('aes after padding bytes:', bytesToHex(bytes)); - - const cipher = new IGE(bytesToWordss(keyBytes), bytesToWordss(ivBytes)); - const encryptedBytes = cipher.encrypt(bytesToWordss(bytes)); - //console.log(dT(), 'AES encrypt finish'); - - return bytesFromWordss(encryptedBytes); -} - -export function aesDecryptSync(bytes: Uint8Array, keyBytes: Uint8Array, ivBytes: Uint8Array) { - //console.log(dT(), 'AES decrypt start', bytes, keyBytes, ivBytes); - - const cipher = new IGE(bytesToWordss(keyBytes), bytesToWordss(ivBytes)); - const decryptedBytes = cipher.decrypt(bytesToWordss(bytes)); - - //console.log(dT(), 'AES decrypt finish'); - - return bytesFromWordss(decryptedBytes); -} - -export function rsaEncrypt(bytes: Uint8Array, publicKey: RSAPublicKeyHex) { - //console.log(dT(), 'RSA encrypt start', publicKey, bytes); - - const N = str2bigInt(publicKey.modulus, 16); - const E = str2bigInt(publicKey.exponent, 16); - const X = str2bigInt(bytesToHex(bytes), 16); - - const encryptedBigInt = powMod(X, E, N); - const encryptedBytes = bytesFromHex(bigInt2str(encryptedBigInt, 16)); - - //console.log(dT(), 'RSA encrypt finish'); - - return encryptedBytes; -} - -export async function hash_pbkdf2(buffer: Parameters[1], salt: HkdfParams['salt'], iterations: number) { - const importKey = await subtle.importKey( - 'raw', - buffer, - {name: 'PBKDF2'}, - false, - [/* 'deriveKey', */'deriveBits'] - ); - - /* await subtle.deriveKey( - { - name: 'PBKDF2', - salt, - iterations, - hash: {name: 'SHA-512'} - }, - importKey, - { - name: 'AES-CTR', - length: 256 - }, - false, - ['encrypt', 'decrypt'] - ); */ - - let bits = subtle.deriveBits({ - name: 'PBKDF2', - salt, - iterations, - hash: {name: 'SHA-512'}, - }, - importKey, - 512 - ); - - return bits.then(buffer => new Uint8Array(buffer)); -} - -export function pqPrimeFactorization(pqBytes: Uint8Array | number[]) { - let result: ReturnType; - - //console.log('PQ start', pqBytes, bytesToHex(pqBytes)); - - try { - //console.time('PQ leemon'); - result = pqPrimeLeemon(str2bigInt(bytesToHex(pqBytes), 16, Math.ceil(64 / bpe) + 1)); - //console.timeEnd('PQ leemon'); - } catch(e) { - console.error('Pq leemon Exception', e); - } - - //console.log('PQ finish', result); - - return result; -} - -export function pqPrimeLeemon(what: number[]): [Uint8Array, Uint8Array, number] { - var minBits = 64; - var minLen = Math.ceil(minBits / bpe) + 1; - var it = 0; - var i, q; - var j, lim; - var P; - var Q; - var a = new Array(minLen); - var b = new Array(minLen); - var c = new Array(minLen); - var g = new Array(minLen); - var z = new Array(minLen); - var x = new Array(minLen); - var y = new Array(minLen); - - for(i = 0; i < 3; ++i) { - q = (nextRandomUint(8) & 15) + 17; - copy_(x, mod(int2bigInt(nextRandomUint(32), 32, 0), what)); - copy_(y, x); - lim = 1 << (i + 18); - - for (j = 1; j < lim; ++j) { - ++it; - copy_(a, x); - copy_(b, x); - copyInt_(c, q); - - while(!isZero(b)) { - if(b[0] & 1) { - add_(c, a); - if(greater(c, what)) { - sub_(c, what); - } - } - add_(a, a); - if(greater(a, what)) { - sub_(a, what); - } - rightShift_(b, 1); - } - - copy_(x, c); - if(greater(x, y)) { - copy_(z, x); - sub_(z, y); - } else { - copy_(z, y); - sub_(z, x); - } - eGCD_(z, what, g, a, b); - if(!equalsInt(g, 1)) { - break; - } - if((j & (j - 1)) === 0) { - copy_(y, x); - } - } - if(greater(g, one)) { - break; - } - } - - divide_(what, g, x, y); - - if(greater(g, x)) { - P = x; - Q = g; - } else { - P = g; - Q = x; - } - - // console.log(dT(), 'done', bigInt2str(what, 10), bigInt2str(P, 10), bigInt2str(Q, 10)) - - return [new Uint8Array(bigInt2bytes(P)), new Uint8Array(bigInt2bytes(Q)), it]; -} - -export function bytesModPow(x: number[] | Uint8Array, y: number[] | Uint8Array, m: number[] | Uint8Array) { - try { - const xBigInt = str2bigInt(bytesToHex(x), 16); - const yBigInt = str2bigInt(bytesToHex(y), 16); - const mBigInt = str2bigInt(bytesToHex(m), 16); - const resBigInt = powMod(xBigInt, yBigInt, mBigInt); - - return bytesFromHex(bigInt2str(resBigInt, 16)); - } catch(e) { - console.error('mod pow error', e); - } - - //return bytesFromBigInt(new BigInteger(x).modPow(new BigInteger(y), new BigInteger(m)), 256); -} - -//export function gzipUncompress(bytes: ArrayBuffer, toString: true): string; -//export function gzipUncompress(bytes: ArrayBuffer, toString?: false): Uint8Array; -export function gzipUncompress(bytes: ArrayBuffer, toString?: boolean): string | Uint8Array { - //console.log(dT(), 'Gzip uncompress start'); - const result = pako.inflate(bytes, toString ? {to: 'string'} : undefined); - //console.log(dT(), 'Gzip uncompress finish'/* , result */); - return result; -} diff --git a/src/lib/crypto/cryptoworker.ts b/src/lib/crypto/cryptoworker.ts index 3eaa0a771..067074e3e 100644 --- a/src/lib/crypto/cryptoworker.ts +++ b/src/lib/crypto/cryptoworker.ts @@ -13,8 +13,19 @@ import CryptoWorkerMethods, { CryptoMethods } from './crypto_methods'; /// #if MTPROTO_WORKER -import { aesDecryptSync, aesEncryptSync, bytesModPow, gzipUncompress, hash_pbkdf2, pqPrimeFactorization, rsaEncrypt, sha1HashSync, sha256HashSync } from './crypto_utils'; -import { computeSRP } from './srp'; +import gzipUncompress from '../../helpers/gzipUncompress'; +import bytesModPow from '../../helpers/bytes/bytesModPow'; +import computeSRP from './srp'; +import { aesEncryptSync, aesDecryptSync } from './utils/aesIGE'; +import pbkdf2 from './utils/pbkdf2'; +import rsaEncrypt from './utils/rsa'; +import sha1 from './utils/sha1'; +import sha256 from './utils/sha256'; +import factorizeBrentPollardPQ from './utils/factorize/BrentPollard'; +import generateDh from './generateDh'; +import computeDhKey from './computeDhKey'; +import getEmojisFingerprint from '../calls/helpers/getEmojisFingerprint'; +// import factorizeTdlibPQ from './utils/factorize/tdlib'; /// #endif type Task = { @@ -44,16 +55,21 @@ class CryptoWorker extends CryptoWorkerMethods { /// #if MTPROTO_WORKER this.utils = { - 'sha1-hash': sha1HashSync, - 'sha256-hash': sha256HashSync, - 'pbkdf2': hash_pbkdf2, + 'sha1': sha1, + 'sha256': sha256, + 'pbkdf2': pbkdf2, 'aes-encrypt': aesEncryptSync, 'aes-decrypt': aesDecryptSync, 'rsa-encrypt': rsaEncrypt, - 'factorize': pqPrimeFactorization, + 'factorize': factorizeBrentPollardPQ, + // 'factorize-tdlib': factorizeTdlibPQ, + // 'factorize-new-new': pqPrimeLeemonNew, 'mod-pow': bytesModPow, 'gzipUncompress': gzipUncompress, - 'computeSRP': computeSRP + 'computeSRP': computeSRP, + 'generate-dh': generateDh, + 'compute-dh-key': computeDhKey, + 'get-emojis-fingerprint': getEmojisFingerprint }; // Promise.all([ diff --git a/src/lib/crypto/generateDh.ts b/src/lib/crypto/generateDh.ts new file mode 100644 index 000000000..f8a817c54 --- /dev/null +++ b/src/lib/crypto/generateDh.ts @@ -0,0 +1,52 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import bigInt from "big-integer"; +import { bigIntFromBytes } from "../../helpers/bigInt/bigIntConversion"; +import addPadding from "../../helpers/bytes/addPadding"; +import bytesFromHex from "../../helpers/bytes/bytesFromHex"; +import { MessagesDhConfig } from "../../layer"; +import CallInstance from "../calls/callInstance"; +import cryptoWorker from "../crypto/cryptoworker"; + +export default async function generateDh(dhConfig: MessagesDhConfig.messagesDhConfig) { + const {p, g} = dhConfig; + + const generateA = (p: Uint8Array) => { + for(;;) { + const a = new Uint8Array(p.length).randomize(); + // const a = new Uint8Array(4).randomize(); + + const aBigInt = bigIntFromBytes(a); // str2bigInt(bytesToHex(a), 16); + if(!aBigInt.greater(bigInt.one)) { + continue; + } + + const pBigInt = bigIntFromBytes(p); // str2bigInt(bytesToHex(p), 16); + if(!aBigInt.lesser(pBigInt.subtract(bigInt.one))) { + continue; + } + + return a; + } + }; + + const a = generateA(p); + // const a = new Uint8Array([0]); + + const gBytes = bytesFromHex(g.toString(16)); + const g_a = addPadding(await cryptoWorker.invokeCrypto('mod-pow', gBytes, a, p), 256, true, true, true); + const g_a_hash = await cryptoWorker.invokeCrypto('sha256', g_a); + + const dh: CallInstance['dh'] = { + a: a, + g_a: g_a, + g_a_hash: g_a_hash, + p + }; + + return dh; +} diff --git a/src/lib/crypto/srp.ts b/src/lib/crypto/srp.ts index 79050fe0f..383ae151c 100644 --- a/src/lib/crypto/srp.ts +++ b/src/lib/crypto/srp.ts @@ -4,50 +4,36 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import CryptoWorker from "../crypto/cryptoworker"; -import {str2bigInt, isZero, - bigInt2str, powMod, int2bigInt, mult, mod, sub, bitSize, negative, add, greater} from '../../vendor/leemon'; - -import {logger, LogTypes} from '../logger'; +import cryptoWorker from "../crypto/cryptoworker"; import { AccountPassword, InputCheckPasswordSRP, PasswordKdfAlgo } from "../../layer"; -import { bufferConcats, bytesToHex, bytesFromHex, bytesXor, convertToUint8Array } from "../../helpers/bytes"; -import { addPadding } from "../mtproto/bin_utils"; -//import { MOUNT_CLASS_TO } from "../../config/debug"; - -const log = logger('SRP', LogTypes.Error); - -//MOUNT_CLASS_TO && Object.assign(MOUNT_CLASS_TO, {str2bigInt, bigInt2str, int2bigInt}); +import addPadding from "../../helpers/bytes/addPadding"; +import bufferConcats from "../../helpers/bytes/bufferConcats"; +import bytesXor from "../../helpers/bytes/bytesXor"; +import convertToUint8Array from "../../helpers/bytes/convertToUint8Array"; +import bigInt from 'big-integer'; +import { bigIntFromBytes, bigIntToBytes } from "../../helpers/bigInt/bigIntConversion"; +import bytesToHex from "../../helpers/bytes/bytesToHex"; export async function makePasswordHash(password: string, client_salt: Uint8Array, server_salt: Uint8Array) { // ! look into crypto_methods.test.ts - let buffer = await CryptoWorker.invokeCrypto('sha256-hash', bufferConcats(client_salt, new TextEncoder().encode(password), client_salt)); - //log('encoded 1', bytesToHex(new Uint8Array(buffer))); - + let buffer = await cryptoWorker.invokeCrypto('sha256', bufferConcats(client_salt, new TextEncoder().encode(password), client_salt)); buffer = bufferConcats(server_salt, buffer, server_salt); + buffer = await cryptoWorker.invokeCrypto('sha256', buffer); - buffer = await CryptoWorker.invokeCrypto('sha256-hash', buffer); - //log('encoded 2', buffer, bytesToHex(new Uint8Array(buffer))); - - let hash = await CryptoWorker.invokeCrypto('pbkdf2', new Uint8Array(buffer), client_salt, 100000); - //log('encoded 3', hash, bytesToHex(new Uint8Array(hash))); - + let hash = await cryptoWorker.invokeCrypto('pbkdf2', new Uint8Array(buffer), client_salt, 100000); hash = bufferConcats(server_salt, hash, server_salt); - buffer = await CryptoWorker.invokeCrypto('sha256-hash', hash); - //log('got password hash:', buffer, bytesToHex(new Uint8Array(buffer))); + buffer = await cryptoWorker.invokeCrypto('sha256', hash); return buffer; } -export async function computeSRP(password: string, state: AccountPassword, isNew: boolean) { +export default async function computeSRP(password: string, state: AccountPassword, isNew: boolean) { const algo = (isNew ? state.new_algo : state.current_algo) as PasswordKdfAlgo.passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow; - //console.log('computeSRP:', password, state, isNew, algo); - const p = str2bigInt(bytesToHex(algo.p), 16); - const g = int2bigInt(algo.g, 32, 256); - - //log('p', bigInt2str(p, 16)); - + const p = bigIntFromBytes(algo.p); + const g = bigInt(algo.g); + /* if(B.compareTo(BigInteger.ZERO) < 0) { console.error('srp_B < 0') } @@ -69,9 +55,7 @@ export async function computeSRP(password: string, state: AccountPassword, isNew //check_prime_and_good(algo.p, g); const pw_hash = await makePasswordHash(password, algo.salt1, algo.salt2); - const x = str2bigInt(bytesToHex(pw_hash), 16); - - //log('computed pw_hash:', pw_hash, x, bytesToHex(new Uint8Array(pw_hash))); + const x = bigInt(bytesToHex(pw_hash), 16); const padArray = function(arr: number[] | Uint8Array, len: number) { if(!(arr instanceof Uint8Array)) { @@ -81,7 +65,7 @@ export async function computeSRP(password: string, state: AccountPassword, isNew return addPadding(arr, len, true, true, true); }; - const v = powMod(g, x, p); + const v = g.modPow(x, p); const flipper = (arr: Uint8Array | number[]) => { const out = new Uint8Array(arr.length); @@ -97,125 +81,85 @@ export async function computeSRP(password: string, state: AccountPassword, isNew // * https://core.telegram.org/api/srp#setting-a-new-2fa-password if(isNew) { - const bytes = bytesFromHex(bigInt2str(v, 16)); + const bytes = bigIntToBytes(v); return padArray(/* (isBigEndian ? bytes.reverse() : bytes) */bytes, 256); } - const B = str2bigInt(bytesToHex(state.srp_B), 16); - //log('B', bigInt2str(B, 16)); + const B = bigIntFromBytes(state.srp_B); - const pForHash = padArray(bytesFromHex(bigInt2str(p, 16)), 256); - const gForHash = padArray(bytesFromHex(bigInt2str(g, 16)), 256); // like uint8array - const b_for_hash = padArray(bytesFromHex(bigInt2str(B, 16)), 256); - /* log(bytesToHex(pForHash)); - log(bytesToHex(gForHash)); - log(bytesToHex(b_for_hash)); */ - - //log('g_x', bigInt2str(g_x, 16)); - - const kHash = await CryptoWorker.invokeCrypto('sha256-hash', bufferConcats(pForHash, gForHash)); - const k = str2bigInt(bytesToHex(kHash), 16); + const pForHash = padArray(bigIntToBytes(p), 256); + const gForHash = padArray(bigIntToBytes(g), 256); + const b_for_hash = padArray(bigIntToBytes(B), 256); - //log('k', bigInt2str(k, 16)); + const kHash = await cryptoWorker.invokeCrypto('sha256', bufferConcats(pForHash, gForHash)); + const k = bigIntFromBytes(kHash); - // kg_x = (k * g_x) % p - const k_v = mod(mult(k, v), p); + const k_v = k.multiply(v).mod(p); - // good - - //log('kg_x', bigInt2str(kg_x, 16)); - - const is_good_mod_exp_first = (modexp: any, prime: any) => { - const diff = sub(prime, modexp); + const is_good_mod_exp_first = (modexp: bigInt.BigInteger, prime: bigInt.BigInteger) => { + const diff = prime.subtract(modexp); const min_diff_bits_count = 2048 - 64; const max_mod_exp_size = 256; - if(negative(diff) || - bitSize(diff) < min_diff_bits_count || - bitSize(modexp) < min_diff_bits_count || - Math.floor((bitSize(modexp) + 7) / 8) > max_mod_exp_size) + if(diff.isNegative() || + diff.bitLength().toJSNumber() < min_diff_bits_count || + modexp.bitLength().toJSNumber() < min_diff_bits_count || + Math.floor((modexp.bitLength().toJSNumber() + 7) / 8) > max_mod_exp_size) return false; return true; }; const generate_and_check_random = async() => { while(true) { - const a = str2bigInt(bytesToHex(flipper(state.secure_random)), 16); + const a = bigIntFromBytes(flipper(state.secure_random)); //const a = str2bigInt('9153faef8f2bb6da91f6e5bc96bc00860a530a572a0f45aac0842b4602d711f8bda8d59fb53705e4ae3e31a3c4f0681955425f224297b8e9efd898fec22046debb7ba8a0bcf2be1ada7b100424ea318fdcef6ccfe6d7ab7d978c0eb76a807d4ab200eb767a22de0d828bc53f42c5a35c2df6e6ceeef9a3487aae8e9ef2271f2f6742e83b8211161fb1a0e037491ab2c2c73ad63c8bd1d739de1b523fe8d461270cedcf240de8da75f31be4933576532955041dc5770c18d3e75d0b357df9da4a5c8726d4fced87d15752400883dc57fa1937ac17608c5446c4774dcd123676d683ce3a1ab9f7e020ca52faafc99969822717c8e07ea383d5fb1a007ba0d170cb', 16); - //console.log('ITERATION'); - - //log('g a p', bigInt2str(g, 16), bigInt2str(a, 16), bigInt2str(p, 16)); - - const A = powMod(g, a, p); - //log('A MODPOW', bigInt2str(A, 16)); + const A = g.modPow(a, p); if(is_good_mod_exp_first(A, p)) { - const a_for_hash = bytesFromHex(bigInt2str(A, 16)); + const a_for_hash = bigIntToBytes(A); - const s = await CryptoWorker.invokeCrypto('sha256-hash', bufferConcats(a_for_hash, b_for_hash)); - const u = str2bigInt(s.hex, 16); - if(!isZero(u) && !negative(u)) + const s = await cryptoWorker.invokeCrypto('sha256', bufferConcats(a_for_hash, b_for_hash)); + const u = bigInt(s.hex, 16); + if(!u.isZero() && !u.isNegative()) return {a, a_for_hash, u}; } } } - const {a, a_for_hash, u} = await generate_and_check_random(); - /* log('a', bigInt2str(a, 16)); - log('a_for_hash', bytesToHex(a_for_hash)); - log('u', bigInt2str(u, 16)); */ - - // g_b = (B - kg_x) % p - /* log('B - kg_x', bigInt2str(sub(B, kg_x), 16)); - log('subtract', bigInt2str(B, 16), bigInt2str(kg_x, 16)); - log('B - kg_x', bigInt2str(sub(B, kg_x), 16)); */ - - let g_b: number[]; - if(!greater(B, k_v)) { - //log('negative'); - g_b = add(B, p); + let g_b: bigInt.BigInteger; + if(!B.greater(k_v)) { + g_b = B.add(p); } else g_b = B; - g_b = mod(sub(g_b, k_v), p); - /* let g_b = sub(B, kg_x); - if(negative(g_b)) g_b = add(g_b, p); */ - - //log('g_b', bigInt2str(g_b, 16)); - - /* if(!is_good_mod_exp_first(g_b, p)) - throw new Error('bad g_b'); */ + g_b = g_b.subtract(k_v).mod(p); - const ux = mult(u, x); - //log('u and x multiply', bigInt2str(u, 16), bigInt2str(x, 16), bigInt2str(ux, 16)); - const a_ux = add(a, ux); - const S = powMod(g_b, a_ux, p); + const ux = u.multiply(x); + const a_ux = a.add(ux); + const S = g_b.modPow(a_ux, p); - const K = await CryptoWorker.invokeCrypto('sha256-hash', padArray(bytesFromHex(bigInt2str(S, 16)), 256)); + const K = await cryptoWorker.invokeCrypto('sha256', padArray(bigIntToBytes(S), 256)); - //log('K', bytesToHex(K), new Uint32Array(new Uint8Array(K).buffer)); - - let h1 = await CryptoWorker.invokeCrypto('sha256-hash', pForHash); - const h2 = await CryptoWorker.invokeCrypto('sha256-hash', gForHash); + let h1 = await cryptoWorker.invokeCrypto('sha256', pForHash); + const h2 = await cryptoWorker.invokeCrypto('sha256', gForHash); h1 = bytesXor(h1, h2); - const buff = bufferConcats(h1, - await CryptoWorker.invokeCrypto('sha256-hash', algo.salt1), - await CryptoWorker.invokeCrypto('sha256-hash', algo.salt2), + const buff = bufferConcats( + h1, + await cryptoWorker.invokeCrypto('sha256', algo.salt1), + await cryptoWorker.invokeCrypto('sha256', algo.salt2), a_for_hash, b_for_hash, K ); - const M1 = await CryptoWorker.invokeCrypto('sha256-hash', buff); + const M1 = await cryptoWorker.invokeCrypto('sha256', buff); - const out = { + const out: InputCheckPasswordSRP.inputCheckPasswordSRP = { _: 'inputCheckPasswordSRP', srp_id: state.srp_id, A: new Uint8Array(a_for_hash), M1 - } as InputCheckPasswordSRP.inputCheckPasswordSRP; - + }; - //log('out', bytesToHex(out.A), bytesToHex(out.M1)); return out; } diff --git a/src/lib/crypto/subtle.ts b/src/lib/crypto/subtle.ts new file mode 100644 index 000000000..871e268ee --- /dev/null +++ b/src/lib/crypto/subtle.ts @@ -0,0 +1,3 @@ +const subtle = typeof(window) !== 'undefined' && 'crypto' in window ? window.crypto.subtle : self.crypto.subtle; + +export default subtle; diff --git a/src/lib/crypto/utils/aesIGE.ts b/src/lib/crypto/utils/aesIGE.ts new file mode 100644 index 000000000..c4d9dc7bf --- /dev/null +++ b/src/lib/crypto/utils/aesIGE.ts @@ -0,0 +1,22 @@ +import {IGE} from '@cryptography/aes'; +import addPadding from '../../../helpers/bytes/addPadding'; +import bytesFromWordss from '../../../helpers/bytes/bytesFromWordss'; +import bytesToWordss from '../../../helpers/bytes/bytesToWordss'; + +export default function aesSync(bytes: Uint8Array, keyBytes: Uint8Array, ivBytes: Uint8Array, encrypt = true) { + //console.log(dT(), 'AES start', bytes, keyBytes, ivBytes); + + const cipher = new IGE(bytesToWordss(keyBytes), bytesToWordss(ivBytes)); + const performedBytes = cipher[encrypt ? 'encrypt' : 'decrypt'](bytesToWordss(bytes)); + //console.log(dT(), 'AES finish'); + + return bytesFromWordss(performedBytes); +} + +export function aesEncryptSync(bytes: Uint8Array, keyBytes: Uint8Array, ivBytes: Uint8Array) { + return aesSync(addPadding(bytes), keyBytes, ivBytes, true); +} + +export function aesDecryptSync(bytes: Uint8Array, keyBytes: Uint8Array, ivBytes: Uint8Array) { + return aesSync(bytes, keyBytes, ivBytes, false); +} diff --git a/src/lib/crypto/utils/factorize/BrentPollard.ts b/src/lib/crypto/utils/factorize/BrentPollard.ts new file mode 100644 index 000000000..3fed1c809 --- /dev/null +++ b/src/lib/crypto/utils/factorize/BrentPollard.ts @@ -0,0 +1,138 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +// Thanks to https://xn--2-umb.com/09/12/brent-pollard-rho-factorisation/ + +import bigInt from "big-integer"; +import { bigIntFromBytes, bigIntToBytes } from "../../../../helpers/bigInt/bigIntConversion"; +import bigIntRandom from "../../../../helpers/bigInt/bigIntRandom"; + +// let test = 0; +function BrentPollardFactor(n: bigInt.BigInteger) { + const two = bigInt[2]; + if(n.remainder(two).isZero()) { + return two; + } + + const m = bigInt(1000); + let a: bigInt.BigInteger, + x: bigInt.BigInteger, + y: bigInt.BigInteger, + ys: bigInt.BigInteger, + r: bigInt.BigInteger, + q: bigInt.BigInteger, + g: bigInt.BigInteger; + do + a = bigIntRandom(bigInt.one, n.minus(1)); + while(a.isZero() || a.eq(n.minus(two))); + y = bigIntRandom(bigInt.one, n.minus(1)); + r = bigInt.one; + q = bigInt.one; + + // if(!test++) { + // a = bigInt(3); + // y = bigInt(3); + // } + + const bigIntUint64 = bigInt('FFFFFFFFFFFFFFFF', 16); + const bigIntUint64MinusPqPlusOne = bigIntUint64.minus(n).plus(1); + + const performY = (y: bigInt.BigInteger) => { + y = y.pow(two).mod(n); + y = y.add(a); + if(y.lesser(a)) { // it slows down the script + y = y.add(bigIntUint64MinusPqPlusOne); + } + y = y.mod(n); + return y; + }; + + do { + x = y; + for(let i = 0; bigInt(i).lesser(r); ++i) { + y = performY(y); + } + + let k = bigInt.zero; + do { + ys = y; + const condition = bigInt.min(m, r.minus(k)); + for(let i = 0; bigInt(i).lesser(condition); ++i) { + y = performY(y); + q = q.multiply(x.greater(y) ? x.minus(y) : y.minus(x)).mod(n); + } + g = bigInt.gcd(q, n); + k = k.add(m); + } while(k.lesser(r) && g.eq(bigInt.one)); + + r = r.shiftLeft(bigInt.one); + } while(g.eq(bigInt.one)); + + if(g.eq(n)) { + do { + ys = performY(ys); + g = bigInt.gcd(x.minus(ys).abs(), n); + } while(g.eq(bigInt.one)); + } + + return g; +} + +function primeFactors(pqBytes: Uint8Array | number[]) { + const n = bigIntFromBytes(pqBytes); + + const factors: bigInt.BigInteger[] = []; + const primes: bigInt.BigInteger[] = []; + + let factor = BrentPollardFactor(n); + factors.push(n.divide(factor)); + factors.push(factor); + + // return [factor]; + + do { + const m = factors.pop(); + + if(m.eq(bigInt.one)) + continue; + + if(m.isPrime(true)) { + primes.push(m); + + // Remove the prime from the other factors + for(let i = 0; i < factors.length; ++i) { + let k = factors[i]; + if(k.mod(m).isZero()) { + do + k = k.divide(m); + while(k.mod(m).isZero()); + factors[i] = k; + } + } + } else { + // factor = m.lesser(100) ? bigInt(PollardRho(m.toJSNumber())) : this.brentPollardFactor(m); + factor = BrentPollardFactor(m); + factors.push(m.divide(factor)); + factors.push(factor); + } + } while(factors.length); + + return primes; +} + +export default function factorizeBrentPollardPQ(pqBytes: Uint8Array | number[]): [Uint8Array, Uint8Array] { + let factors = primeFactors(pqBytes); + factors.sort((a, b) => a.compare(b)); + if(factors.length > 2) { + factors = [ + factors.splice(factors.length - 2, 1)[0], + factors.reduce((acc, v) => acc.multiply(v), bigInt.one) + ]; + } + + const p = factors[0], q = factors[factors.length - 1]; + return (p.lesser(q) ? [p, q] : [q, p]).map(b => bigIntToBytes(b)) as any; +} diff --git a/src/lib/crypto/utils/factorize/tdlib.ts b/src/lib/crypto/utils/factorize/tdlib.ts new file mode 100644 index 000000000..6ae445176 --- /dev/null +++ b/src/lib/crypto/utils/factorize/tdlib.ts @@ -0,0 +1,141 @@ +// Thanks to https://github.com/tdlib/td/blob/3f54c301ead1bbe6529df4ecfb63c7f645dd181c/tdutils/td/utils/crypto.cpp#L234 + +import bigInt from "big-integer"; +import { bigIntFromBytes, bigIntToBytes } from "../../../../helpers/bigInt/bigIntConversion"; +import bigIntRandom from "../../../../helpers/bigInt/bigIntRandom"; +import { nextRandomUint } from "../../../../helpers/random"; + +export function factorizeSmallPQ(pq: bigInt.BigInteger) { + if(pq.lesser(2) || pq.greater(bigInt.one.shiftLeft(63))) { + return bigInt.one; + } + + let a: bigInt.BigInteger, + b: bigInt.BigInteger, + c: bigInt.BigInteger, + q: bigInt.BigInteger, + x: bigInt.BigInteger, + y: bigInt.BigInteger, + z: bigInt.BigInteger, + i: number, + iter: number, + lim: number, + j: number; + + let g = bigInt.zero; + for(i = 0, iter = 0; i < 3 || iter < 1000; ++i) { + q = bigIntRandom(15, 17).mod(pq.subtract(1)); // Random::fast(17, 32) % (pq - 1); + x = bigIntRandom(0, bigInt[2].pow(64)).mod(pq.subtract(1)).add(1); + y = bigInt(x); + lim = 1 << (Math.min(5, i) + 18); + for(j = 1; j < lim; ++j) { + ++iter; + a = bigInt(x); + b = bigInt(x); + c = bigInt(q); + + c = c.add(a).multiply(b).mod(pq); + // while(!b.isZero()) { + // if(!b.and(1).isZero()) { + // c = c.add(a); + // if(c.greaterOrEquals(pq)) { + // c = c.subtract(pq); + // } + // } + // a = a.add(a); + // if(a.greaterOrEquals(pq)) { + // a = a.subtract(pq); + // } + // b = b.shiftRight(1); + // } + + x = bigInt(c); + z = x.lesser(y) ? pq.add(x).subtract(y) : x.subtract(y); + g = bigInt.gcd(z, pq); + if(g.notEquals(bigInt.one)) { + break; + } + + if(!(j & (j - 1))) { + y = bigInt(x); + } + } + if(g.greater(bigInt.one) && g.lesser(pq)) { + break; + } + } + if(!g.isZero()) { + const other = pq.divide(g); + if(other.lesser(g)) { + g = other; + } + } + return g; +} + +export function factorizeBiqPQ(pqBytes: Uint8Array | number[]): [Uint8Array, Uint8Array] { + let q: bigInt.BigInteger, + p: bigInt.BigInteger, + b: bigInt.BigInteger; + + const pq = bigIntFromBytes(pqBytes); + + let found = false; + for(let i = 0, iter = 0; !found && (i < 3 || iter < 1000); i++) { + const t = bigIntRandom(17, 32); + let a = bigInt(nextRandomUint(32)); + let b = bigInt(a); + + const lim = 1 << (i + 23); + for(let j = 1; j < lim; j++) { + iter++; + a = a.mod(a).multiply(pq); // BigNum::mod_mul(a, a, a, pq, context); + + a = a.add(t); + if(a.compare(pq) >= 0) { + a = a.subtract(pq); + } + if(a.compare(b) > 0) { + q = a.subtract(b); + } else { + q = b.subtract(a); + } + p = bigInt.gcd(q, pq); + if(p.compare(bigInt.one) != 0) { + found = true; + break; + } + if((j & (j - 1)) == 0) { + b = bigInt(a); + } + } + } + + if(found) { + q = pq.divide(p); + if(p.compare(q) > 0) { + [p, q] = [q, p]; + } + + return [bigIntToBytes(p), bigIntToBytes(q)]; + } +} + +export default function factorizeTdlibPQ(pqBytes: Uint8Array | number[]): [Uint8Array, Uint8Array] { + const size = pqBytes.length; + if(size > 8 || (size === 8 && (pqBytes[0] & 128) != 0)) { + return factorizeBiqPQ(pqBytes); + } + + let pq = bigInt.zero; + for(let i = 0; i < size; i++) { + pq = pq.shiftLeft(8).or(pqBytes[i]); + } + + const p = factorizeSmallPQ(pq); + if(p.isZero() || pq.mod(p).notEquals(bigInt.zero)) { + return; + } + + return [bigIntToBytes(p), bigIntToBytes(pq.divide(p))]; +} diff --git a/src/lib/crypto/utils/factorize/vanillaPollandRho.ts b/src/lib/crypto/utils/factorize/vanillaPollandRho.ts new file mode 100644 index 000000000..9ea972be0 --- /dev/null +++ b/src/lib/crypto/utils/factorize/vanillaPollandRho.ts @@ -0,0 +1,66 @@ +// Thanks to https://www.geeksforgeeks.org/pollards-rho-algorithm-prime-factorization/ + +function modPow(base: number, exponent: number, modulus: number) { + /* initialize result */ + let result = 1; + + while(exponent > 0) { + /* if y is odd, multiply base with result */ + if(exponent % 2 == 1) + result = (result * base) % modulus; + + /* exponent = exponent/2 */ + exponent = exponent >> 1; + + /* base = base * base */ + base = (base * base) % modulus; + } + return result; +} + +/* method to return prime divisor for n */ +export default function factorizePollardRhoPQ(n: number): number { + /* no prime divisor for 1 */ + if(n === 1) + return n; + + /* even number means one of the divisors is 2 */ + if(n % 2 === 0) + return 2; + + /* we will pick from the range [2, N) */ + let x = Math.floor(Math.random() * (-n + 1)); + let y = x; + + /* the constant in f(x). + * Algorithm can be re-run with a different c + * if it throws failure for a composite. */ + let c = Math.floor(Math.random() * (-n + 1)); + + /* Initialize candidate divisor (or result) */ + let d = 1; + /* until the prime factor isn't obtained. + If n is prime, return n */ + while(d == 1) { + /* Tortoise Move: x(i+1) = f(x(i)) */ + x = (modPow(x, 2, n) + c + n) % n; + + /* Hare Move: y(i+1) = f(f(y(i))) */ + y = (modPow(y, 2, n) + c + n) % n; + y = (modPow(y, 2, n) + c + n) % n; + + /* check gcd of |x-y| and n */ + d = gcd(Math.abs(x - y), n); + + /* retry if the algorithm fails to find prime factor + * with chosen x and c */ + if(d === n) return factorizePollardRhoPQ(n); + } + + return d; +} + +// Recursive function to return gcd of a and b +function gcd(a: number, b: number): number { + return b == 0? a : gcd(b, a % b); +} diff --git a/src/lib/crypto/utils/pbkdf2.ts b/src/lib/crypto/utils/pbkdf2.ts new file mode 100644 index 000000000..dd99cddb9 --- /dev/null +++ b/src/lib/crypto/utils/pbkdf2.ts @@ -0,0 +1,39 @@ +import subtle from "../subtle"; + +export default async function pbkdf2(buffer: Parameters[1], salt: HkdfParams['salt'], iterations: number) { + const importKey = await subtle.importKey( + 'raw', + buffer, + {name: 'PBKDF2'}, + false, + [/* 'deriveKey', */'deriveBits'] + ); + + /* await subtle.deriveKey( + { + name: 'PBKDF2', + salt, + iterations, + hash: {name: 'SHA-512'} + }, + importKey, + { + name: 'AES-CTR', + length: 256 + }, + false, + ['encrypt', 'decrypt'] + ); */ + + const bits = subtle.deriveBits({ + name: 'PBKDF2', + salt, + iterations, + hash: {name: 'SHA-512'}, + }, + importKey, + 512 + ); + + return bits.then(buffer => new Uint8Array(buffer)); +} diff --git a/src/lib/crypto/utils/rsa.ts b/src/lib/crypto/utils/rsa.ts new file mode 100644 index 000000000..e9a091123 --- /dev/null +++ b/src/lib/crypto/utils/rsa.ts @@ -0,0 +1,7 @@ +import type { RSAPublicKeyHex } from "../../mtproto/rsaKeysManager"; +import bytesModPow from "../../../helpers/bytes/bytesModPow"; +import bytesFromHex from "../../../helpers/bytes/bytesFromHex"; + +export default function rsaEncrypt(bytes: Uint8Array, publicKey: RSAPublicKeyHex) { + return bytesModPow(bytes, bytesFromHex(publicKey.exponent), bytesFromHex(publicKey.modulus)); +} diff --git a/src/lib/crypto/utils/sha1.ts b/src/lib/crypto/utils/sha1.ts new file mode 100644 index 000000000..13ecd14bc --- /dev/null +++ b/src/lib/crypto/utils/sha1.ts @@ -0,0 +1,22 @@ +import convertToUint8Array from "../../../helpers/bytes/convertToUint8Array"; +import subtle from "../subtle"; +//import sha1 from '@cryptography/sha1'; + +export default function sha1(bytes: Parameters[0]) { + return subtle.digest('SHA-1', convertToUint8Array(bytes)).then(b => { + return new Uint8Array(b); + }); + /* //console.trace(dT(), 'SHA-1 hash start', bytes); + + const hashBytes: number[] = []; + + let hash = sha1(String.fromCharCode.apply(null, + bytes instanceof Uint8Array ? [...bytes] : [...new Uint8Array(bytes)])); + for(let i = 0; i < hash.length; ++i) { + hashBytes.push(hash.charCodeAt(i)); + } + + //console.log(dT(), 'SHA-1 hash finish', hashBytes, bytesToHex(hashBytes)); + + return new Uint8Array(hashBytes); */ +} diff --git a/src/lib/crypto/utils/sha256.ts b/src/lib/crypto/utils/sha256.ts new file mode 100644 index 000000000..5eb1be8ec --- /dev/null +++ b/src/lib/crypto/utils/sha256.ts @@ -0,0 +1,23 @@ +import convertToUint8Array from "../../../helpers/bytes/convertToUint8Array"; +import subtle from "../subtle"; +//import sha256 from '@cryptography/sha256'; + +export default function sha256(bytes: Parameters[0]) { + return subtle.digest('SHA-256', convertToUint8Array(bytes)).then(b => { + //console.log('legacy', performance.now() - perfS); + return new Uint8Array(b); + }); + /* //console.log('SHA-256 hash start'); + + let perfS = performance.now(); + + + let perfD = performance.now(); + let words = typeof(bytes) === 'string' ? bytes : bytesToWordss(bytes as any); + let hash = sha256(words); + console.log('darutkin', performance.now() - perfD); + + //console.log('SHA-256 hash finish', hash, sha256(words, 'hex')); + + return bytesFromWordss(hash); */ +} diff --git a/src/lib/idb.ts b/src/lib/idb.ts index 1250d1745..5d67fd722 100644 --- a/src/lib/idb.ts +++ b/src/lib/idb.ts @@ -12,7 +12,7 @@ import { Database } from '../config/databases'; import Modes from '../config/modes'; import { blobConstruct } from '../helpers/blob'; -import { safeAssign } from '../helpers/object'; +import safeAssign from '../helpers/object/safeAssign'; import { logger } from './logger'; /** diff --git a/src/lib/langPack.ts b/src/lib/langPack.ts index 8cb421aec..43dcfa021 100644 --- a/src/lib/langPack.ts +++ b/src/lib/langPack.ts @@ -5,7 +5,6 @@ */ import DEBUG, { MOUNT_CLASS_TO } from "../config/debug"; -import { deepEqual, safeAssign } from "../helpers/object"; import { capitalizeFirstLetter } from "../helpers/string"; import type lang from "../lang"; import type langSign from "../langSign"; @@ -17,6 +16,8 @@ import App from "../config/app"; import rootScope from "./rootScope"; import RichTextProcessor from "./richtextprocessor"; import { IS_MOBILE } from "../environment/userAgent"; +import deepEqual from "../helpers/object/deepEqual"; +import safeAssign from "../helpers/object/safeAssign"; export const langPack: {[actionType: string]: LangPackKey} = { "messageActionChatCreate": "ActionCreateGroup", diff --git a/src/lib/mtproto/apiFileManager.ts b/src/lib/mtproto/apiFileManager.ts index 41d802ffa..7cc8050f7 100644 --- a/src/lib/mtproto/apiFileManager.ts +++ b/src/lib/mtproto/apiFileManager.ts @@ -25,10 +25,10 @@ import FileManager from "../filemanager"; import { logger, LogTypes } from "../logger"; import apiManager from "./apiManager"; import { isWebpSupported } from "./mtproto.worker"; -import { bytesToHex } from "../../helpers/bytes"; import assumeType from "../../helpers/assumeType"; import ctx from "../../environment/ctx"; import noop from "../../helpers/noop"; +import bytesToHex from "../../helpers/bytes/bytesToHex"; type Delayed = { offset: number, diff --git a/src/lib/mtproto/apiManager.ts b/src/lib/mtproto/apiManager.ts index ab1d955c8..f3ba94b52 100644 --- a/src/lib/mtproto/apiManager.ts +++ b/src/lib/mtproto/apiManager.ts @@ -12,7 +12,6 @@ import type { UserAuth } from './mtproto_config'; import sessionStorage from '../sessionStorage'; import MTPNetworker, { MTMessage } from './networker'; -import { isObject } from './bin_utils'; import networkerFactory from './networkerFactory'; //import { telegramMeWebService } from './mtproto'; import authorizer from './authorizer'; @@ -21,7 +20,6 @@ import { logger } from '../logger'; import type { DcAuthKey, DcId, DcServerSalt, InvokeApiOptions } from '../../types'; import type { MethodDeclMap } from '../../layer'; import { CancellablePromise, deferredPromise } from '../../helpers/cancellablePromise'; -import { bytesFromHex, bytesToHex } from '../../helpers/bytes'; //import { clamp } from '../../helpers/number'; import { IS_SAFARI } from '../../environment/userAgent'; import App from '../../config/app'; @@ -38,6 +36,9 @@ import rootScope from '../rootScope'; /// #if MTPROTO_AUTO import transportController from './transports/controller'; +import bytesFromHex from '../../helpers/bytes/bytesFromHex'; +import bytesToHex from '../../helpers/bytes/bytesToHex'; +import isObject from '../../helpers/object/isObject'; /// #endif /* var networker = apiManager.cachedNetworkers.websocket.upload[2]; @@ -352,7 +353,7 @@ export class ApiManager { } const authKey = bytesFromHex(authKeyHex); - const authKeyId = (await CryptoWorker.invokeCrypto('sha1-hash', authKey)).slice(-8); + const authKeyId = (await CryptoWorker.invokeCrypto('sha1', authKey)).slice(-8); const serverSalt = bytesFromHex(serverSaltHex); networker = networkerFactory.getNetworker(dcId, authKey, authKeyId, serverSalt, options); diff --git a/src/lib/mtproto/authorizer.ts b/src/lib/mtproto/authorizer.ts index a391f4313..2a9adf25e 100644 --- a/src/lib/mtproto/authorizer.ts +++ b/src/lib/mtproto/authorizer.ts @@ -9,6 +9,10 @@ * https://github.com/zhukov/webogram/blob/master/LICENSE */ +/// #if MTPROTO_AUTO +import transportController from "./transports/controller"; +/// #endif + import { TLSerialization, TLDeserialization } from "./tl_utils"; import dcConfigurator, { TransportType } from "./dcConfigurator"; import rsaKeysManager from "./rsaKeysManager"; @@ -17,16 +21,16 @@ import timeManager from "./timeManager"; import CryptoWorker from "../crypto/cryptoworker"; import { logger, LogTypes } from "../logger"; -import { bytesCmp, bytesToHex, bytesFromHex, bytesXor } from "../../helpers/bytes"; import DEBUG from "../../config/debug"; -import { cmp, int2bigInt, one, pow, str2bigInt, sub } from "../../vendor/leemon"; -import { addPadding } from "./bin_utils"; import { Awaited, DcId } from "../../types"; import { ApiError } from "./apiManager"; - -/// #if MTPROTO_AUTO -import transportController from "./transports/controller"; -/// #endif +import addPadding from "../../helpers/bytes/addPadding"; +import bytesCmp from "../../helpers/bytes/bytesCmp"; +import bytesFromHex from "../../helpers/bytes/bytesFromHex"; +import bytesToHex from "../../helpers/bytes/bytesToHex"; +import bytesXor from "../../helpers/bytes/bytesXor"; +import { bigIntFromBytes } from "../../helpers/bigInt/bigIntConversion"; +import bigInt from "big-integer"; /* let fNewNonce: any = bytesFromHex('8761970c24cb2329b5b2459752c502f3057cb7e8dbab200e526e8767fdc73b3c').reverse(); let fNonce: any = bytesFromHex('b597720d11faa5914ef485c529cde414').reverse(); @@ -285,19 +289,19 @@ export class Authorizer { const getKeyAesEncrypted = async() => { for(;;) { const tempKey = new Uint8Array(32).randomize(); - const dataWithHash = dataPadReversed.concat(await CryptoWorker.invokeCrypto('sha256-hash', tempKey.concat(dataWithPadding))); + const dataWithHash = dataPadReversed.concat(await CryptoWorker.invokeCrypto('sha256', tempKey.concat(dataWithPadding))); if(dataWithHash.length !== 224) { throw 'DH_params: dataWithHash !== 224 bytes!'; } const aesEncrypted = await CryptoWorker.invokeCrypto('aes-encrypt', dataWithHash, tempKey, new Uint8Array([0])); - const tempKeyXor = bytesXor(tempKey, await CryptoWorker.invokeCrypto('sha256-hash', aesEncrypted)); + const tempKeyXor = bytesXor(tempKey, await CryptoWorker.invokeCrypto('sha256', aesEncrypted)); const keyAesEncrypted = tempKeyXor.concat(aesEncrypted); - const keyAesEncryptedBigInt = str2bigInt(bytesToHex(keyAesEncrypted), 16); - const publicKeyModulusBigInt = str2bigInt(auth.publicKey.modulus, 16); + const keyAesEncryptedBigInt = bigIntFromBytes(keyAesEncrypted); + const publicKeyModulusBigInt = bigInt(auth.publicKey.modulus, 16); - if(cmp(keyAesEncryptedBigInt, publicKeyModulusBigInt) === -1) { + if(keyAesEncryptedBigInt.compare(publicKeyModulusBigInt) === -1) { return keyAesEncrypted; } } @@ -351,7 +355,7 @@ export class Authorizer { } if(response._ === 'server_DH_params_fail') { - const newNonceHash = (await CryptoWorker.invokeCrypto('sha1-hash', auth.newNonce)).slice(-16); + const newNonceHash = (await CryptoWorker.invokeCrypto('sha1', auth.newNonce)).slice(-16); if(!bytesCmp(newNonceHash, response.new_nonce_hash)) { throw new Error('[MT] server_DH_params_fail new_nonce_hash mismatch'); } @@ -376,11 +380,11 @@ export class Authorizer { auth.localTime = Date.now(); // ! can't concat Array with Uint8Array! - auth.tmpAesKey = (await CryptoWorker.invokeCrypto('sha1-hash', auth.newNonce.concat(auth.serverNonce))) - .concat((await CryptoWorker.invokeCrypto('sha1-hash', auth.serverNonce.concat(auth.newNonce))).slice(0, 12)); + auth.tmpAesKey = (await CryptoWorker.invokeCrypto('sha1', auth.newNonce.concat(auth.serverNonce))) + .concat((await CryptoWorker.invokeCrypto('sha1', auth.serverNonce.concat(auth.newNonce))).slice(0, 12)); - auth.tmpAesIv = (await CryptoWorker.invokeCrypto('sha1-hash', auth.serverNonce.concat(auth.newNonce))).slice(12) - .concat(await CryptoWorker.invokeCrypto('sha1-hash', auth.newNonce.concat(auth.newNonce)), auth.newNonce.slice(0, 4)); + auth.tmpAesIv = (await CryptoWorker.invokeCrypto('sha1', auth.serverNonce.concat(auth.newNonce))).slice(12) + .concat(await CryptoWorker.invokeCrypto('sha1', auth.newNonce.concat(auth.newNonce)), auth.newNonce.slice(0, 4)); const answerWithHash = new Uint8Array(await CryptoWorker.invokeCrypto('aes-decrypt', encryptedAnswer, auth.tmpAesKey, auth.tmpAesIv)); @@ -415,8 +419,8 @@ export class Authorizer { const offset = deserializer.getOffset(); - if(!bytesCmp(hash, await CryptoWorker.invokeCrypto('sha1-hash', answerWithPadding.slice(0, offset)))) { - throw new Error('[MT] server_DH_inner_data SHA1-hash mismatch'); + if(!bytesCmp(hash, await CryptoWorker.invokeCrypto('sha1', answerWithPadding.slice(0, offset)))) { + throw new Error('[MT] server_DH_inner_data SHA1 mismatch'); } timeManager.applyServerTime(auth.serverTime, auth.localTime); @@ -437,14 +441,14 @@ export class Authorizer { this.log('dhPrime cmp OK'); } - const _gABigInt = str2bigInt(bytesToHex(gA), 16); - const _dhPrimeBigInt = str2bigInt(dhPrimeHex, 16); + const gABigInt = bigIntFromBytes(gA); + const dhPrimeBigInt = bigInt(dhPrimeHex, 16); - if(cmp(_gABigInt, one) <= 0) { + if(gABigInt.compare(bigInt.one) <= 0) { throw new Error('[MT] DH params are not verified: gA <= 1'); } - if(cmp(_gABigInt, sub(_dhPrimeBigInt, one)) >= 0) { + if(gABigInt.compare(dhPrimeBigInt.subtract(bigInt.one)) >= 0) { throw new Error('[MT] DH params are not verified: gA >= dhPrime - 1'); } @@ -452,13 +456,12 @@ export class Authorizer { this.log('1 < gA < dhPrime-1 OK'); } - const _two = int2bigInt(2, 32, 0); - const _twoPow = pow(_two, 2048 - 64); + const twoPow = bigInt(2).pow(2048 - 64); - if(cmp(_gABigInt, _twoPow) < 0) { + if(gABigInt.compare(twoPow) < 0) { throw new Error('[MT] DH params are not verified: gA < 2^{2048-64}'); } - if(cmp(_gABigInt, sub(_dhPrimeBigInt, _twoPow)) >= 0) { + if(gABigInt.compare(dhPrimeBigInt.subtract(twoPow)) >= 0) { throw new Error('[MT] DH params are not verified: gA > dhPrime - 2^{2048-64}'); } @@ -491,7 +494,7 @@ export class Authorizer { g_b: gB }, 'Client_DH_Inner_Data'); - const dataWithHash = (await CryptoWorker.invokeCrypto('sha1-hash', data.getBuffer())).concat(data.getBytes(true)); + const dataWithHash = (await CryptoWorker.invokeCrypto('sha1', data.getBuffer())).concat(data.getBytes(true)); const encryptedData = await CryptoWorker.invokeCrypto('aes-encrypt', dataWithHash, auth.tmpAesKey, auth.tmpAesIv); const request = new TLSerialization({mtproto: true}); @@ -533,7 +536,7 @@ export class Authorizer { throw authKey; } - const authKeyHash = await CryptoWorker.invokeCrypto('sha1-hash', authKey), + const authKeyHash = await CryptoWorker.invokeCrypto('sha1', authKey), authKeyAux = authKeyHash.slice(0, 8), authKeyId = authKeyHash.slice(-8); @@ -542,7 +545,7 @@ export class Authorizer { } switch(response._) { case 'dh_gen_ok': { - const newNonceHash1 = (await CryptoWorker.invokeCrypto('sha1-hash', auth.newNonce.concat([1], authKeyAux))).slice(-16); + const newNonceHash1 = (await CryptoWorker.invokeCrypto('sha1', auth.newNonce.concat([1], authKeyAux))).slice(-16); if(!bytesCmp(newNonceHash1, response.new_nonce_hash1)) { this.log.error('Set_client_DH_params_answer new_nonce_hash1 mismatch', newNonceHash1, response); @@ -562,7 +565,7 @@ export class Authorizer { } case 'dh_gen_retry': { - const newNonceHash2 = (await CryptoWorker.invokeCrypto('sha1-hash', auth.newNonce.concat([2], authKeyAux))).slice(-16); + const newNonceHash2 = (await CryptoWorker.invokeCrypto('sha1', auth.newNonce.concat([2], authKeyAux))).slice(-16); if(!bytesCmp(newNonceHash2, response.new_nonce_hash2)) { throw new Error('[MT] Set_client_DH_params_answer new_nonce_hash2 mismatch'); } @@ -571,7 +574,7 @@ export class Authorizer { } case 'dh_gen_fail': { - const newNonceHash3 = (await CryptoWorker.invokeCrypto('sha1-hash', auth.newNonce.concat([3], authKeyAux))).slice(-16); + const newNonceHash3 = (await CryptoWorker.invokeCrypto('sha1', auth.newNonce.concat([3], authKeyAux))).slice(-16); if(!bytesCmp(newNonceHash3, response.new_nonce_hash3)) { throw new Error('[MT] Set_client_DH_params_answer new_nonce_hash3 mismatch'); } diff --git a/src/lib/mtproto/bin_utils.ts b/src/lib/mtproto/bin_utils.ts index efb0ebeac..ec886ff1c 100644 --- a/src/lib/mtproto/bin_utils.ts +++ b/src/lib/mtproto/bin_utils.ts @@ -9,34 +9,6 @@ * https://github.com/zhukov/webogram/blob/master/LICENSE */ -import { bufferConcats } from '../../helpers/bytes'; -import { add_, bigInt2str, cmp, leftShift_, str2bigInt } from '../../vendor/leemon'; - -/// #if !MTPROTO_WORKER -// @ts-ignore -import pako from 'pako/dist/pako_inflate.min.js'; - -export function gzipUncompress(bytes: ArrayBuffer, toString: true): string; -export function gzipUncompress(bytes: ArrayBuffer, toString?: false): Uint8Array; -export function gzipUncompress(bytes: ArrayBuffer, toString?: boolean): string | Uint8Array { - //console.log(dT(), 'Gzip uncompress start'); - var result = pako.inflate(bytes, toString ? {to: 'string'} : undefined); - //console.log(dT(), 'Gzip uncompress finish'/* , result */); - return result; -} -/// #endif - -export function isObject(object: any) { - return typeof(object) === 'object' && object !== null; -} - -/* export function bigint(num: number) { - return new BigInteger(num.toString(16), 16); -} */ - -/* export function bigStringInt(strNum: string) { - return new BigInteger(strNum, 10); -} */ /* export function base64ToBlob(base64str: string, mimeType: string) { var sliceSize = 1024; @@ -70,10 +42,7 @@ export function dataUrlToBlob(url: string) { return blob; } */ -export function intToUint(val: number) { - // return val < 0 ? val + 4294967296 : val; // 0 <= val <= Infinity - return val >>> 0; // (4294967296 >>> 0) === 0; 0 <= val <= 4294967295 -} +export {}; /* export function bytesFromBigInt(bigInt: BigInteger, len?: number) { var bytes = bigInt.toByteArray(); @@ -97,67 +66,6 @@ export function intToUint(val: number) { return bytes; } */ -export function longFromInts(high: number, low: number): string { - //let perf = performance.now(); - //let str = bigint(high).shiftLeft(32).add(bigint(low)).toString(10); - //console.log('longFromInts jsbn', performance.now() - perf); - high = intToUint(high); - low = intToUint(low); - - //perf = performance.now(); - const bigInt = str2bigInt(high.toString(16), 16, 32);//int2bigInt(high, 64, 64); - //console.log('longFromInts construct high', bigint(high).toString(10), bigInt2str(bigInt, 10)); - leftShift_(bigInt, 32); - //console.log('longFromInts shiftLeft', bigint(high).shiftLeft(32).toString(10), bigInt2str(bigInt, 10)); - add_(bigInt, str2bigInt(low.toString(16), 16, 32)); - const _str = bigInt2str(bigInt, 10); - - //console.log('longFromInts leemon', performance.now() - perf); - - //console.log('longFromInts', high, low, str, _str, str === _str); - return _str; -} - -export function sortLongsArray(arr: string[]) { - return arr.map(long => { - return str2bigInt(long, 10); - }).sort((a, b) => { - return cmp(a, b); - }).map(bigInt => { - return bigInt2str(bigInt, 10); - }); -} - -export function addPadding( - bytes: T, - blockSize: number = 16, - zeroes?: boolean, - blockSizeAsTotalLength = false, - prepend = false -): T { - const len = (bytes as ArrayBuffer).byteLength || (bytes as Uint8Array).length; - const needPadding = blockSizeAsTotalLength ? blockSize - len : blockSize - (len % blockSize); - if(needPadding > 0 && needPadding < blockSize) { - ////console.log('addPadding()', len, blockSize, needPadding); - const padding = new Uint8Array(needPadding); - if(zeroes) { - for(let i = 0; i < needPadding; ++i) { - padding[i] = 0; - } - } else { - padding.randomize(); - } - if(bytes instanceof ArrayBuffer) { - return (prepend ? bufferConcats(padding, bytes) : bufferConcats(bytes, padding)).buffer as T; - } else if(bytes instanceof Uint8Array) { - return (prepend ? bufferConcats(padding, bytes) : bufferConcats(bytes, padding)) as T; - } else { - // @ts-ignore - return (prepend ? [...padding].concat(bytes) : bytes.concat([...padding])) as T; - } - } - return bytes; -} diff --git a/src/lib/mtproto/dcConfigurator.ts b/src/lib/mtproto/dcConfigurator.ts index 14d0bd8ea..9f398436c 100644 --- a/src/lib/mtproto/dcConfigurator.ts +++ b/src/lib/mtproto/dcConfigurator.ts @@ -11,8 +11,8 @@ import MTTransport, { MTConnectionConstructable } from './transports/transport'; import Modes from '../../config/modes'; -import { indexOfAndSplice } from '../../helpers/array'; import App from '../../config/app'; +import indexOfAndSplice from '../../helpers/array/indexOfAndSplice'; /// #if MTPROTO_HAS_HTTP import HTTP from './transports/http'; diff --git a/src/lib/mtproto/mtproto.worker.ts b/src/lib/mtproto/mtproto.worker.ts index 12e163f04..f272ee201 100644 --- a/src/lib/mtproto/mtproto.worker.ts +++ b/src/lib/mtproto/mtproto.worker.ts @@ -19,8 +19,8 @@ import { notifyAll } from '../../helpers/context'; import CacheStorageController from '../cacheStorage'; import sessionStorage from '../sessionStorage'; import { socketsProxied } from './transports/socketProxied'; -import { bytesToHex } from '../../helpers/bytes'; import ctx from '../../environment/ctx'; +import bytesToHex from '../../helpers/bytes/bytesToHex'; let webpSupported = false; export const isWebpSupported = () => { diff --git a/src/lib/mtproto/mtprotoworker.ts b/src/lib/mtproto/mtprotoworker.ts index 162b0f8fb..3be8a688d 100644 --- a/src/lib/mtproto/mtprotoworker.ts +++ b/src/lib/mtproto/mtprotoworker.ts @@ -10,7 +10,6 @@ import type { Awaited, InvokeApiOptions, WorkerTaskVoidTemplate } from '../../ty import type { Config, InputFile, MethodDeclMap, User } from '../../layer'; import MTProtoWorker from 'worker-loader!./mtproto.worker'; //import './mtproto.worker'; -import { isObject } from '../../helpers/object'; import CryptoWorkerMethods, { CryptoMethods } from '../crypto/crypto_methods'; import { logger } from '../logger'; import rootScope from '../rootScope'; @@ -32,6 +31,7 @@ import { CacheStorageDbName } from '../cacheStorage'; import { pause } from '../../helpers/schedulers/pause'; import IS_WEBP_SUPPORTED from '../../environment/webpSupport'; import type { ApiError } from './apiManager'; +import isObject from '../../helpers/object/isObject'; type Task = { taskId: number, diff --git a/src/lib/mtproto/networker.ts b/src/lib/mtproto/networker.ts index ae8283065..19f22a361 100644 --- a/src/lib/mtproto/networker.ts +++ b/src/lib/mtproto/networker.ts @@ -9,7 +9,6 @@ * https://github.com/zhukov/webogram/blob/master/LICENSE */ -import {isObject, sortLongsArray} from './bin_utils'; import {TLDeserialization, TLSerialization} from './tl_utils'; import CryptoWorker from '../crypto/cryptoworker'; import sessionStorage from '../sessionStorage'; @@ -18,9 +17,8 @@ import timeManager from './timeManager'; import networkerFactory from './networkerFactory'; import { logger, LogTypes } from '../logger'; import { InvokeApiOptions } from '../../types'; -import { longToBytes } from '../crypto/crypto_utils'; +import longToBytes from '../../helpers/long/longToBytes'; import MTTransport from './transports/transport'; -import { convertToUint8Array, bytesCmp, bytesToHex, bufferConcats } from '../../helpers/bytes'; import { nextRandomUint, randomLong } from '../../helpers/random'; import App from '../../config/app'; import DEBUG from '../../config/debug'; @@ -32,11 +30,17 @@ import HTTP from './transports/http'; /// #endif import type TcpObfuscated from './transports/tcpObfuscated'; -import { bigInt2str, rightShift_, str2bigInt } from '../../vendor/leemon'; -import { forEachReverse } from '../../helpers/array'; +import bigInt from 'big-integer'; import { ConnectionStatus } from './connectionStatus'; import ctx from '../../environment/ctx'; import dcConfigurator, { DcConfigurator } from './dcConfigurator'; +import bufferConcats from '../../helpers/bytes/bufferConcats'; +import bytesCmp from '../../helpers/bytes/bytesCmp'; +import bytesToHex from '../../helpers/bytes/bytesToHex'; +import convertToUint8Array from '../../helpers/bytes/convertToUint8Array'; +import isObject from '../../helpers/object/isObject'; +import forEachReverse from '../../helpers/array/forEachReverse'; +import sortLongsArray from '../../helpers/long/sortLongsArray'; //console.error('networker included!', new Error().stack); @@ -843,7 +847,7 @@ export default class MTPNetworker { const x = isOut ? 0 : 8; const msgKeyLargePlain = bufferConcats(this.authKeyUint8.subarray(88 + x, 88 + x + 32), dataWithPadding); - const msgKeyLarge = await CryptoWorker.invokeCrypto('sha256-hash', msgKeyLargePlain); + const msgKeyLarge = await CryptoWorker.invokeCrypto('sha256', msgKeyLargePlain); const msgKey = new Uint8Array(msgKeyLarge).subarray(8, 24); return msgKey; }; @@ -857,11 +861,11 @@ export default class MTPNetworker { sha2aText.set(msgKey, 0); sha2aText.set(this.authKeyUint8.subarray(x, x + 36), 16); - promises.push(CryptoWorker.invokeCrypto('sha256-hash', sha2aText)); + promises.push(CryptoWorker.invokeCrypto('sha256', sha2aText)); sha2bText.set(this.authKeyUint8.subarray(40 + x, 40 + x + 36), 0); sha2bText.set(msgKey, 36); - promises.push(CryptoWorker.invokeCrypto('sha256-hash', sha2bText)); + promises.push(CryptoWorker.invokeCrypto('sha256', sha2bText)); return Promise.all(promises).then((results) => { const aesKey = new Uint8Array(32); @@ -1594,10 +1598,7 @@ export default class MTPNetworker { case 32: // * msg_seqno too low case 33: // * msg_seqno too high case 64: { // * invalid container - //const changedOffset = timeManager.applyServerTime(bigStringInt(messageId).shiftRight(32).toString(10)); - const bigInt = str2bigInt(messageId, 10); - rightShift_(bigInt, 32); - const changedOffset = timeManager.applyServerTime(+bigInt2str(bigInt, 10)); + const changedOffset = timeManager.applyServerTime(bigInt(messageId).shiftRight(32).toJSNumber()); if(message.error_code === 17 || changedOffset) { this.log('Update session'); this.updateSession(); diff --git a/src/lib/mtproto/networkerFactory.ts b/src/lib/mtproto/networkerFactory.ts index f2d498b65..f9e9fd108 100644 --- a/src/lib/mtproto/networkerFactory.ts +++ b/src/lib/mtproto/networkerFactory.ts @@ -14,7 +14,7 @@ import MTPNetworker from "./networker"; import { InvokeApiOptions } from "../../types"; import App from "../../config/app"; import { MOUNT_CLASS_TO } from "../../config/debug"; -import { indexOfAndSplice } from "../../helpers/array"; +import indexOfAndSplice from "../../helpers/array/indexOfAndSplice"; export class NetworkerFactory { private networkers: MTPNetworker[] = []; diff --git a/src/lib/mtproto/referenceDatabase.ts b/src/lib/mtproto/referenceDatabase.ts index a925ba4c5..9accb6346 100644 --- a/src/lib/mtproto/referenceDatabase.ts +++ b/src/lib/mtproto/referenceDatabase.ts @@ -7,12 +7,12 @@ import { RefreshReferenceTask, RefreshReferenceTaskResponse } from "./apiFileManager"; import appMessagesManager from "../appManagers/appMessagesManager"; import { Photo } from "../../layer"; -import { bytesToHex } from "../../helpers/bytes"; -import { deepEqual } from "../../helpers/object"; import { MOUNT_CLASS_TO } from "../../config/debug"; import apiManager from "./mtprotoworker"; import assumeType from "../../helpers/assumeType"; import { logger } from "../logger"; +import bytesToHex from "../../helpers/bytes/bytesToHex"; +import deepEqual from "../../helpers/object/deepEqual"; export type ReferenceContext = ReferenceContext.referenceContextProfilePhoto | ReferenceContext.referenceContextMessage; export namespace ReferenceContext { diff --git a/src/lib/mtproto/rsaKeysManager.ts b/src/lib/mtproto/rsaKeysManager.ts index 5fb6173f0..9a15dcd9d 100644 --- a/src/lib/mtproto/rsaKeysManager.ts +++ b/src/lib/mtproto/rsaKeysManager.ts @@ -11,9 +11,10 @@ import { TLSerialization } from "./tl_utils"; import CryptoWorker from '../crypto/cryptoworker'; -import { bytesFromHex, bytesToHex } from "../../helpers/bytes"; -import { bigInt2str, str2bigInt } from "../../vendor/leemon"; import Modes from "../../config/modes"; +import bytesFromHex from "../../helpers/bytes/bytesFromHex"; +import bytesToHex from "../../helpers/bytes/bytesToHex"; +import bigInt from 'big-integer'; export type RSAPublicKeyHex = { modulus: string, @@ -102,7 +103,7 @@ export class RSAKeysManager { const buffer = RSAPublicKey.getBuffer(); - return CryptoWorker.invokeCrypto('sha1-hash', buffer).then(bytes => { + return CryptoWorker.invokeCrypto('sha1', buffer).then(bytes => { const fingerprintBytes = bytes.slice(-8); fingerprintBytes.reverse(); @@ -123,8 +124,7 @@ export class RSAKeysManager { await this.prepare(); for(let i = 0; i < fingerprints.length; ++i) { - //fingerprintHex = bigStringInt(fingerprints[i]).toString(16); - let fingerprintHex = bigInt2str(str2bigInt(fingerprints[i], 10), 16).toLowerCase(); + let fingerprintHex = bigInt(fingerprints[i]).toString(16).toLowerCase(); if(fingerprintHex.length < 16) { fingerprintHex = new Array(16 - fingerprintHex.length).fill('0').join('') + fingerprintHex; diff --git a/src/lib/mtproto/timeManager.ts b/src/lib/mtproto/timeManager.ts index 94f9f364a..380cb3d16 100644 --- a/src/lib/mtproto/timeManager.ts +++ b/src/lib/mtproto/timeManager.ts @@ -10,11 +10,11 @@ */ import sessionStorage from '../sessionStorage'; -import { longFromInts } from './bin_utils'; import { nextRandomUint } from '../../helpers/random'; import { MOUNT_CLASS_TO } from '../../config/debug'; import { WorkerTaskVoidTemplate } from '../../types'; import { notifySomeone } from '../../helpers/context'; +import longFromInts from '../../helpers/long/longFromInts'; /* let lol: any = {}; diff --git a/src/lib/mtproto/tl_utils.ts b/src/lib/mtproto/tl_utils.ts index 69bba4a1d..f00f98718 100644 --- a/src/lib/mtproto/tl_utils.ts +++ b/src/lib/mtproto/tl_utils.ts @@ -9,16 +9,13 @@ * https://github.com/zhukov/webogram/blob/master/LICENSE */ -import { bytesToHex } from '../../helpers/bytes'; -import { isObject, longFromInts } from './bin_utils'; import { MOUNT_CLASS_TO } from '../../config/debug'; -import { str2bigInt, dup, divide_, bigInt2str } from '../../vendor/leemon'; import Schema, { MTProtoConstructor } from './schema'; - -/// #if MTPROTO_WORKER -// @ts-ignore -import { gzipUncompress } from '../crypto/crypto_utils'; -/// #endif +import bytesToHex from '../../helpers/bytes/bytesToHex'; +import isObject from '../../helpers/object/isObject'; +import gzipUncompress from '../../helpers/gzipUncompress'; +import bigInt from 'big-integer'; +import longFromInts from '../../helpers/long/longFromInts'; const boolFalse = +Schema.API.constructors.find(c => c.predicate === 'boolFalse').id; const boolTrue = +Schema.API.constructors.find(c => c.predicate === 'boolTrue').id; @@ -155,26 +152,10 @@ class TLSerialization { sLong = sLong ? sLong.toString() : '0'; } - const R = 0x100000000; - //const divRem = bigStringInt(sLong).divideAndRemainder(bigint(R)); - - const a = str2bigInt(sLong, 10, 64); - const q = dup(a); - const r = dup(a); - divide_(a, str2bigInt((R).toString(16), 16, 64), q, r); - //divInt_(a, R); - - const high = +bigInt2str(q, 10); - let low = +bigInt2str(r, 10); + const {quotient, remainder} = bigInt(sLong).divmod(0x100000000); + const high = quotient.toJSNumber(); + const low = remainder.toJSNumber(); - if(high < low) { - low -= R; - } - - //console.log('storeLong', sLong, divRem[0].intValue(), divRem[1].intValue(), high, low); - - //this.writeInt(divRem[1].intValue(), (field || '') + ':long[low]'); - //this.writeInt(divRem[0].intValue(), (field || '') + ':long[high]'); this.writeInt(low, (field || '') + ':long[low]'); this.writeInt(high, (field || '') + ':long[high]'); } diff --git a/src/lib/mtproto/transports/obfuscation.ts b/src/lib/mtproto/transports/obfuscation.ts index ae2e7dc2b..577078d20 100644 --- a/src/lib/mtproto/transports/obfuscation.ts +++ b/src/lib/mtproto/transports/obfuscation.ts @@ -6,22 +6,23 @@ //import aesjs from 'aes-js'; import AES from "@cryptography/aes"; -import { bytesFromWordss } from "../../../helpers/bytes"; +import bytesFromWordss from "../../../helpers/bytes/bytesFromWordss"; import { Codec } from "./codec"; class Counter { - _counter: Uint8Array; + public counter: Uint8Array; constructor(initialValue: Uint8Array) { - this._counter = initialValue; + this.counter = initialValue; } - increment() { - for(let i = 15; i >= 0; i--) { - if(this._counter[i] === 255) { - this._counter[i] = 0; + public increment() { + const counter = this.counter; + for(let i = 15; i >= 0; --i) { + if(counter[i] === 255) { + counter[i] = 0; } else { - this._counter[i]++; + ++counter[i]; break; } } @@ -29,27 +30,28 @@ class Counter { } class CTR { - _counter: Counter; - _remainingCounter: Uint8Array = null; - _remainingCounterIndex = 16; - _aes: AES; + #counter: Counter; + #remainingCounter: Uint8Array; + #remainingCounterIndex: number; + #aes: AES; constructor(key: Uint8Array, counter: Uint8Array) { - this._counter = new Counter(counter); - this._aes = new AES(key); + this.#counter = new Counter(counter); + this.#aes = new AES(key); + this.#remainingCounterIndex = 16; } - update(payload: Uint8Array) { + public update(payload: Uint8Array) { const encrypted = payload.slice(); - for(let i = 0; i < encrypted.length; i++) { - if(this._remainingCounterIndex === 16) { - this._remainingCounter = new Uint8Array(bytesFromWordss(this._aes.encrypt(this._counter._counter))); - this._remainingCounterIndex = 0; - this._counter.increment(); + for(let i = 0; i < encrypted.length; ++i) { + if(this.#remainingCounterIndex === 16) { + this.#remainingCounter = new Uint8Array(bytesFromWordss(this.#aes.encrypt(this.#counter.counter))); + this.#remainingCounterIndex = 0; + this.#counter.increment(); } - encrypted[i] ^= this._remainingCounter[this._remainingCounterIndex++]; + encrypted[i] ^= this.#remainingCounter[this.#remainingCounterIndex++]; } return encrypted; @@ -60,19 +62,21 @@ class CTR { @cryptography/aes не работает с массивами которые не кратны 4, поэтому использую intermediate а не abridged */ export default class Obfuscation { - /* public enc: aesjs.ModeOfOperation.ModeOfOperationCTR; - public dec: aesjs.ModeOfOperation.ModeOfOperationCTR; */ + /* private enc: aesjs.ModeOfOperation.ModeOfOperationCTR; + private dec: aesjs.ModeOfOperation.ModeOfOperationCTR; */ - public encNew: CTR; - public decNew: CTR; + private encNew: CTR; + private decNew: CTR; + // private cryptoEncKey: CryptoKey; + // encIv: Uint8Array; - public init(codec: Codec) { + public /* async */ init(codec: Codec) { const initPayload = new Uint8Array(64); initPayload.randomize(); while(true) { - let val = (initPayload[3] << 24) | (initPayload[2] << 16) | (initPayload[1] << 8) | (initPayload[0]); - let val2 = (initPayload[7] << 24) | (initPayload[6] << 16) | (initPayload[5] << 8) | (initPayload[4]); + const val = (initPayload[3] << 24) | (initPayload[2] << 16) | (initPayload[1] << 8) | initPayload[0]; + const val2 = (initPayload[7] << 24) | (initPayload[6] << 16) | (initPayload[5] << 8) | initPayload[4]; if(initPayload[0] !== 0xef && val !== 0x44414548 && val !== 0x54534f50 && @@ -94,7 +98,7 @@ export default class Obfuscation { const reversedPayload = initPayload.slice().reverse(); const encKey = initPayload.slice(8, 40); - const encIv = initPayload.slice(40, 56); + const encIv = /* this.encIv = */initPayload.slice(40, 56); const decKey = reversedPayload.slice(8, 40); const decIv = reversedPayload.slice(40, 56); @@ -107,8 +111,16 @@ export default class Obfuscation { this.encNew = new CTR(encKey, encIv); this.decNew = new CTR(decKey, decIv); + /* const key = this.cryptoEncKey = await subtle.importKey( + 'raw', + encKey, + {name: 'AES-CTR'}, + false, + ['encrypt'] + ); */ + initPayload.set(codec.obfuscateTag, 56); - const encrypted = this.encode(initPayload); + const encrypted = /* await */ this.encode(initPayload); //console.log('encrypted', encrypted); @@ -151,6 +163,14 @@ export default class Obfuscation { return res; } */ public encode(payload: Uint8Array) { + /* return subtle.encrypt({ + name: 'AES-CTR', + counter: this.encIv, + length: 64 + }, + this.cryptoEncKey, + payload + ); */ return this.encNew.update(payload); } diff --git a/src/lib/mtproto/transports/tcpObfuscated.ts b/src/lib/mtproto/transports/tcpObfuscated.ts index 6509c13dd..b316e901f 100644 --- a/src/lib/mtproto/transports/tcpObfuscated.ts +++ b/src/lib/mtproto/transports/tcpObfuscated.ts @@ -54,14 +54,14 @@ export default class TcpObfuscated implements MTTransport { this.connect(); } - private onOpen = () => { + private onOpen = /* async */() => { this.connected = true; /// #if MTPROTO_AUTO transportController.setTransportOpened('websocket'); /// #endif - const initPayload = this.obfuscation.init(this.codec); + const initPayload = /* await */ this.obfuscation.init(this.codec); this.connection.send(initPayload); diff --git a/src/lib/mtproto/webPushApiManager.ts b/src/lib/mtproto/webPushApiManager.ts index 3d0dcb418..2842095a0 100644 --- a/src/lib/mtproto/webPushApiManager.ts +++ b/src/lib/mtproto/webPushApiManager.ts @@ -11,7 +11,6 @@ import type { NotificationSettings } from "../appManagers/appNotificationsManager"; import { MOUNT_CLASS_TO } from "../../config/debug"; -import { copy } from "../../helpers/object"; import { logger } from "../logger"; import rootScope from "../rootScope"; import { ServiceWorkerNotificationsClearTask, ServiceWorkerPingTask, ServiceWorkerPushClickTask } from "../serviceWorker/index.service"; @@ -19,6 +18,7 @@ import apiManager from "./mtprotoworker"; import I18n, { LangPackKey } from "../langPack"; import { IS_MOBILE } from "../../environment/userAgent"; import appRuntimeManager from "../appManagers/appRuntimeManager"; +import copy from "../../helpers/object/copy"; export type PushSubscriptionNotifyType = 'init' | 'subscribe' | 'unsubscribe'; export type PushSubscriptionNotifyEvent = `push_${PushSubscriptionNotifyType}`; diff --git a/src/lib/polyfill.ts b/src/lib/polyfill.ts index d2b132790..67557d434 100644 --- a/src/lib/polyfill.ts +++ b/src/lib/polyfill.ts @@ -4,7 +4,9 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import { bytesToHex, bytesFromHex, bufferConcats } from '../helpers/bytes'; +import bufferConcats from "../helpers/bytes/bufferConcats"; +import bytesFromHex from "../helpers/bytes/bytesFromHex"; +import bytesToHex from "../helpers/bytes/bytesToHex"; Object.defineProperty(Uint8Array.prototype, 'hex', { get: function(): string { diff --git a/src/lib/rlottie/rlottieIcon.ts b/src/lib/rlottie/rlottieIcon.ts index fd1c11e1f..7b7801005 100644 --- a/src/lib/rlottie/rlottieIcon.ts +++ b/src/lib/rlottie/rlottieIcon.ts @@ -5,7 +5,7 @@ */ import noop from "../../helpers/noop"; -import { safeAssign } from "../../helpers/object"; +import safeAssign from "../../helpers/object/safeAssign"; import rootScope from "../rootScope"; import lottieLoader, { LottieAssetName } from "./lottieLoader"; import type RLottiePlayer from "./rlottiePlayer"; diff --git a/src/lib/storages/dialogs.ts b/src/lib/storages/dialogs.ts index df3a96414..2c90441cf 100644 --- a/src/lib/storages/dialogs.ts +++ b/src/lib/storages/dialogs.ts @@ -22,15 +22,18 @@ import type { AppMessagesIdsManager } from "../appManagers/appMessagesIdsManager import { tsNow } from "../../helpers/date"; import apiManager from "../mtproto/mtprotoworker"; import SearchIndex from "../searchIndex"; -import { forEachReverse, indexOfAndSplice, insertInDescendSortedArray } from "../../helpers/array"; import rootScope from "../rootScope"; -import { defineNotNumerableProperties, safeReplaceObject } from "../../helpers/object"; import { AppStateManager } from "../appManagers/appStateManager"; import { SliceEnd } from "../../helpers/slicedArray"; import { MyDialogFilter } from "./filters"; import { NULL_PEER_ID } from "../mtproto/mtproto_config"; import { NoneToVoidFunction } from "../../types"; import ctx from "../../environment/ctx"; +import forEachReverse from "../../helpers/array/forEachReverse"; +import indexOfAndSplice from "../../helpers/array/indexOfAndSplice"; +import insertInDescendSortedArray from "../../helpers/array/insertInDescendSortedArray"; +import defineNotNumerableProperties from "../../helpers/object/defineNotNumerableProperties"; +import safeReplaceObject from "../../helpers/object/safeReplaceObject"; export type FolderDialog = { dialog: Dialog, diff --git a/src/lib/storages/filters.ts b/src/lib/storages/filters.ts index da5863faa..f408cd96e 100644 --- a/src/lib/storages/filters.ts +++ b/src/lib/storages/filters.ts @@ -4,7 +4,6 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import { copy, safeReplaceObject } from "../../helpers/object"; import type { DialogFilter, Update } from "../../layer"; import type { Modify } from "../../types"; import type { AppPeersManager } from "../appManagers/appPeersManager"; @@ -15,8 +14,10 @@ import type {AppMessagesManager, Dialog} from '../appManagers/appMessagesManager import type {AppNotificationsManager} from "../appManagers/appNotificationsManager"; import type { ApiUpdatesManager } from "../appManagers/apiUpdatesManager"; import apiManager from "../mtproto/mtprotoworker"; -import { forEachReverse } from "../../helpers/array"; import { AppStateManager } from "../appManagers/appStateManager"; +import forEachReverse from "../../helpers/array/forEachReverse"; +import copy from "../../helpers/object/copy"; +import safeReplaceObject from "../../helpers/object/safeReplaceObject"; export type MyDialogFilter = Modify { - for(let i = 0; i < 10; ++i) { - CryptoWorker.invokeCrypto('factorize', new Uint8Array([20, 149, 30, 137, 202, 169, 105, 69])).then(pAndQ => { - pAndQ.pop(); - expect(pAndQ).toEqual([new Uint8Array([59, 165, 190, 67]), new Uint8Array([88, 86, 117, 215])]); - }); +test('factorize', async() => { + const data: {good?: [Uint8Array, Uint8Array], pq: Uint8Array}[] = [{ + good: [new Uint8Array([86, 190, 62, 123]), new Uint8Array([88, 30, 39, 1])], + pq: new Uint8Array([29, 219, 156, 252, 236, 172, 251, 123]) + }, { + good: [new Uint8Array([59, 165, 190, 67]), new Uint8Array([88, 86, 117, 215])], + pq: new Uint8Array([20, 149, 30, 137, 202, 169, 105, 69]) + }, { + good: [new Uint8Array([75, 215, 20, 103]), new Uint8Array([77, 137, 174, 55])], + pq: new Uint8Array([22, 248, 122, 217, 97, 50, 100, 33]) + }, { // leemon cannot factorize that + good: [new Uint8Array([59, 223, 139, 105]), new Uint8Array([62, 179, 202, 59])], + pq: new Uint8Array([14, 170, 48, 94, 24, 240, 251, 51]) + }, { + good: [new Uint8Array([11]), new Uint8Array([1, 15, 141])], + pq: new Uint8Array([11, 171, 15]) + }, { + good: [new Uint8Array([3]), new Uint8Array([5])], + pq: new Uint8Array([15]) + }]; + + const methods = [ + 'factorize' as const, + // 'factorize-tdlib' as const, + // 'factorize-new-new' as const + ]; + + for(const {good, pq} of data) { + for(const method of methods) { + const perf = performance.now(); + await CryptoWorker.invokeCrypto(method, pq).then(pAndQ => { + // console.log(method, performance.now() - perf, pAndQ); + if(good) { + expect(pAndQ).toEqual(good); + } + }); + } + + // break; } }); test('sha1', () => { const bytes = new Uint8Array(bytesFromHex('ec5ac983081eeb1da706316227000000044af6cfb1000000046995dd57000000d55105998729349339eb322d86ec13bc0884f6ba0449d8ecbad0ef574837422579a11a88591796cdcc4c05690da0652462489286450179a635924bcc0ab83848')); - CryptoWorker.invokeCrypto('sha1-hash', bytes) + CryptoWorker.invokeCrypto('sha1', bytes) .then(bytes => { //console.log(bytesFromArrayBuffer(buffer)); @@ -28,7 +62,7 @@ test('sha1', () => { }); test('sha256', () => { - CryptoWorker.invokeCrypto('sha256-hash', new Uint8Array([112, 20, 211, 20, 106, 249, 203, 252, 39, 107, 106, 194, 63, 60, 13, 130, 51, 78, 107, 6, 110, 156, 214, 65, 205, 10, 30, 150, 79, 10, 145, 194, 232, 240, 127, 55, 146, 103, 248, 227, 160, 172, 30, 153, 122, 189, 110, 162, 33, 86, 174, 117])) + CryptoWorker.invokeCrypto('sha256', new Uint8Array([112, 20, 211, 20, 106, 249, 203, 252, 39, 107, 106, 194, 63, 60, 13, 130, 51, 78, 107, 6, 110, 156, 214, 65, 205, 10, 30, 150, 79, 10, 145, 194, 232, 240, 127, 55, 146, 103, 248, 227, 160, 172, 30, 153, 122, 189, 110, 162, 33, 86, 174, 117])) .then(bytes => { expect(bytes).toEqual(new Uint8Array([158, 59, 39, 247, 130, 244, 235, 160, 16, 249, 34, 114, 67, 171, 203, 208, 187, 72, 217, 106, 253, 62, 195, 242, 52, 118, 99, 72, 221, 29, 203, 95])); }); @@ -51,7 +85,7 @@ test('sha256', () => { payload.forEach(pair => { //const uint8 = new TextEncoder().encode(pair[0]); //CryptoWorker.sha256Hash(new Uint8Array(pair[0].split('').map(c => c.charCodeAt(0)))).then(bytes => { - CryptoWorker.invokeCrypto('sha256-hash', pair[0]).then(bytes => { + CryptoWorker.invokeCrypto('sha256', pair[0]).then(bytes => { const hex = bytesToHex(bytes); expect(hex).toEqual(pair[1]); }); @@ -135,3 +169,94 @@ test('pbkdf2', () => { }); + +test('mod-pow', () => { + const g_a = new Uint8Array([ + 0xa8, 0x8b, 0xf9, 0xeb, 0xf9, 0x15, 0x19, 0x11, 0xdf, 0x3b, 0x1, 0x82, 0x52, + 0x9c, 0x8f, 0xe1, 0xcd, 0x6, 0xf0, 0x46, 0xf7, 0x50, 0x34, 0x53, 0xe, 0xb9, + 0x51, 0x21, 0x6d, 0xab, 0x1a, 0x36, 0x9d, 0x45, 0x3a, 0x7c, 0x62, 0x4a, 0x41, + 0x4e, 0x0, 0x15, 0x42, 0x87, 0xfc, 0xef, 0x51, 0x2d, 0xfa, 0x6f, 0x5b, 0xde, + 0xfb, 0x74, 0x62, 0xc3, 0x19, 0x20, 0x74, 0x91, 0x75, 0x84, 0xf2, 0xa8, 0x4b, + 0xd8, 0x62, 0xb0, 0xb4, 0x19, 0xfe, 0x9, 0x65, 0x8, 0x94, 0xae, 0x27, 0xd2, + 0x82, 0xd9, 0x96, 0xd9, 0xad, 0x1f, 0xbd, 0xef, 0xce, 0x77, 0x62, 0x6c, 0x7f, + 0x79, 0xf5, 0x62, 0xbc, 0xd6, 0x4c, 0xf3, 0x6, 0x31, 0xf4, 0xf7, 0x3f, 0xc1, + 0xde, 0x99, 0x41, 0x15, 0xec, 0x5d, 0xea, 0x98, 0x4f, 0x2b, 0x71, 0x70, 0x6d, + 0xc3, 0x39, 0x44, 0x7a, 0x37, 0x25, 0xa2, 0x25, 0x46, 0xdd, 0xd9, 0x4, 0x6b, + 0xf0, 0xe5, 0xd7, 0x3f, 0x1, 0x32, 0x20, 0x2f, 0xfa, 0xc5, 0xbd, 0x69, 0xc0, + 0xa5, 0x26, 0xb0, 0x2d, 0xa7, 0x7d, 0xa7, 0x39, 0xe4, 0x2d, 0xb6, 0x32, 0x95, + 0xdf, 0x56, 0x88, 0x8c, 0x82, 0xe7, 0xc6, 0x89, 0x78, 0xfd, 0xe3, 0xb2, 0xc1, + 0xd7, 0x3f, 0x95, 0x33, 0xb9, 0x9d, 0xbe, 0x4c, 0x95, 0x6b, 0x24, 0x21, 0xda, + 0xa1, 0xa3, 0xab, 0xcd, 0x88, 0x45, 0xd5, 0x49, 0x92, 0xc5, 0x46, 0x21, 0xca, + 0x8b, 0x51, 0xc7, 0x61, 0x7e, 0x68, 0x75, 0xf7, 0x4e, 0x53, 0x55, 0xce, 0xc6, + 0xa1, 0x8d, 0x99, 0x2d, 0x50, 0x50, 0x2b, 0x51, 0x8c, 0x9, 0x8f, 0x49, 0xdd, + 0x33, 0x98, 0xa9, 0x70, 0x1a, 0x8f, 0xc2, 0xf4, 0x4d, 0x2b, 0xab, 0x9b, 0x90, + 0x8e, 0x1e, 0xfe, 0x1a, 0xe2, 0xfb, 0xe, 0x44, 0x58, 0x43, 0xc3, 0x94, 0x65, + 0x92, 0x90, 0xa0, 0xd, 0x30, 0xdf, 0x9b, 0x1c, 0x45 + ]); + + const randomPower = new Uint8Array([ + 0xbc, 0x52, 0x41, 0x6a, 0x18, 0x8b, 0x7a, 0x51, 0x99, 0xc2, 0x3d, 0x1a, 0xaa, 0xda, + 0xda, 0x8a, 0xb4, 0x4d, 0x77, 0x1b, 0x3a, 0x54, 0xaf, 0x1c, 0x48, 0xdc, 0x9b, 0x6b, + 0x59, 0x85, 0xbf, 0xa, 0xd6, 0x52, 0x92, 0x6f, 0xf3, 0xc2, 0xbd, 0x46, 0xb6, 0x13, + 0xf7, 0xe0, 0x39, 0xcc, 0x6a, 0x9d, 0xee, 0x5d, 0xa4, 0x49, 0x94, 0x7b, 0xa6, 0xa3, + 0x53, 0xa4, 0x38, 0xfd, 0x7a, 0xf9, 0xbf, 0xc0, 0xa8, 0x46, 0x1a, 0xb8, 0x3e, 0x49, + 0xb7, 0xf7, 0xbf, 0x5d, 0xf4, 0x9, 0x95, 0x41, 0x23, 0x3d, 0x35, 0x50, 0x49, 0x4, + 0xce, 0x5f, 0x26, 0xc9, 0x2b, 0x54, 0x78, 0x66, 0x1a, 0x9e, 0xd9, 0x2d, 0xb1, 0x79, + 0x7c, 0xb4, 0xd0, 0x1d, 0xe3, 0x62, 0x81, 0x12, 0x98, 0xf5, 0x90, 0xf3, 0xd5, 0x71, + 0xee, 0x48, 0xb6, 0xae, 0xd6, 0x5f, 0x85, 0x59, 0xce, 0x36, 0x96, 0xa3, 0xa5, 0xa3, + 0x96, 0x64, 0xe, 0x7e, 0xa4, 0xa1, 0x3c, 0x9b, 0x68, 0x33, 0x67, 0xd7, 0xf3, 0x3f, + 0x85, 0x15, 0x34, 0x6c, 0xd0, 0x7a, 0x94, 0x75, 0x12, 0xf2, 0x1, 0x98, 0x1, 0x90, + 0x11, 0xbd, 0xa1, 0xa0, 0xda, 0x79, 0x3, 0xce, 0x22, 0x21, 0x69, 0xdf, 0x5d, 0x9a, + 0xee, 0xd7, 0x98, 0xae, 0x1e, 0x74, 0x96, 0xb3, 0xda, 0xbd, 0x31, 0x4b, 0xb4, 0x71, + 0x14, 0xba, 0xfa, 0xa9, 0x1, 0x62, 0x46, 0x7d, 0x35, 0x1c, 0xbf, 0x88, 0xa4, 0x46, + 0x45, 0xb1, 0x91, 0x89, 0x69, 0xfb, 0x9f, 0xf, 0x9a, 0x8b, 0xe, 0xc0, 0xfc, 0xa, + 0x7b, 0x78, 0x16, 0xe5, 0xce, 0x90, 0x4e, 0xb2, 0xf0, 0x39, 0x2c, 0xbd, 0x1e, 0xa9, + 0xdc, 0x5c, 0xc1, 0x35, 0x29, 0xe2, 0xc4, 0x1a, 0x9a, 0xd7, 0xb5, 0x69, 0x30, 0xf2, + 0x72, 0xc2, 0x6d, 0x90, 0x49, 0x48, 0x49, 0xc5, 0x87, 0x96, 0xa5, 0xf3, 0xb6, 0xa6, + 0xc, 0xe5, 0xf8, 0x8e + ]); + + const p = new Uint8Array([ + 0xc7, 0x1c, 0xae, 0xb9, 0xc6, 0xb1, 0xc9, 0x4, 0x8e, 0x6c, 0x52, 0x2f, 0x70, 0xf1, + 0x3f, 0x73, 0x98, 0xd, 0x40, 0x23, 0x8e, 0x3e, 0x21, 0xc1, 0x49, 0x34, 0xd0, 0x37, + 0x56, 0x3d, 0x93, 0xf, 0x48, 0x19, 0x8a, 0xa, 0xa7, 0xc1, 0x40, 0x58, 0x22, 0x94, + 0x93, 0xd2, 0x25, 0x30, 0xf4, 0xdb, 0xfa, 0x33, 0x6f, 0x6e, 0xa, 0xc9, 0x25, 0x13, + 0x95, 0x43, 0xae, 0xd4, 0x4c, 0xce, 0x7c, 0x37, 0x20, 0xfd, 0x51, 0xf6, 0x94, 0x58, + 0x70, 0x5a, 0xc6, 0x8c, 0xd4, 0xfe, 0x6b, 0x6b, 0x13, 0xab, 0xdc, 0x97, 0x46, 0x51, + 0x29, 0x69, 0x32, 0x84, 0x54, 0xf1, 0x8f, 0xaf, 0x8c, 0x59, 0x5f, 0x64, 0x24, 0x77, + 0xfe, 0x96, 0xbb, 0x2a, 0x94, 0x1d, 0x5b, 0xcd, 0x1d, 0x4a, 0xc8, 0xcc, 0x49, 0x88, + 0x7, 0x8, 0xfa, 0x9b, 0x37, 0x8e, 0x3c, 0x4f, 0x3a, 0x90, 0x60, 0xbe, 0xe6, 0x7c, + 0xf9, 0xa4, 0xa4, 0xa6, 0x95, 0x81, 0x10, 0x51, 0x90, 0x7e, 0x16, 0x27, 0x53, 0xb5, + 0x6b, 0xf, 0x6b, 0x41, 0xd, 0xba, 0x74, 0xd8, 0xa8, 0x4b, 0x2a, 0x14, 0xb3, 0x14, + 0x4e, 0xe, 0xf1, 0x28, 0x47, 0x54, 0xfd, 0x17, 0xed, 0x95, 0xd, 0x59, 0x65, 0xb4, + 0xb9, 0xdd, 0x46, 0x58, 0x2d, 0xb1, 0x17, 0x8d, 0x16, 0x9c, 0x6b, 0xc4, 0x65, 0xb0, + 0xd6, 0xff, 0x9c, 0xa3, 0x92, 0x8f, 0xef, 0x5b, 0x9a, 0xe4, 0xe4, 0x18, 0xfc, 0x15, + 0xe8, 0x3e, 0xbe, 0xa0, 0xf8, 0x7f, 0xa9, 0xff, 0x5e, 0xed, 0x70, 0x5, 0xd, 0xed, + 0x28, 0x49, 0xf4, 0x7b, 0xf9, 0x59, 0xd9, 0x56, 0x85, 0xc, 0xe9, 0x29, 0x85, 0x1f, + 0xd, 0x81, 0x15, 0xf6, 0x35, 0xb1, 0x5, 0xee, 0x2e, 0x4e, 0x15, 0xd0, 0x4b, 0x24, + 0x54, 0xbf, 0x6f, 0x4f, 0xad, 0xf0, 0x34, 0xb1, 0x4, 0x3, 0x11, 0x9c, 0xd8, 0xe3, + 0xb9, 0x2f, 0xcc, 0x5b + ]); + + CryptoWorker.invokeCrypto('mod-pow', g_a, randomPower, p).then(encrypted => { + const good = new Uint8Array([ + 0x2c, 0xb2, 0x4, 0xe7, 0xa8, 0x63, 0x5f, 0x3e, 0xd0, 0x67, 0x5f, 0x76, 0x87, 0x37, 0x56, 0xc2, + 0x2d, 0xe7, 0xd, 0xe3, 0x9b, 0xbd, 0x9d, 0xf6, 0x3b, 0x1f, 0xc, 0xb4, 0x37, 0xc6, 0xf, 0x75, + 0x83, 0x1a, 0x8b, 0x65, 0x73, 0xf6, 0x83, 0x64, 0x16, 0x7e, 0xb3, 0xd8, 0xc1, 0xd, 0x1d, 0x69, + 0xf4, 0x4, 0x25, 0x80, 0x6, 0x3b, 0xc7, 0x70, 0x55, 0xdb, 0x7d, 0x99, 0x39, 0x18, 0x6e, 0xcb, + 0x35, 0x98, 0x9f, 0xa2, 0x47, 0x63, 0x2c, 0x1b, 0xaf, 0x13, 0xdc, 0x1e, 0x52, 0xf5, 0x36, 0x5e, + 0xc5, 0x41, 0xd5, 0x4, 0x2b, 0x9c, 0x28, 0xee, 0xcf, 0x89, 0xa8, 0xcb, 0x6e, 0x43, 0xda, 0xbc, + 0xbf, 0xcd, 0x12, 0xa8, 0x32, 0xe8, 0x3d, 0x27, 0x5f, 0xfb, 0xa9, 0x5, 0xa, 0x29, 0xfa, 0x70, + 0x5e, 0x96, 0x8b, 0xd1, 0xe5, 0xdf, 0x4d, 0xfe, 0xed, 0xfc, 0xc1, 0xd9, 0x67, 0x25, 0x1b, 0x5a, + 0x5b, 0x26, 0x41, 0x83, 0x52, 0x89, 0xf9, 0xb3, 0xed, 0x9d, 0xfd, 0xa3, 0xce, 0xbc, 0x5, 0x27, + 0xd8, 0x54, 0xef, 0x4f, 0x4e, 0x73, 0xa1, 0xd5, 0x7d, 0x92, 0xdc, 0xe5, 0x64, 0xcd, 0x83, 0x87, + 0x31, 0x98, 0xf5, 0x3f, 0x27, 0xd0, 0x78, 0x4b, 0x47, 0x58, 0x8b, 0x4f, 0x77, 0x8a, 0x1a, 0x85, + 0x37, 0xc2, 0x68, 0xe9, 0xbc, 0xbe, 0x38, 0x2d, 0x51, 0xd3, 0x68, 0x89, 0xa1, 0x41, 0x38, 0x9c, + 0xd6, 0x1c, 0x30, 0xf4, 0x83, 0x85, 0xba, 0x43, 0x12, 0xc, 0xff, 0xb3, 0x35, 0x43, 0xf7, 0x8f, + 0x26, 0xb3, 0xcb, 0xfd, 0xa0, 0x27, 0xfc, 0xe2, 0xbd, 0x9d, 0xa9, 0xbf, 0x8e, 0xe, 0xf6, 0x88, + 0x83, 0xc3, 0x4d, 0xae, 0x7c, 0x2, 0x7e, 0xcc, 0x9d, 0xb1, 0x4f, 0x28, 0x20, 0xed, 0x13, 0x32, + 0x5b, 0x36, 0x1b, 0x50, 0x5a, 0xf2, 0x86, 0x35, 0xb2, 0x9f, 0x24, 0xf5, 0x64, 0xb3, 0x11, 0x75 + ]); + expect(encrypted).toEqual(good); + }); +}); diff --git a/src/tests/srp.test.ts b/src/tests/srp.test.ts index d8ab36313..cf491cbf1 100644 --- a/src/tests/srp.test.ts +++ b/src/tests/srp.test.ts @@ -1,5 +1,5 @@ import { salt1, salt2, g, p, srp_id, secure_random, srp_B, password, A, M1, passwordHashed } from '../mock/srp'; -import { computeSRP, makePasswordHash } from '../lib/crypto/srp'; +import computeSRP, { makePasswordHash } from '../lib/crypto/srp'; import '../lib/polyfill'; import assumeType from '../helpers/assumeType'; import { InputCheckPasswordSRP } from '../layer'; diff --git a/tweb-design b/tweb-design index d7664548d..2c4d08587 160000 --- a/tweb-design +++ b/tweb-design @@ -1 +1 @@ -Subproject commit d7664548de0373baa27e006bbe1f62467f566277 +Subproject commit 2c4d08587b77a388d4beb8bc018dcb56ebd8a589