diff --git a/app/index.html b/app/index.html index ade74629..7b7b2dee 100644 --- a/app/index.html +++ b/app/index.html @@ -47,8 +47,12 @@ + + + + - + diff --git a/app/js/app.js b/app/js/app.js index 2cbb4093..10992620 100644 --- a/app/js/app.js +++ b/app/js/app.js @@ -24,7 +24,9 @@ angular.module('myApp', [ 'ngSanitize', 'ui.bootstrap', 'pasvaz.bindonce', - 'mtproto.services', + 'izhukov.utils', + 'izhukov.mtproto', + 'izhukov.mtproto.wrapper', 'myApp.filters', 'myApp.services', /*PRODUCTION_ONLY_BEGIN @@ -33,7 +35,7 @@ angular.module('myApp', [ 'myApp.directives', 'myApp.controllers' ]). -config(['$locationProvider', '$routeProvider', '$compileProvider', function($locationProvider, $routeProvider, $compileProvider) { +config(['$locationProvider', '$routeProvider', '$compileProvider', 'StorageProvider', function($locationProvider, $routeProvider, $compileProvider, StorageProvider) { var icons = {}, reverseIcons = {}, i, j, hex, name, dataItem, row, column, totalColumns; @@ -49,6 +51,10 @@ config(['$locationProvider', '$routeProvider', '$compileProvider', function($loc } } + if (Config.Modes.test) { + StorageProvider.setPrefix('t_'); + } + $.emojiarea.spritesheetPath = 'img/emojisprite_!.png'; $.emojiarea.spritesheetDimens = Config.EmojiCategorySpritesheetDimens; $.emojiarea.iconSize = 20; @@ -59,7 +65,6 @@ config(['$locationProvider', '$routeProvider', '$compileProvider', function($loc $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|file|mailto|blob|filesystem|chrome-extension|app):|data:image\//); - // $locationProvider.html5Mode(true); $routeProvider.when('/', {templateUrl: 'partials/welcome.html', controller: 'AppWelcomeController'}); $routeProvider.when('/login', {templateUrl: 'partials/login.html', controller: 'AppLoginController'}); $routeProvider.when('/im', {templateUrl: 'partials/im.html', controller: 'AppIMController', reloadOnSearch: false}); diff --git a/app/js/controllers.js b/app/js/controllers.js index 226aa0e5..56b63e79 100644 --- a/app/js/controllers.js +++ b/app/js/controllers.js @@ -1125,7 +1125,7 @@ angular.module('myApp.controllers', []) $scope.$on('user_update', angular.noop); }) - .controller('AppImSendController', function ($scope, $timeout, MtpApiManager, AppConfigManager, AppPeersManager, AppMessagesManager, ApiUpdatesManager, MtpApiFileManager) { + .controller('AppImSendController', function ($scope, $timeout, MtpApiManager, Storage, AppPeersManager, AppMessagesManager, ApiUpdatesManager, MtpApiFileManager) { $scope.$watch('curDialog.peer', resetDraft); $scope.$on('user_update', angular.noop); @@ -1180,7 +1180,7 @@ angular.module('myApp.controllers', []) function resetDraft (newPeer) { if (newPeer) { - AppConfigManager.get('draft' + $scope.curDialog.peerID).then(function (draftText) { + Storage.get('draft' + $scope.curDialog.peerID).then(function (draftText) { // console.log('Restore draft', 'draft' + $scope.curDialog.peerID, draftText); $scope.draftMessage.text = draftText || ''; // console.log('send broadcast', $scope.draftMessage); @@ -1204,10 +1204,10 @@ angular.module('myApp.controllers', []) var backupDraftObj = {}; backupDraftObj['draft' + $scope.curDialog.peerID] = newVal; - AppConfigManager.set(backupDraftObj); + Storage.set(backupDraftObj); // console.log('draft save', backupDraftObj); } else { - AppConfigManager.remove('draft' + $scope.curDialog.peerID); + Storage.remove('draft' + $scope.curDialog.peerID); // console.log('draft delete', 'draft' + $scope.curDialog.peerID); } } @@ -1780,7 +1780,7 @@ angular.module('myApp.controllers', []) }) - .controller('SettingsModalController', function ($rootScope, $scope, $timeout, $modal, AppUsersManager, AppChatsManager, AppPhotosManager, MtpApiManager, AppConfigManager, NotificationsManager, MtpApiFileManager, ApiUpdatesManager, ChangelogNotifyService, ErrorService) { + .controller('SettingsModalController', function ($rootScope, $scope, $timeout, $modal, AppUsersManager, AppChatsManager, AppPhotosManager, MtpApiManager, Storage, NotificationsManager, MtpApiFileManager, ApiUpdatesManager, ChangelogNotifyService, ErrorService) { $scope.profile = {}; $scope.photo = {}; @@ -1876,7 +1876,7 @@ angular.module('myApp.controllers', []) }); }; - AppConfigManager.get('notify_nodesktop', 'notify_nosound', 'send_ctrlenter', 'notify_volume').then(function (settings) { + Storage.get('notify_nodesktop', 'notify_nosound', 'send_ctrlenter', 'notify_volume').then(function (settings) { $scope.notify.desktop = !settings[0]; $scope.send.enter = settings[2] ? '' : '1'; @@ -1904,8 +1904,8 @@ angular.module('myApp.controllers', []) $scope.$watch('notify.volume', function (newValue, oldValue) { if (newValue !== oldValue) { var storeVolume = newValue / 10; - AppConfigManager.set({notify_volume: storeVolume}); - AppConfigManager.remove('notify_nosound'); + Storage.set({notify_volume: storeVolume}); + Storage.remove('notify_nosound'); NotificationsManager.clear(); if (testSoundPromise) { @@ -1921,9 +1921,9 @@ angular.module('myApp.controllers', []) $scope.notify.desktop = !$scope.notify.desktop; if ($scope.notify.desktop) { - AppConfigManager.remove('notify_nodesktop'); + Storage.remove('notify_nodesktop'); } else { - AppConfigManager.set({notify_nodesktop: true}); + Storage.set({notify_nodesktop: true}); } } @@ -1931,9 +1931,9 @@ angular.module('myApp.controllers', []) $scope.send.enter = newValue; if ($scope.send.enter) { - AppConfigManager.remove('send_ctrlenter'); + Storage.remove('send_ctrlenter'); } else { - AppConfigManager.set({send_ctrlenter: true}); + Storage.set({send_ctrlenter: true}); } $rootScope.$broadcast('settings_changed'); } @@ -1944,7 +1944,7 @@ angular.module('myApp.controllers', []) } }) - .controller('ProfileEditModalController', function ($rootScope, $scope, $timeout, $modal, $modalInstance, AppUsersManager, AppChatsManager, MtpApiManager, AppConfigManager, NotificationsManager, MtpApiFileManager, ApiUpdatesManager) { + .controller('ProfileEditModalController', function ($rootScope, $scope, $timeout, $modal, $modalInstance, AppUsersManager, AppChatsManager, MtpApiManager, Storage, NotificationsManager, MtpApiFileManager, ApiUpdatesManager) { $scope.profile = {}; $scope.error = {}; diff --git a/app/js/directives.js b/app/js/directives.js index 64c49af0..d17e8a66 100644 --- a/app/js/directives.js +++ b/app/js/directives.js @@ -609,7 +609,7 @@ angular.module('myApp.directives', ['myApp.filters']) }) - .directive('mySendForm', function ($timeout, $modalStack, AppConfigManager, ErrorService) { + .directive('mySendForm', function ($timeout, $modalStack, Storage, ErrorService) { return { link: link, @@ -665,7 +665,7 @@ angular.module('myApp.directives', ['myApp.filters']) var sendOnEnter = true, updateSendSettings = function () { - AppConfigManager.get('send_ctrlenter').then(function (sendOnCtrl) { + Storage.get('send_ctrlenter').then(function (sendOnCtrl) { sendOnEnter = !sendOnCtrl; }); }; diff --git a/app/js/lib/bin_utils.js b/app/js/lib/bin_utils.js new file mode 100644 index 00000000..945cc4db --- /dev/null +++ b/app/js/lib/bin_utils.js @@ -0,0 +1,435 @@ +/*! + * Webogram v0.1.6 - messaging web application for MTProto + * https://github.com/zhukov/webogram + * Copyright (C) 2014 Igor Zhukov + * https://github.com/zhukov/webogram/blob/master/LICENSE + */ + +function bigint (num) { + return new BigInteger(num.toString(16), 16); +} + +function bigStringInt (strNum) { + return new BigInteger(strNum, 10); +} + +function dHexDump (bytes) { + var arr = []; + for (var i = 0; i < bytes.length; i++) { + if (i && !(i % 2)) { + if (!(i % 16)) { + arr.push("\n"); + } else if (!(i % 4)) { + arr.push(' '); + } else { + arr.push(' '); + } + } + arr.push((bytes[i] < 16 ? '0' : '') + bytes[i].toString(16)); + } + + console.log(arr.join('')); +} + +function bytesToHex (bytes) { + bytes = bytes || []; + var arr = []; + for (var i = 0; i < bytes.length; i++) { + arr.push((bytes[i] < 16 ? '0' : '') + (bytes[i] || 0).toString(16)); + } + return arr.join(''); +} + +function bytesFromHex (hexString) { + var len = hexString.length, + i, + bytes = []; + + for (i = 0; i < len; i += 2) { + bytes.push(parseInt(hexString.substr(i, 2), 16)); + } + + return bytes; +} + +function bytesToBase64 (bytes) { + var mod3, result = ''; + + for (var 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, '='); +} + +function uint6ToBase64 (nUint6) { + return nUint6 < 26 + ? nUint6 + 65 + : nUint6 < 52 + ? nUint6 + 71 + : nUint6 < 62 + ? nUint6 - 4 + : nUint6 === 62 + ? 43 + : nUint6 === 63 + ? 47 + : 65; +} + +function bytesCmp (bytes1, bytes2) { + var len = bytes1.length; + if (len != bytes2.length) { + return false; + } + + for (var i = 0; i < len; i++) { + if (bytes1[i] != bytes2[i]) { + return false; + } + } + return true; +} + +function bytesXor (bytes1, bytes2) { + var len = bytes1.length, + bytes = []; + + for (var i = 0; i < len; ++i) { + bytes[i] = bytes1[i] ^ bytes2[i]; + } + + return bytes; +} + +function bytesToWords (bytes) { + var len = bytes.length, + words = []; + + for (var i = 0; i < len; i++) { + words[i >>> 2] |= bytes[i] << (24 - (i % 4) * 8); + } + + return new CryptoJS.lib.WordArray.init(words, len); +} + +function bytesFromWords (wordArray) { + var words = wordArray.words, + sigBytes = wordArray.sigBytes, + bytes = []; + + for (var i = 0; i < sigBytes; i++) { + bytes.push((words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff); + } + + return bytes; +} + +function bytesFromBigInt (bigInt, len) { + var bytes = bigInt.toByteArray(); + + while (!bytes[0] && (!len || bytes.length > len)) { + bytes = bytes.slice(1); + } + + return bytes; +} + +function bytesToArrayBuffer (b) { + return (new Uint8Array(b)).buffer; +} + +function bytesFromArrayBuffer (buffer) { + var len = buffer.byteLength, + byteView = new Uint8Array(buffer), + bytes = []; + + for (var i = 0; i < len; ++i) { + bytes[i] = byteView[i]; + } + + return bytes; +} + +function longToInts (sLong) { + var divRem = bigStringInt(sLong).divideAndRemainder(bigint(0x100000000)); + + return [divRem[0].intValue(), divRem[1].intValue()]; +} + +function longToBytes (sLong) { + return bytesFromWords({words: longToInts(sLong), sigBytes: 8}).reverse(); +} + +function longFromInts (high, low) { + return bigint(high).shiftLeft(32).add(bigint(low)).toString(10); +} + +function intToUint (val) { + val = parseInt(val); + if (val < 0) { + val = val + 4294967296; + } + return val; +} + +function uintToInt (val) { + if (val > 2147483647) { + val = val - 4294967296; + } + return val; +} + +function sha1Hash (bytes) { + // console.log('SHA-1 hash start'); + var hashBytes = sha1.hash(bytes, true); + // console.log('SHA-1 hash finish'); + + return hashBytes; +} + + + +function rsaEncrypt (publicKey, bytes) { + var needPadding = 255 - bytes.length; + if (needPadding > 0) { + var padding = new Array(needPadding); + (new SecureRandom()).nextBytes(padding); + + bytes = bytes.concat(padding); + } + + // console.log('RSA encrypt start'); + var N = new BigInteger(publicKey.modulus, 16), + E = new BigInteger(publicKey.exponent, 16), + X = new BigInteger(bytes), + encryptedBigInt = X.modPowInt(E, N), + encryptedBytes = bytesFromBigInt(encryptedBigInt, 256); + + // console.log('RSA encrypt finish'); + + return encryptedBytes; +} + +function aesEncrypt (bytes, keyBytes, ivBytes) { + // console.log('AES encrypt start', bytes.length/*, bytesToHex(keyBytes), bytesToHex(ivBytes)*/); + + var needPadding = 16 - (bytes.length % 16); + if (needPadding > 0 && needPadding < 16) { + var padding = new Array(needPadding); + (new SecureRandom()).nextBytes(padding); + + bytes = bytes.concat(padding); + } + + var encryptedWords = CryptoJS.AES.encrypt(bytesToWords(bytes), bytesToWords(keyBytes), { + iv: bytesToWords(ivBytes), + padding: CryptoJS.pad.NoPadding, + mode: CryptoJS.mode.IGE + }).ciphertext; + + var encryptedBytes = bytesFromWords(encryptedWords); + + // console.log('AES encrypt finish'); + + return encryptedBytes; +} + +function aesDecrypt (encryptedBytes, keyBytes, ivBytes) { + // console.log('AES decrypt start', encryptedBytes.length/*, bytesToHex(keyBytes), bytesToHex(ivBytes)*/); + + var decryptedWords = CryptoJS.AES.decrypt({ciphertext: bytesToWords(encryptedBytes)}, bytesToWords(keyBytes), { + iv: bytesToWords(ivBytes), + padding: CryptoJS.pad.NoPadding, + mode: CryptoJS.mode.IGE + }); + + var bytes = bytesFromWords(decryptedWords); + + // console.log('AES decrypt finish'); + + return bytes; +} + +function gzipUncompress (bytes) { + // console.log('Gzip uncompress start'); + var result = (new Zlib.Gunzip(bytes)).decompress(); + // console.log('Gzip uncompress finish'); + return result; +} + +function nextRandomInt (maxValue) { + return Math.floor(Math.random() * maxValue); +}; + +function pqPrimeFactorization (pqBytes) { + var what = new BigInteger(pqBytes), + result = false; + + console.log('PQ start', pqBytes, what.bitLength()); + + if (what.bitLength() <= 64) { + // console.time('PQ long'); + try { + result = pqPrimeLong(goog.math.Long.fromString(what.toString(16), 16)); + } catch (e) { + console.error('Pq long Exception', e); + }; + // console.timeEnd('PQ long'); + } + // console.log(result); + + if (result === false) { + // console.time('pq BigInt'); + result = pqPrimeBigInteger(what); + // console.timeEnd('pq BigInt'); + } + + console.log('PQ finish'); + + return result; +} + +function pqPrimeBigInteger (what) { + var it = 0, + g; + for (var i = 0; i < 3; i++) { + var q = (nextRandomInt(128) & 15) + 17, + x = bigint(nextRandomInt(1000000000) + 1), + y = x.clone(), + lim = 1 << (i + 18); + + for (var j = 1; j < lim; j++) { + ++it; + var a = x.clone(), + b = x.clone(), + c = bigint(q); + + while (!b.equals(BigInteger.ZERO)) { + if (!b.and(BigInteger.ONE).equals(BigInteger.ZERO)) { + c = c.add(a); + if (c.compareTo(what) > 0) { + c = c.subtract(what); + } + } + a = a.add(a); + if (a.compareTo(what) > 0) { + a = a.subtract(what); + } + b = b.shiftRight(1); + } + + x = c.clone(); + var z = x.compareTo(y) < 0 ? y.subtract(x) : x.subtract(y); + g = z.gcd(what); + if (!g.equals(BigInteger.ONE)) { + break; + } + if ((j & (j - 1)) == 0) { + y = x.clone(); + } + } + if (g.compareTo(BigInteger.ONE) > 0) { + break; + } + } + + var f = what.divide(g), P, Q; + + if (g.compareTo(f) > 0) { + P = f; + Q = g; + } else { + P = g; + Q = f; + } + + return [bytesFromBigInt(P), bytesFromBigInt(Q)]; +} + +function gcdLong(a, b) { + while (a.notEquals(goog.math.Long.ZERO) && b.notEquals(goog.math.Long.ZERO)) { + while (b.and(goog.math.Long.ONE).equals(goog.math.Long.ZERO)) { + b = b.shiftRight(1); + } + while (a.and(goog.math.Long.ONE).equals(goog.math.Long.ZERO)) { + a = a.shiftRight(1); + } + if (a.compare(b) > 0) { + a = a.subtract(b); + } else { + b = b.subtract(a); + } + } + return b.equals(goog.math.Long.ZERO) ? a : b; +} + +function pqPrimeLong(what) { + // console.log('start long'); + var it = 0, + g; + for (var i = 0; i < 3; i++) { + var q = goog.math.Long.fromInt((nextRandomInt(128) & 15) + 17), + x = goog.math.Long.fromInt(nextRandomInt(1000000000) + 1), + y = x, + lim = 1 << (i + 18); + + for (var j = 1; j < lim; j++) { + ++it; + // if (!(it % 100)) { + // console.log(dT(), 'it', it, i, j, x.toString()); + // } + var a = x, + b = x, + c = q; + + while (b.notEquals(goog.math.Long.ZERO)) { + if (b.and(goog.math.Long.ONE).notEquals(goog.math.Long.ZERO)) { + c = c.add(a); + if (c.compare(what) > 0) { + c = c.subtract(what); + } + } + a = a.add(a); + if (a.compare(what) > 0) { + a = a.subtract(what); + } + b = b.shiftRight(1); + } + + x = c; + var z = x.compare(y) < 0 ? y.subtract(x) : x.subtract(y); + g = gcdLong(z, what); + if (g.notEquals(goog.math.Long.ONE)) { + break; + } + if ((j & (j - 1)) == 0) { + y = x; + } + } + if (g.compare(goog.math.Long.ONE) > 0) { + break; + } + } + + var f = what.div(g), P, Q; + + if (g.compare(f) > 0) { + P = f; + Q = g; + } else { + P = g; + Q = f; + } + + return [bytesFromHex(P.toString(16)), bytesFromHex(Q.toString(16))]; +} diff --git a/app/js/lib/mtproto.js b/app/js/lib/mtproto.js index 245a1da9..762ebade 100644 --- a/app/js/lib/mtproto.js +++ b/app/js/lib/mtproto.js @@ -5,1004 +5,9 @@ * https://github.com/zhukov/webogram/blob/master/LICENSE */ -function bigint (num) { - return new BigInteger(num.toString(16), 16); -} - -function bigStringInt (strNum) { - return new BigInteger(strNum, 10); -} - -function dHexDump (bytes) { - var arr = []; - for (var i = 0; i < bytes.length; i++) { - if (i && !(i % 2)) { - if (!(i % 16)) { - arr.push("\n"); - } else if (!(i % 4)) { - arr.push(' '); - } else { - arr.push(' '); - } - } - arr.push((bytes[i] < 16 ? '0' : '') + bytes[i].toString(16)); - } - - console.log(arr.join('')); -} - -function bytesToHex (bytes) { - bytes = bytes || []; - var arr = []; - for (var i = 0; i < bytes.length; i++) { - arr.push((bytes[i] < 16 ? '0' : '') + (bytes[i] || 0).toString(16)); - } - return arr.join(''); -} - -function bytesFromHex (hexString) { - var len = hexString.length, - i, - bytes = []; - - for (i = 0; i < len; i += 2) { - bytes.push(parseInt(hexString.substr(i, 2), 16)); - } - - return bytes; -} - -function bytesToBase64 (bytes) { - var mod3, result = ''; - - for (var 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, '='); -} - -function uint6ToBase64 (nUint6) { - return nUint6 < 26 - ? nUint6 + 65 - : nUint6 < 52 - ? nUint6 + 71 - : nUint6 < 62 - ? nUint6 - 4 - : nUint6 === 62 - ? 43 - : nUint6 === 63 - ? 47 - : 65; -} - -function bytesCmp (bytes1, bytes2) { - var len = bytes1.length; - if (len != bytes2.length) { - return false; - } - - for (var i = 0; i < len; i++) { - if (bytes1[i] != bytes2[i]) { - return false; - } - } - return true; -} - -function bytesXor (bytes1, bytes2) { - var len = bytes1.length, - bytes = []; - - for (var i = 0; i < len; ++i) { - bytes[i] = bytes1[i] ^ bytes2[i]; - } - - return bytes; -} - -function bytesToWords (bytes) { - var len = bytes.length, - words = []; - - for (var i = 0; i < len; i++) { - words[i >>> 2] |= bytes[i] << (24 - (i % 4) * 8); - } - - return new CryptoJS.lib.WordArray.init(words, len); -} - -function bytesFromWords (wordArray) { - var words = wordArray.words, - sigBytes = wordArray.sigBytes, - bytes = []; - - for (var i = 0; i < sigBytes; i++) { - bytes.push((words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff); - } - - return bytes; -} - -function bytesFromBigInt (bigInt, len) { - var bytes = bigInt.toByteArray(); - - while (!bytes[0] && (!len || bytes.length > len)) { - bytes = bytes.slice(1); - } - - return bytes; -} - -function bytesToArrayBuffer (b) { - return (new Uint8Array(b)).buffer; -} - -function bytesFromArrayBuffer (buffer) { - var len = buffer.byteLength, - byteView = new Uint8Array(buffer), - bytes = []; - - for (var i = 0; i < len; ++i) { - bytes[i] = byteView[i]; - } - - return bytes; -} - -function longToInts (sLong) { - var divRem = bigStringInt(sLong).divideAndRemainder(bigint(0x100000000)); - - return [divRem[0].intValue(), divRem[1].intValue()]; -} - -function longToBytes (sLong) { - return bytesFromWords({words: longToInts(sLong), sigBytes: 8}).reverse(); -} - -function longFromInts (high, low) { - return bigint(high).shiftLeft(32).add(bigint(low)).toString(10); -} - -function intToUint (val) { - val = parseInt(val); - if (val < 0) { - val = val + 4294967296; - } - return val; -} - -function uintToInt (val) { - if (val > 2147483647) { - val = val - 4294967296; - } - return val; -} - -function sha1Hash (bytes) { - // console.log('SHA-1 hash start'); - var hashBytes = sha1.hash(bytes, true); - // console.log('SHA-1 hash finish'); - - return hashBytes; -} - - - -function rsaEncrypt (publicKey, bytes) { - var needPadding = 255 - bytes.length; - if (needPadding > 0) { - var padding = new Array(needPadding); - (new SecureRandom()).nextBytes(padding); - - bytes = bytes.concat(padding); - } - - // console.log('RSA encrypt start'); - var N = new BigInteger(publicKey.modulus, 16), - E = new BigInteger(publicKey.exponent, 16), - X = new BigInteger(bytes), - encryptedBigInt = X.modPowInt(E, N), - encryptedBytes = bytesFromBigInt(encryptedBigInt, 256); - - // console.log('RSA encrypt finish'); - - return encryptedBytes; -} - -function aesEncrypt (bytes, keyBytes, ivBytes) { - // console.log('AES encrypt start', bytes.length/*, bytesToHex(keyBytes), bytesToHex(ivBytes)*/); - - var needPadding = 16 - (bytes.length % 16); - if (needPadding > 0 && needPadding < 16) { - var padding = new Array(needPadding); - (new SecureRandom()).nextBytes(padding); - - bytes = bytes.concat(padding); - } - - var encryptedWords = CryptoJS.AES.encrypt(bytesToWords(bytes), bytesToWords(keyBytes), { - iv: bytesToWords(ivBytes), - padding: CryptoJS.pad.NoPadding, - mode: CryptoJS.mode.IGE - }).ciphertext; - - var encryptedBytes = bytesFromWords(encryptedWords); - - // console.log('AES encrypt finish'); - - return encryptedBytes; -} - -function aesDecrypt (encryptedBytes, keyBytes, ivBytes) { - // console.log('AES decrypt start', encryptedBytes.length/*, bytesToHex(keyBytes), bytesToHex(ivBytes)*/); - - var decryptedWords = CryptoJS.AES.decrypt({ciphertext: bytesToWords(encryptedBytes)}, bytesToWords(keyBytes), { - iv: bytesToWords(ivBytes), - padding: CryptoJS.pad.NoPadding, - mode: CryptoJS.mode.IGE - }); - - var bytes = bytesFromWords(decryptedWords); - - // console.log('AES decrypt finish'); - - return bytes; -} - -function gzipUncompress (bytes) { - // console.log('Gzip uncompress start'); - var result = (new Zlib.Gunzip(bytes)).decompress(); - // console.log('Gzip uncompress finish'); - return result; -} - -function nextRandomInt (maxValue) { - return Math.floor(Math.random() * maxValue); -}; - -function pqPrimeFactorization (pqBytes) { - var what = new BigInteger(pqBytes), - result = false; - - console.log('PQ start', pqBytes, what.bitLength()); - - if (what.bitLength() <= 64) { - // console.time('PQ long'); - try { - result = pqPrimeLong(goog.math.Long.fromString(what.toString(16), 16)); - } catch (e) { - console.error('Pq long Exception', e); - }; - // console.timeEnd('PQ long'); - } - // console.log(result); - - if (result === false) { - // console.time('pq BigInt'); - result = pqPrimeBigInteger(what); - // console.timeEnd('pq BigInt'); - } - - console.log('PQ finish'); - - return result; -} - -function pqPrimeBigInteger (what) { - var it = 0, - g; - for (var i = 0; i < 3; i++) { - var q = (nextRandomInt(128) & 15) + 17, - x = bigint(nextRandomInt(1000000000) + 1), - y = x.clone(), - lim = 1 << (i + 18); - - for (var j = 1; j < lim; j++) { - ++it; - var a = x.clone(), - b = x.clone(), - c = bigint(q); - - while (!b.equals(BigInteger.ZERO)) { - if (!b.and(BigInteger.ONE).equals(BigInteger.ZERO)) { - c = c.add(a); - if (c.compareTo(what) > 0) { - c = c.subtract(what); - } - } - a = a.add(a); - if (a.compareTo(what) > 0) { - a = a.subtract(what); - } - b = b.shiftRight(1); - } - - x = c.clone(); - var z = x.compareTo(y) < 0 ? y.subtract(x) : x.subtract(y); - g = z.gcd(what); - if (!g.equals(BigInteger.ONE)) { - break; - } - if ((j & (j - 1)) == 0) { - y = x.clone(); - } - } - if (g.compareTo(BigInteger.ONE) > 0) { - break; - } - } - - var f = what.divide(g), P, Q; - - if (g.compareTo(f) > 0) { - P = f; - Q = g; - } else { - P = g; - Q = f; - } - - return [bytesFromBigInt(P), bytesFromBigInt(Q)]; -} - -function gcdLong(a, b) { - while (a.notEquals(goog.math.Long.ZERO) && b.notEquals(goog.math.Long.ZERO)) { - while (b.and(goog.math.Long.ONE).equals(goog.math.Long.ZERO)) { - b = b.shiftRight(1); - } - while (a.and(goog.math.Long.ONE).equals(goog.math.Long.ZERO)) { - a = a.shiftRight(1); - } - if (a.compare(b) > 0) { - a = a.subtract(b); - } else { - b = b.subtract(a); - } - } - return b.equals(goog.math.Long.ZERO) ? a : b; -} - -function pqPrimeLong(what) { - // console.log('start long'); - var it = 0, - g; - for (var i = 0; i < 3; i++) { - var q = goog.math.Long.fromInt((nextRandomInt(128) & 15) + 17), - x = goog.math.Long.fromInt(nextRandomInt(1000000000) + 1), - y = x, - lim = 1 << (i + 18); - - for (var j = 1; j < lim; j++) { - ++it; - // if (!(it % 100)) { - // console.log(dT(), 'it', it, i, j, x.toString()); - // } - var a = x, - b = x, - c = q; - - while (b.notEquals(goog.math.Long.ZERO)) { - if (b.and(goog.math.Long.ONE).notEquals(goog.math.Long.ZERO)) { - c = c.add(a); - if (c.compare(what) > 0) { - c = c.subtract(what); - } - } - a = a.add(a); - if (a.compare(what) > 0) { - a = a.subtract(what); - } - b = b.shiftRight(1); - } - - x = c; - var z = x.compare(y) < 0 ? y.subtract(x) : x.subtract(y); - g = gcdLong(z, what); - if (g.notEquals(goog.math.Long.ONE)) { - break; - } - if ((j & (j - 1)) == 0) { - y = x; - } - } - if (g.compare(goog.math.Long.ONE) > 0) { - break; - } - } - - var f = what.div(g), P, Q; - - if (g.compare(f) > 0) { - P = f; - Q = g; - } else { - P = g; - Q = f; - } - - return [bytesFromHex(P.toString(16)), bytesFromHex(Q.toString(16))]; -} - - -function TLSerialization (options) { - options = options || {}; - this.maxLength = options.startMaxLength || 2048; // 2Kb - this.offset = 0; // in bytes - - this.createBuffer(); - - // this.debug = options.debug !== undefined ? options.debug : Config.Modes.debug; - this.mtproto = options.mtproto || false; - return this; -} - -TLSerialization.prototype.createBuffer = function () { - this.buffer = new ArrayBuffer(this.maxLength); - this.intView = new Int32Array(this.buffer); - this.byteView = new Uint8Array(this.buffer); -}; - -TLSerialization.prototype.getArray = function () { - var resultBuffer = new ArrayBuffer(this.offset); - var resultArray = new Int32Array(resultBuffer); - - resultArray.set(this.intView.subarray(0, this.offset / 4)); - - return resultArray; -}; - -TLSerialization.prototype.getBuffer = function () { - return this.getArray().buffer; -}; - -TLSerialization.prototype.getBytes = function () { - var bytes = []; - for (var i = 0; i < this.offset; i++) { - bytes.push(this.byteView[i]); - } - return bytes; -}; - -TLSerialization.prototype.checkLength = function (needBytes) { - if (this.offset + needBytes < this.maxLength) { - return; - } - - console.trace('Increase buffer', this.offset, needBytes, this.maxLength); - this.maxLength = Math.ceil(Math.max(this.maxLength * 2, this.offset + needBytes + 16) / 4) * 4; - var previousBuffer = this.buffer, - previousArray = new Int32Array(previousBuffer); - - this.createBuffer(); - - new Int32Array(this.buffer).set(previousArray); -}; - -TLSerialization.prototype.writeInt = function (i, field) { - this.debug && console.log('>>>', i.toString(16), i, field); - - this.checkLength(4); - this.intView[this.offset / 4] = i; - this.offset += 4; -}; - -TLSerialization.prototype.storeInt = function (i, field) { - this.writeInt(i, (field || '') + ':int'); -}; - -TLSerialization.prototype.storeBool = function (i, field) { - if (i) { - this.writeInt(0x997275b5, (field || '') + ':bool'); - } else { - this.writeInt(0xbc799737, (field || '') + ':bool'); - } -}; - -TLSerialization.prototype.storeLongP = function (iHigh, iLow, field) { - this.writeInt(iLow, (field || '') + ':long[low]'); - this.writeInt(iHigh, (field || '') + ':long[high]'); -}; - -TLSerialization.prototype.storeLong = function (sLong, field) { - if (angular.isArray(sLong)) { - if (sLong.length == 2) { - return this.storeLongP(sLong[0], sLong[1], field); - } else { - return this.storeIntBytes(sLong, 64, field); - } - } - - var divRem = bigStringInt(sLong).divideAndRemainder(bigint(0x100000000)); - - this.writeInt(intToUint(divRem[1].intValue()), (field || '') + ':long[low]'); - this.writeInt(intToUint(divRem[0].intValue()), (field || '') + ':long[high]'); -}; - -TLSerialization.prototype.storeDouble = function (f) { - var buffer = new ArrayBuffer(8); - var intView = new Int32Array(buffer); - var doubleView = new Float64Array(buffer); - - doubleView[0] = f; - - this.writeInt(intView[0], (field || '') + ':double[low]'); - this.writeInt(intView[1], (field || '') + ':double[high]'); -}; - -TLSerialization.prototype.storeString = function (s, field) { - this.debug && console.log('>>>', s, (field || '') + ':string'); - - var sUTF8 = unescape(encodeURIComponent(s)); - - this.checkLength(sUTF8.length + 8); - - - var len = sUTF8.length; - if (len <= 253) { - this.byteView[this.offset++] = len; - } else { - this.byteView[this.offset++] = 254; - this.byteView[this.offset++] = len & 0xFF; - this.byteView[this.offset++] = (len & 0xFF00) >> 8; - this.byteView[this.offset++] = (len & 0xFF0000) >> 16; - } - for (var i = 0; i < len; i++) { - this.byteView[this.offset++] = sUTF8.charCodeAt(i); - } - - // Padding - while (this.offset % 4) { - this.byteView[this.offset++] = 0; - } -} - - -TLSerialization.prototype.storeBytes = function (bytes, field) { - this.debug && console.log('>>>', bytesToHex(bytes), (field || '') + ':bytes'); - - this.checkLength(bytes.length + 8); - - var len = bytes.length; - if (len <= 253) { - this.byteView[this.offset++] = len; - } else { - this.byteView[this.offset++] = 254; - this.byteView[this.offset++] = len & 0xFF; - this.byteView[this.offset++] = (len & 0xFF00) >> 8; - this.byteView[this.offset++] = (len & 0xFF0000) >> 16; - } - for (var i = 0; i < len; i++) { - this.byteView[this.offset++] = bytes[i]; - } - - // Padding - while (this.offset % 4) { - this.byteView[this.offset++] = 0; - } -} - -TLSerialization.prototype.storeIntBytes = function (bytes, bits, field) { - var len = bytes.length; - if ((bits % 32) || (len * 8) != bits) { - throw new Error('Invalid bits: ' + bits + ', ' + bytes.length); - } - - this.debug && console.log('>>>', bytesToHex(bytes), (field || '') + ':int' + bits); - this.checkLength(len); - - for (var i = 0; i < len; i++) { - this.byteView[this.offset++] = bytes[i]; - } -}; - -TLSerialization.prototype.storeRawBytes = function (bytes, field) { - var len = bytes.length; - - this.debug && console.log('>>>', bytesToHex(bytes), (field || '')); - this.checkLength(len); - - for (var i = 0; i < len; i++) { - this.byteView[this.offset++] = bytes[i]; - } -}; - - -TLSerialization.prototype.storeMethod = function (methodName, params) { - var schema = this.mtproto ? Config.Schema.MTProto : Config.Schema.API, - methodData = false, - i; - - for (i = 0; i < schema.methods.length; i++) { - if (schema.methods[i].method == methodName) { - methodData = schema.methods[i]; - break - } - } - if (!methodData) { - throw new Error('No method ' + methodName + ' found'); - } - - this.storeInt(intToUint(methodData.id), methodName + '[id]'); - - var self = this; - angular.forEach(methodData.params, function (param) { - self.storeObject(params[param.name], param.type, methodName + '[' + param.name + ']'); - }); - - return methodData.type; -}; - -TLSerialization.prototype.storeObject = function (obj, type, field) { - switch (type) { - case 'int': return this.storeInt(obj, field); - case 'long': return this.storeLong(obj, field); - case 'int128': return this.storeIntBytes(obj, 128, field); - case 'int256': return this.storeIntBytes(obj, 256, field); - case 'int512': return this.storeIntBytes(obj, 512, field); - case 'string': return this.storeString(obj, field); - case 'bytes': return this.storeBytes(obj, field); - case 'double': return this.storeDouble(obj, field); - case 'Bool': return this.storeBool(obj, field); - } - - if (angular.isArray(obj)) { - if (type.substr(0, 6) == 'Vector') { - this.writeInt(0x1cb5c415, field + '[id]'); - } - else if (type.substr(0, 6) != 'vector') { - throw new Error('Invalid vector type ' + type); - } - var itemType = type.substr(7, type.length - 8); // for "Vector" - this.writeInt(obj.length, field + '[count]'); - for (var i = 0; i < obj.length; i++) { - this.storeObject(obj[i], itemType, field + '[' + i + ']'); - } - return true; - } - else if (type.substr(0, 6).toLowerCase() == 'vector') { - throw new Error('Invalid vector object'); - } - - if (!angular.isObject(obj)) { - throw new Error('Invalid object for type ' + type); - } - - var schema = this.mtproto ? Config.Schema.MTProto : Config.Schema.API, - predicate = obj['_'], - isBare = false, - constructorData = false, - i; - - if (isBare = (type.charAt(0) == '%')) { - type = type.substr(1); - } - - for (i = 0; i < schema.constructors.length; i++) { - if (schema.constructors[i].predicate == predicate) { - constructorData = schema.constructors[i]; - break - } - } - if (!constructorData) { - throw new Error('No predicate ' + predicate + ' found'); - } - - if (predicate == type) { - isBare = true; - } - - if (!isBare) { - this.writeInt(intToUint(constructorData.id), field + '[' + predicate + '][id]'); - } - - var self = this; - angular.forEach(constructorData.params, function (param) { - self.storeObject(obj[param.name], param.type, field + '[' + predicate + '][' + param.name + ']'); - }); - - return constructorData.type; -}; - - - -function TLDeserialization (buffer, options) { - options = options || {}; - - this.offset = 0; // in bytes - this.override = options.override || {}; - - this.buffer = buffer; - this.intView = new Uint32Array(this.buffer); - this.byteView = new Uint8Array(this.buffer); - - // this.debug = options.debug !== undefined ? options.debug : Config.Modes.debug; - this.mtproto = options.mtproto || false; - return this; -} - -TLDeserialization.prototype.readInt = function (field) { - if (this.offset >= this.intView.length * 4) { - throw new Error('Nothing to fetch: ' + field); - } - - var i = this.intView[this.offset / 4]; - - this.debug && console.log('<<<', i.toString(16), i, field); - - this.offset += 4; - - return i; -}; - -TLDeserialization.prototype.fetchInt = function (field) { - return this.readInt((field || '') + ':int'); -} - -TLDeserialization.prototype.fetchDouble = function (field) { - var buffer = new ArrayBuffer(8); - var intView = new Int32Array(buffer); - var doubleView = new Float64Array(buffer); - - intView[0] = this.readInt((field || '') + ':double[low]'), - intView[1] = this.readInt((field || '') + ':double[high]'); - - return doubleView[0]; -}; - -TLDeserialization.prototype.fetchLong = function (field) { - var iLow = this.readInt((field || '') + ':long[low]'), - iHigh = this.readInt((field || '') + ':long[high]'); - - var longDec = bigint(iHigh).shiftLeft(32).add(bigint(iLow)).toString(); - - return longDec; -} - -TLDeserialization.prototype.fetchBool = function (field) { - var i = this.readInt((field || '') + ':bool'); - if (i == 0x997275b5) { - return true; - } else if (i == 0xbc799737) { - return false - } - - this.offset -= 4; - return this.fetchObject('Object', field); -} - -TLDeserialization.prototype.fetchString = function (field) { - var len = this.byteView[this.offset++]; - - if (len == 254) { - var len = this.byteView[this.offset++] | - (this.byteView[this.offset++] << 8) | - (this.byteView[this.offset++] << 16); - } - - var sUTF8 = ''; - for (var i = 0; i < len; i++) { - sUTF8 += String.fromCharCode(this.byteView[this.offset++]); - } - - // Padding - while (this.offset % 4) { - this.offset++; - } - - try { - var s = decodeURIComponent(escape(sUTF8)); - } catch (e) { - var s = sUTF8; - } - - this.debug && console.log('<<<', s, (field || '') + ':string'); - - return s; -} - - -TLDeserialization.prototype.fetchBytes = function (field) { - var len = this.byteView[this.offset++]; - - if (len == 254) { - var len = this.byteView[this.offset++] | - (this.byteView[this.offset++] << 8) | - (this.byteView[this.offset++] << 16); - } - - var bytes = []; - for (var i = 0; i < len; i++) { - bytes.push(this.byteView[this.offset++]); - } - - // Padding - while (this.offset % 4) { - this.offset++; - } - - this.debug && console.log('<<<', bytesToHex(bytes), (field || '') + ':bytes'); - - return bytes; -} - -TLDeserialization.prototype.fetchIntBytes = function (bits, field) { - if (bits % 32) { - throw new Error('Invalid bits: ' + bits); - } - - var len = bits / 8; - var bytes = []; - for (var i = 0; i < len; i++) { - bytes.push(this.byteView[this.offset++]); - } - - this.debug && console.log('<<<', bytesToHex(bytes), (field || '') + ':int' + bits); - - return bytes; -}; - - -TLDeserialization.prototype.fetchRawBytes = function (len, field) { - if (len === false) { - len = this.readInt((field || '') + '_length'); - } - - var bytes = []; - for (var i = 0; i < len; i++) { - bytes.push(this.byteView[this.offset++]); - } - - this.debug && console.log('<<<', bytesToHex(bytes), (field || '')); - - return bytes; -}; - -TLDeserialization.prototype.fetchObject = function (type, field) { - switch (type) { - case 'int': return this.fetchInt(field); - case 'long': return this.fetchLong(field); - case 'int128': return this.fetchIntBytes(128, field); - case 'int256': return this.fetchIntBytes(256, field); - case 'int512': return this.fetchIntBytes(512, field); - case 'string': return this.fetchString(field); - case 'bytes': return this.fetchBytes(field); - case 'double': return this.fetchDouble(field); - case 'Bool': return this.fetchBool(field); - } - - field = field || type || 'Object'; - - if (type.substr(0, 6) == 'Vector' || type.substr(0, 6) == 'vector') { - if (type.charAt(0) == 'V') { - var constructor = this.readInt(field + '[id]'); - if (constructor != 0x1cb5c415) { - throw new Error('Invalid vector constructor ' + constructor); - } - } - var len = this.readInt(field + '[count]'); - var result = []; - if (len > 0) { - var itemType = type.substr(7, type.length - 8); // for "Vector" - for (var i = 0; i < len; i++) { - result.push(this.fetchObject(itemType, field + '[' + i + ']')) - } - } - - return result; - } - - var schema = this.mtproto ? Config.Schema.MTProto : Config.Schema.API, - predicate = false, - constructorData = false; - - if (type.charAt(0) == '%') { - var checkType = type.substr(1); - for (i = 0; i < schema.constructors.length; i++) { - if (schema.constructors[i].type == checkType) { - constructorData = schema.constructors[i]; - break - } - } - if (!constructorData) { - throw new Error('Constructor not found for type: ' + type); - } - } - else if (type.charAt(0) >= 97 && type.charAt(0) <= 122) { - for (i = 0; i < schema.constructors.length; i++) { - if (schema.constructors[i].predicate == type) { - constructorData = schema.constructors[i]; - break - } - } - if (!constructorData) { - throw new Error('Constructor not found for predicate: ' + type); - } - } - else { - var constructor = this.readInt(field + '[id]'), - constructorCmp = uintToInt(constructor); - - if (constructorCmp == 0x3072cfa1) { // Gzip packed - var compressed = this.fetchBytes(field + '[packed_string]'), - uncompressed = gzipUncompress(compressed), - buffer = bytesToArrayBuffer(uncompressed), - newDeserializer = (new TLDeserialization(buffer)); - - return newDeserializer.fetchObject(type, field); - } - - for (i = 0; i < schema.constructors.length; i++) { - if (schema.constructors[i].id == constructorCmp) { - constructorData = schema.constructors[i]; - break; - } - } - - var fallback = false; - if (!constructorData && this.mtproto) { - var schemaFallback = Config.Schema.API; - for (i = 0; i < schemaFallback.constructors.length; i++) { - if (schemaFallback.constructors[i].id == constructorCmp) { - constructorData = schemaFallback.constructors[i]; - - delete this.mtproto; - fallback = true; - break; - } - } - } - if (!constructorData) { - throw new Error('Constructor not found: ' + constructor); - } - } - - predicate = constructorData.predicate; - - var result = {'_': predicate}, - overrideKey = (this.mtproto ? 'mt_' : '') + predicate, - self = this; - - - if (this.override[overrideKey]) { - this.override[overrideKey].apply(this, [result, field + '[' + predicate + ']']); - } else { - angular.forEach(constructorData.params, function (param) { - result[param.name] = self.fetchObject(param.type, field + '[' + predicate + '][' + param.name + ']'); - }); - } - - if (fallback) { - this.mtproto = true; - } - - return result; -}; - -TLDeserialization.prototype.getOffset = function () { - return this.offset; -}; - -TLDeserialization.prototype.fetchEnd = function () { - if (this.offset != this.byteView.length) { - throw new Error('Fetch end with non-empty buffer'); - } - return true; -}; +angular.module('izhukov.mtproto', ['izhukov.utils']) -if (typeof angular != 'undefined') angular.module('mtproto.services', ['myApp.services']). - -factory('MtpDcConfigurator', function () { +.factory('MtpDcConfigurator', function () { var dcOptions = Config.Modes.test ? [ {id: 1, host: '173.240.5.253', port: 80}, @@ -1038,9 +43,9 @@ factory('MtpDcConfigurator', function () { return { chooseServer: chooseServer }; -}). +}) -factory('MtpRsaKeysManager', function () { +.factory('MtpRsaKeysManager', function () { /** * Server public key, obtained from here: https://core.telegram.org/api/obtaining_api_id @@ -1109,17 +114,17 @@ factory('MtpRsaKeysManager', function () { prepare: prepareRsaKeys, select: selectRsaKeyByFingerPrint }; -}). +}) -service('MtpSecureRandom', function () { +.service('MtpSecureRandom', function () { return new SecureRandom(); -}). +}) -factory('MtpMessageIdGenerator', function (AppConfigManager) { +.factory('MtpMessageIdGenerator', function (Storage) { var lastMessageID = [0, 0], timeOffset = 0; - AppConfigManager.get('server_time_offset').then(function (to) { + Storage.get('server_time_offset').then(function (to) { if (to) { timeOffset = to; } @@ -1148,7 +153,7 @@ factory('MtpMessageIdGenerator', function (AppConfigManager) { function applyServerTime (serverTime, localTime) { var newTimeOffset = serverTime - Math.floor((localTime || tsNow()) / 1000), changed = Math.abs(timeOffset - newTimeOffset) > 10; - AppConfigManager.set({server_time_offset: newTimeOffset}); + Storage.set({server_time_offset: newTimeOffset}); lastMessageID = [0, 0]; timeOffset = newTimeOffset; @@ -1161,9 +166,9 @@ factory('MtpMessageIdGenerator', function (AppConfigManager) { generateID: generateMessageID, applyServerTime: applyServerTime }; -}). +}) -factory('MtpAuthorizer', function (MtpDcConfigurator, MtpRsaKeysManager, MtpSecureRandom, MtpMessageIdGenerator, $http, $q, $timeout) { +.factory('MtpAuthorizer', function (MtpDcConfigurator, MtpRsaKeysManager, MtpSecureRandom, MtpMessageIdGenerator, $http, $q, $timeout) { function mtpSendPlainRequest (dcID, requestBuffer) { var requestLength = requestBuffer.byteLength, @@ -1536,9 +541,9 @@ factory('MtpAuthorizer', function (MtpDcConfigurator, MtpRsaKeysManager, MtpSecu auth: mtpAuth }; -}). +}) -factory('MtpAesService', function ($q) { +.factory('MtpAesService', function ($q) { if (!window.Worker || true) { return { encrypt: function (bytes, keyBytes, ivBytes) { @@ -1575,7 +580,7 @@ factory('MtpAesService', function ($q) { // console.log('AES post message', {taskID: taskID, task: 'encrypt', bytes: bytes, keyBytes: keyBytes, ivBytes: ivBytes}) worker.postMessage({taskID: taskID, task: 'encrypt', bytes: bytes, keyBytes: keyBytes, ivBytes: ivBytes}); - taskID++ + taskID++; return deferred.promise; }, @@ -1590,10 +595,9 @@ factory('MtpAesService', function ($q) { return deferred.promise; } } -}). - +}) -factory('MtpSha1Service', function ($q) { +.factory('MtpSha1Service', function ($q) { if (!window.Worker || true) { return { hash: function (bytes) { @@ -1637,9 +641,9 @@ factory('MtpSha1Service', function ($q) { return deferred.promise; } } -}). +}) -factory('MtpNetworkerFactory', function (MtpDcConfigurator, MtpMessageIdGenerator, MtpSecureRandom, MtpSha1Service, MtpAesService, AppConfigManager, $http, $q, $timeout, $interval, $rootScope) { +.factory('MtpNetworkerFactory', function (MtpDcConfigurator, MtpMessageIdGenerator, MtpSecureRandom, MtpSha1Service, MtpAesService, Storage, $http, $q, $timeout, $interval, $rootScope) { var updatesProcessor, iii = 0, @@ -1813,7 +817,7 @@ factory('MtpNetworkerFactory', function (MtpDcConfigurator, MtpMessageIdGenerato if (Config.Modes.debug) { console.log(dT(), 'Api call', method, params, messageID, seqNo, options); } else { - console.log(dT(), 'Api call', method, messageID, seqNo); + console.log(dT(), 'Api call', method); } return this.pushMessage(message, options); @@ -1826,7 +830,7 @@ factory('MtpNetworkerFactory', function (MtpDcConfigurator, MtpMessageIdGenerato return false; } var self = this; - AppConfigManager.get('dc').then(function (baseDcID) { + Storage.get('dc').then(function (baseDcID) { if (isClean && (baseDcID != self.dcID || self.upload)) { // console.warn('send long-poll for guest DC is delayed', self.dcID); return; @@ -2299,7 +1303,7 @@ factory('MtpNetworkerFactory', function (MtpDcConfigurator, MtpMessageIdGenerato var storeObj = {}; storeObj['dc' + this.dcID + '_server_salt'] = bytesToHex(serverSalt); - AppConfigManager.set(storeObj); + Storage.set(storeObj); this.serverSalt = serverSalt; return true; @@ -2520,793 +1524,4 @@ factory('MtpNetworkerFactory', function (MtpDcConfigurator, MtpMessageIdGenerato } }; -}). - -factory('MtpApiManager', function (AppConfigManager, MtpAuthorizer, MtpNetworkerFactory, ErrorService, $q) { - var cachedNetworkers = {}, - cachedUploadNetworkers = {}, - cachedExportPromise = {}, - baseDcID = false; - - AppConfigManager.get('dc').then(function (dcID) { - if (dcID) { - baseDcID = dcID; - } - }); - - function mtpSetUserAuth (dcID, userAuth) { - AppConfigManager.set({ - dc: dcID, - user_auth: angular.extend({dcID: dcID}, userAuth) - }); - - baseDcID = dcID; - } - - function mtpLogOut () { - return mtpInvokeApi('auth.logOut').then(function () { - AppConfigManager.remove('dc', 'user_auth'); - - baseDcID = false; - }, function (error) { - AppConfigManager.remove('dc', 'user_auth'); - if (error && error.code != 401) { - AppConfigManager.remove('dc' + baseDcID + '_auth_key'); - } - baseDcID = false; - error.handled = true; - }); - } - - function mtpGetNetworker (dcID, options) { - options = options || {}; - - var cache = (options.fileUpload || options.fileDownload) - ? cachedUploadNetworkers - : cachedNetworkers; - if (!dcID) { - throw new Exception('get Networker without dcID'); - } - - if (cache[dcID] !== undefined) { - return $q.when(cache[dcID]); - } - - var akk = 'dc' + dcID + '_auth_key', - ssk = 'dc' + dcID + '_server_salt'; - - return AppConfigManager.get(akk, ssk).then(function (result) { - - if (cache[dcID] !== undefined) { - return cache[dcID]; - } - - var authKeyHex = result[0], - serverSaltHex = result[1]; - // console.log('ass', dcID, authKeyHex, serverSaltHex); - if (authKeyHex && authKeyHex.length == 512) { - var authKey = bytesFromHex(authKeyHex); - var serverSalt = bytesFromHex(serverSaltHex); - - return cache[dcID] = MtpNetworkerFactory.getNetworker(dcID, authKey, serverSalt, options); - } - - if (!options.createNetworker) { - return $q.reject({type: 'AUTH_KEY_EMPTY', code: 401}); - } - - return MtpAuthorizer.auth(dcID).then(function (auth) { - var storeObj = {}; - storeObj[akk] = bytesToHex(auth.authKey); - storeObj[ssk] = bytesToHex(auth.serverSalt); - AppConfigManager.set(storeObj); - - return cache[dcID] = MtpNetworkerFactory.getNetworker(dcID, auth.authKey, auth.serverSalt, options); - }, function (error) { - console.log('Get networker error', error, error.stack); - return $q.reject(error); - }); - }); - }; - - function mtpInvokeApi (method, params, options) { - options = options || {}; - - var deferred = $q.defer(), - rejectPromise = function (error) { - if (!error) { - error = {type: 'ERROR_EMPTY'}; - } else if (!angular.isObject(error)) { - error = {message: error}; - } - deferred.reject(error); - - if (!options.noErrorBox) { - error.input = method; - error.stack = error.stack || (new Error()).stack; - setTimeout(function () { - if (!error.handled) { - ErrorService.show({error: error}); - error.handled = true; - } - }, 100); - } - }, - dcID, - networkerPromise; - - if (dcID = options.dcID) { - networkerPromise = mtpGetNetworker(dcID, options); - } else { - networkerPromise = AppConfigManager.get('dc').then(function (baseDcID) { - return mtpGetNetworker(dcID = baseDcID || 1, options); - }); - } - - var cachedNetworker, - stack = false; - - networkerPromise.then(function (networker) { - return (cachedNetworker = networker).wrapApiCall(method, params, options).then( - function (result) { - deferred.resolve(result); - // $timeout(function () { - // deferred.resolve(result); - // }, 1000); - }, - function (error) { - console.error(dT(), 'Error', error.code, error.type, baseDcID, dcID); - if (error.code == 401 && baseDcID == dcID) { - AppConfigManager.remove('dc', 'user_auth'); - } - else if (error.code == 401 && baseDcID && dcID != baseDcID) { - if (cachedExportPromise[dcID] === undefined) { - var exportDeferred = $q.defer(); - - mtpInvokeApi('auth.exportAuthorization', {dc_id: dcID}, {noErrorBox: true}).then(function (exportedAuth) { - mtpInvokeApi('auth.importAuthorization', { - id: exportedAuth.id, - bytes: exportedAuth.bytes - }, {dcID: dcID, noErrorBox: true}).then(function () { - exportDeferred.resolve(); - }, function (e) { - exportDeferred.reject(e); - }) - }, function (e) { - exportDeferred.reject(e) - }); - - cachedExportPromise[dcID] = exportDeferred.promise; - } - - cachedExportPromise[dcID].then(function () { - (cachedNetworker = networker).wrapApiCall(method, params, options).then(function (result) { - deferred.resolve(result); - }, function (error) { - rejectPromise(error); - }); - }, function (error) { - rejectPromise(error); - }); - } - else if (error.code == 303) { - var newDcID = error.type.match(/^(PHONE_MIGRATE_|NETWORK_MIGRATE_)(\d+)/)[2]; - if (newDcID != dcID) { - if (options.dcID) { - options.dcID = newDcID; - } else { - AppConfigManager.set({dc: baseDcID = newDcID}); - } - - mtpGetNetworker(newDcID, options).then(function (networker) { - networker.wrapApiCall(method, params, options).then(function (result) { - deferred.resolve(result); - }, function (error) { - rejectPromise(error); - }); - }); - } - } - else { - rejectPromise(error); - } - }); - }, function (error) { - rejectPromise(error); - }); - - if (!(stack = (stack || (new Error()).stack))) { - try {window.unexistingFunction();} catch (e) { - stack = e.stack || ''; - } - } - - return deferred.promise; - }; - - function mtpGetUserID () { - return AppConfigManager.get('user_auth').then(function (auth) { - return auth.id || 0; - }); - } - - function getBaseDcID () { - return baseDcID || false; - } - - return { - getBaseDcID: getBaseDcID, - getUserID: mtpGetUserID, - invokeApi: mtpInvokeApi, - setUserAuth: mtpSetUserAuth, - logOut: mtpLogOut - } -}). - - -factory('MtpApiFileManager', function (MtpApiManager, $q, $window) { - - var cachedFs = false; - var cachedFsPromise = false; - var apiUploadPromise = $q.when(); - var cachedSavePromises = {}; - var cachedDownloadPromises = {}; - var cachedDownloads = {}; - - var downloadPulls = {}; - var downloadActives = {}; - var downloadLimit = 5; - - function downloadRequest(dcID, cb, activeDelta) { - if (downloadPulls[dcID] === undefined) { - downloadPulls[dcID] = []; - downloadActives[dcID] = 0 - } - var downloadPull = downloadPulls[dcID]; - var deferred = $q.defer(); - downloadPull.push({cb: cb, deferred: deferred, activeDelta: activeDelta}); - downloadCheck(dcID); - - return deferred.promise; - }; - - var index = 0; - - function downloadCheck(dcID) { - var downloadPull = downloadPulls[dcID]; - - if (downloadActives[dcID] >= downloadLimit || !downloadPull || !downloadPull.length) { - return false; - } - - var data = downloadPull.shift(), - activeDelta = data.activeDelta || 1; - - downloadActives[dcID] += activeDelta; - - var a = index++; - data.cb() - .then(function (result) { - downloadActives[dcID] -= activeDelta; - downloadCheck(dcID); - - data.deferred.resolve(result); - - }, function (error) { - downloadActives[dcID] -= activeDelta; - downloadCheck(dcID); - - data.deferred.reject(error); - }) - }; - - function requestFS () { - if (cachedFsPromise) { - return cachedFsPromise; - } - - $window.requestFileSystem = $window.requestFileSystem || $window.webkitRequestFileSystem; - - if (!$window.requestFileSystem) { - return cachedFsPromise = $q.reject({type: 'FS_BROWSER_UNSUPPORTED', description: 'requestFileSystem not present'}); - } - - var deferred = $q.defer(); - - $window.requestFileSystem($window.TEMPORARY, 5*1024*1024, function (fs) { - cachedFs = fs; - deferred.resolve(); - }, function (e) { - deferred.reject(e); - }); - - return cachedFsPromise = deferred.promise; - }; - - function fileWriteBytes(fileWriter, bytes) { - var deferred = $q.defer(); - - fileWriter.onwriteend = function(e) { - deferred.resolve(); - }; - fileWriter.onerror = function (e) { - deferred.reject(); - }; - - if (bytes instanceof Blob) { // is file bytes - fileWriter.write(bytes); - } else { - fileWriter.write(new Blob([bytesToArrayBuffer(bytes)])); - } - - return deferred.promise; - } - - function fileCopyTo (fromFileEntry, toFileEntry) { - var deferred = $q.defer(); - - toFileEntry.createWriter(function (fileWriter) { - fileWriteBytes(fileWriter, fromFileEntry).then(function () { - deferred.resolve(fileWriter); - }, function (e) { - deferred.reject(e); - fileWriter.truncate(0); - }); - }, function (e) { - deferred.reject(e); - }); - - return deferred.promise; - } - - function getFileName(location) { - switch (location._) { - case 'inputVideoFileLocation': - return 'video' + location.id + '.mp4'; - - case 'inputDocumentFileLocation': - return 'doc' + location.id; - - case 'inputAudioFileLocation': - return 'audio' + location.id; - } - - if (!location.volume_id) { - console.trace('Empty location', location); - } - - return location.volume_id + '_' + location.local_id + '_' + location.secret + '.jpg'; - }; - - function getTempFileName(file) { - var size = file.size || -1; - var random = nextRandomInt(0xFFFFFFFF); - return '_temp' + random + '_' + size; - }; - - function getCachedFile (location) { - if (!location) { - return false; - } - var fileName = getFileName(location); - - return cachedDownloads[fileName] || false; - } - - function saveSmallFile (location, bytes) { - var fileName = getFileName(location); - - if (cachedSavePromises[fileName]) { - return cachedSavePromises[fileName]; - } - var deferred = $q.defer(), - cacheFileWriter, - errorHandler = function (error) { - deferred.reject(error); - errorHandler = angular.noop; - if (cacheFileWriter) cacheFileWriter.truncate(0); - }; - - requestFS().then(function () { - cachedFs.root.getFile(fileName, {create: false}, function(fileEntry) { - deferred.resolve(cachedDownloads[fileName] = fileEntry.toURL()); - }, function () { - cachedFs.root.getFile(fileName, {create: true}, function(fileEntry) { - fileEntry.createWriter(function (fileWriter) { - cacheFileWriter = fileWriter; - fileWriteBytes(fileWriter, bytes).then(function () { - deferred.resolve(cachedDownloads[fileName] = fileEntry.toURL()); - }, errorHandler); - }, errorHandler); - }, errorHandler); - }); - }, function () { - deferred.resolve('data:image/jpeg;base64,' + bytesToBase64(bytes)) - }); - - return cachedSavePromises[fileName] = deferred.promise; - } - - function downloadSmallFile(location) { - // console.log('dload small', location); - var fileName = getFileName(location), - cachedPromise = cachedSavePromises[fileName] || cachedDownloadPromises[fileName]; - - if (cachedPromise) { - return cachedPromise; - } - - var deferred = $q.defer(), - cacheFileWriter, - errorHandler = function (error) { - deferred.reject(error); - errorHandler = angular.noop; - if (cacheFileWriter) cacheFileWriter.truncate(0); - }, - doDownload = function () { - cachedFs.root.getFile(fileName, {create: true}, function(fileEntry) { - var downloadPromise = downloadRequest(location.dc_id, function () { - // console.log('next small promise'); - return MtpApiManager.invokeApi('upload.getFile', { - location: angular.extend({}, location, {_: 'inputFileLocation'}), - offset: 0, - limit: 0 - }, { - dcID: location.dc_id, - fileDownload: true, - createNetworker: true - }); - }); - - fileEntry.createWriter(function (fileWriter) { - cacheFileWriter = fileWriter; - downloadPromise.then(function (result) { - fileWriteBytes(fileWriter, result.bytes).then(function () { - // console.log('Success', location, fileEntry.toURL()); - deferred.resolve(cachedDownloads[fileName] = fileEntry.toURL()); - }, errorHandler); - }, errorHandler); - }, errorHandler); - }, errorHandler); - }; - - requestFS().then(function () { - cachedFs.root.getFile(fileName, {create: false}, function(fileEntry) { - fileEntry.file(function(file) { - if (file.size) { - deferred.resolve(cachedDownloads[fileName] = fileEntry.toURL()); - } else { - console.log('Small file empty', file); - doDownload(); - } - }, errorHandler); - }, doDownload); - }, function (error) { - downloadRequest(location.dc_id, function () { - return MtpApiManager.invokeApi('upload.getFile', { - location: angular.extend({}, location, {_: 'inputFileLocation'}), - offset: 0, - limit: 0 - }, { - dcID: location.dc_id, - fileDownload: true, - createNetworker: true - }); - }).then(function (result) { - deferred.resolve(cachedDownloads[fileName] = 'data:image/jpeg;base64,' + bytesToBase64(result.bytes)) - }, errorHandler); - }); - - return cachedDownloadPromises[fileName] = deferred.promise; - } - - function downloadFile (dcID, location, size, options) { - options = options || {}; - - console.log(dT(), 'Dload file', dcID, location, size); - var fileName = getFileName(location), - toFileEntry = options.toFileEntry || null, - cachedPromise = cachedSavePromises[fileName] || cachedDownloadPromises[fileName]; - - if (!toFileEntry && cachedPromise) { - return cachedPromise; - } - - var deferred = $q.defer(), - canceled = false, - resolved = false, - cacheFileWriter, - errorHandler = function (error) { - // console.error('Dl Error', error); - deferred.reject(error); - errorHandler = angular.noop; - if (cacheFileWriter) cacheFileWriter.truncate(0); - }, - saveToFileEntry = function (fileEntry) { - fileEntry.createWriter(function (fileWriter) { - cacheFileWriter = fileWriter; - // console.time(fileName + ' ' + (size / 1024)); - - var limit = 524288; - // var limit = size > 16384 ? 524288 : 51200; - var writeFilePromise = $q.when(), - writeFileDeferred; - for (var offset = 0; offset < size; offset += limit) { - writeFileDeferred = $q.defer(); - (function (isFinal, offset, writeFileDeferred, writeFilePromise) { - return downloadRequest(dcID, function () { - // console.log('next big promise'); - if (canceled) { - return $q.when(); - } - return MtpApiManager.invokeApi('upload.getFile', { - location: location, - offset: offset, - limit: limit - }, { - dcID: dcID, - fileDownload: true, - createNetworker: true - }); - - }, 6).then(function (result) { - - // console.log('waiting for file promise', offset); - writeFilePromise.then(function () { - // console.log('resolved file promise', offset); - if (canceled) { - return $q.when(); - } - - return fileWriteBytes(fileWriter, result.bytes).then(function () { - - // console.log('resolve file promise', offset); - writeFileDeferred.resolve(); - - }, errorHandler).then(function () { - - if (isFinal) { - // console.timeEnd(fileName + ' ' + (size / 1024)); - resolved = true; - if (toFileEntry) { - deferred.resolve(); - } else { - deferred.resolve(cachedDownloads[fileName] = fileEntry.toURL(options.mime || 'image/jpeg')); - } - } else { - // console.log('notify', {done: offset + limit, total: size}); - deferred.notify({done: offset + limit, total: size}); - }; - - }); - - }); - - }); - - })(offset + limit >= size, offset, writeFileDeferred, writeFilePromise); - - writeFilePromise = writeFileDeferred.promise; - - } - }, errorHandler); - - }; - - requestFS().then(function () { - cachedFs.root.getFile(fileName, {create: false}, function(fileEntry) { - fileEntry.file(function(file) { - // console.log(dT(), 'Check size', file.size, size); - if (file.size >= size/* && false*/) { - resolved = true; - if (toFileEntry) { - fileCopyTo(file, toFileEntry).then(function () { - deferred.resolve(); - }) - } else { - deferred.resolve(cachedDownloads[fileName] = fileEntry.toURL()); - } - } else { - // setTimeout(function () { - console.log('File bad size', file, size); - if (toFileEntry) { - saveToFileEntry(toFileEntry); - } else { - cachedFs.root.getFile(fileName, {create: true}, saveToFileEntry, errorHandler) - } - // }, 10000); - } - }, errorHandler); - }, function () { - if (toFileEntry) { - saveToFileEntry(toFileEntry); - } else { - cachedFs.root.getFile(fileName, {create: true}, saveToFileEntry, errorHandler) - } - }); - }, function () { - - if (toFileEntry) { - return saveToFileEntry(toFileEntry); - } - - var blobParts = []; - var limit = size > 30400 ? 524288 : 4096; - var writeBlobPromise = $q.when(), - writeBlobDeferred; - for (var offset = 0; offset < size; offset += limit) { - writeBlobDeferred = $q.defer(); - (function (isFinal, offset, writeBlobDeferred, writeBlobPromise) { - return downloadRequest(dcID, function () { - if (canceled) { - return $q.when(); - } - return MtpApiManager.invokeApi('upload.getFile', { - location: location, - offset: offset, - limit: limit - }, { - dcID: dcID, - fileDownload: true, - createNetworker: true - }); - }, 6).then(function (result) { - writeBlobPromise.then(function () { - if (canceled) { - return $q.when(); - } - try { - blobParts.push(bytesToArrayBuffer(result.bytes)); - writeBlobDeferred.resolve(); - - if (isFinal) { - try { - var blob = new Blob(blobParts, {type: options.mime || 'image/jpeg'}); - } catch (e) { - window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder; - var bb = new BlobBuilder; - angular.forEach(blobParts, function(blobPart) { - bb.append(blobPart); - }); - var blob = bb.getBlob(options.mime || 'image/jpeg'); - } - - window.URL = window.URL || window.webkitURL; - resolved = true; - deferred.resolve(cachedDownloads[fileName] = URL.createObjectURL(blob)); - } else { - deferred.notify({done: offset + limit, total: size}); - }; - } catch (e) { - errorHandler(e); - } - }, errorHandler); - - }); - - })(offset + limit >= size, offset, writeBlobDeferred, writeBlobPromise); - - writeBlobPromise = writeBlobDeferred.promise; - - } - - }); - - deferred.promise.cancel = function () { - if (!canceled && !resolved) { - canceled = true; - delete cachedDownloadPromises[fileName]; - errorHandler({type: 'DOWNLOAD_CANCELED'}); - } - } - - if (!toFileEntry) { - cachedDownloadPromises[fileName] = deferred.promise; - } - - return deferred.promise; - } - - function uploadFile (file) { - var fileSize = file.size, - // partSize = fileSize > 102400 ? 65536 : 4096, - // partSize = fileSize > 102400 ? 524288 : 4096, - partSize = fileSize > 102400 ? 524288 : 30720, - totalParts = Math.ceil(fileSize / partSize), - canceled = false, - resolved = false, - doneParts = 0; - - if (totalParts > 1500) { - return $q.reject({type: 'FILE_TOO_BIG'}); - } - - var fileID = [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)], - deferred = $q.defer(), - errorHandler = function (error) { - // console.error('Up Error', error); - deferred.reject(error); - errorHandler = angular.noop; - }, - part = 0, - offset, - resultInputFile = { - _: 'inputFile', - id:fileID, - parts: totalParts, - name: file.name, - md5_checksum: '' - }; - - - var fileReadPromise = $q.when(); - - for (offset = 0; offset < fileSize; offset += partSize) { - (function (offset, part) { - fileReadPromise = fileReadPromise.then(function () { - var fileReadDeferred = $q.defer(); - - var reader = new FileReader(); - var blob = file.slice(offset, offset + partSize); - - reader.onloadend = function (e) { - if (canceled || e.target.readyState != FileReader.DONE) { - return; - } - var apiCurPromise = apiUploadPromise = apiUploadPromise.then(function () { - return MtpApiManager.invokeApi('upload.saveFilePart', { - file_id: fileID, - file_part: part, - bytes: bytesFromArrayBuffer(e.target.result) - }, { - startMaxLength: partSize + 256, - fileUpload: true - }); - }, errorHandler); - - apiCurPromise.then(function (result) { - doneParts++; - fileReadDeferred.resolve(); - if (doneParts >= totalParts) { - deferred.resolve(resultInputFile); - resolved = true; - } else { - console.log(dT(), 'Progress', doneParts * partSize / fileSize); - deferred.notify({done: doneParts * partSize, total: fileSize}); - } - }, errorHandler); - }; - - reader.readAsArrayBuffer(blob); - - return fileReadDeferred.promise; - }); - })(offset, part++); - } - - deferred.promise.cancel = function () { - console.log('cancel upload', canceled, resolved); - if (!canceled && !resolved) { - canceled = true; - errorHandler({type: 'UPLOAD_CANCELED'}); - } - } - - return deferred.promise; - } - - - return { - getCachedFile: getCachedFile, - downloadFile: downloadFile, - downloadSmallFile: downloadSmallFile, - saveSmallFile: saveSmallFile, - uploadFile: uploadFile - }; }) - - - - - - diff --git a/app/js/lib/mtproto_wrapper.js b/app/js/lib/mtproto_wrapper.js new file mode 100644 index 00000000..a53809c5 --- /dev/null +++ b/app/js/lib/mtproto_wrapper.js @@ -0,0 +1,586 @@ +/*! + * Webogram v0.1.6 - messaging web application for MTProto + * https://github.com/zhukov/webogram + * Copyright (C) 2014 Igor Zhukov + * https://github.com/zhukov/webogram/blob/master/LICENSE + */ + +angular.module('izhukov.mtproto.wrapper', ['izhukov.utils', 'izhukov.mtproto']) + +.factory('MtpApiManager', function (Storage, MtpAuthorizer, MtpNetworkerFactory, ErrorService, $q) { + var cachedNetworkers = {}, + cachedUploadNetworkers = {}, + cachedExportPromise = {}, + baseDcID = false; + + Storage.get('dc').then(function (dcID) { + if (dcID) { + baseDcID = dcID; + } + }); + + function mtpSetUserAuth (dcID, userAuth) { + Storage.set({ + dc: dcID, + user_auth: angular.extend({dcID: dcID}, userAuth) + }); + + baseDcID = dcID; + } + + function mtpLogOut () { + return mtpInvokeApi('auth.logOut').then(function () { + Storage.remove('dc', 'user_auth'); + + baseDcID = false; + }, function (error) { + Storage.remove('dc', 'user_auth'); + if (error && error.code != 401) { + Storage.remove('dc' + baseDcID + '_auth_key'); + } + baseDcID = false; + error.handled = true; + }); + } + + function mtpGetNetworker (dcID, options) { + options = options || {}; + + var cache = (options.fileUpload || options.fileDownload) + ? cachedUploadNetworkers + : cachedNetworkers; + if (!dcID) { + throw new Exception('get Networker without dcID'); + } + + if (cache[dcID] !== undefined) { + return $q.when(cache[dcID]); + } + + var akk = 'dc' + dcID + '_auth_key', + ssk = 'dc' + dcID + '_server_salt'; + + return Storage.get(akk, ssk).then(function (result) { + + if (cache[dcID] !== undefined) { + return cache[dcID]; + } + + var authKeyHex = result[0], + serverSaltHex = result[1]; + // console.log('ass', dcID, authKeyHex, serverSaltHex); + if (authKeyHex && authKeyHex.length == 512) { + var authKey = bytesFromHex(authKeyHex); + var serverSalt = bytesFromHex(serverSaltHex); + + return cache[dcID] = MtpNetworkerFactory.getNetworker(dcID, authKey, serverSalt, options); + } + + if (!options.createNetworker) { + return $q.reject({type: 'AUTH_KEY_EMPTY', code: 401}); + } + + return MtpAuthorizer.auth(dcID).then(function (auth) { + var storeObj = {}; + storeObj[akk] = bytesToHex(auth.authKey); + storeObj[ssk] = bytesToHex(auth.serverSalt); + Storage.set(storeObj); + + return cache[dcID] = MtpNetworkerFactory.getNetworker(dcID, auth.authKey, auth.serverSalt, options); + }, function (error) { + console.log('Get networker error', error, error.stack); + return $q.reject(error); + }); + }); + }; + + function mtpInvokeApi (method, params, options) { + options = options || {}; + + var deferred = $q.defer(), + rejectPromise = function (error) { + if (!error) { + error = {type: 'ERROR_EMPTY'}; + } else if (!angular.isObject(error)) { + error = {message: error}; + } + deferred.reject(error); + + if (!options.noErrorBox) { + error.input = method; + error.stack = error.stack || (new Error()).stack; + setTimeout(function () { + if (!error.handled) { + ErrorService.show({error: error}); + error.handled = true; + } + }, 100); + } + }, + dcID, + networkerPromise; + + if (dcID = options.dcID) { + networkerPromise = mtpGetNetworker(dcID, options); + } else { + networkerPromise = Storage.get('dc').then(function (baseDcID) { + return mtpGetNetworker(dcID = baseDcID || 1, options); + }); + } + + var cachedNetworker, + stack = false; + + networkerPromise.then(function (networker) { + return (cachedNetworker = networker).wrapApiCall(method, params, options).then( + function (result) { + deferred.resolve(result); + // $timeout(function () { + // deferred.resolve(result); + // }, 1000); + }, + function (error) { + console.error(dT(), 'Error', error.code, error.type, baseDcID, dcID); + if (error.code == 401 && baseDcID == dcID) { + Storage.remove('dc', 'user_auth'); + } + else if (error.code == 401 && baseDcID && dcID != baseDcID) { + if (cachedExportPromise[dcID] === undefined) { + var exportDeferred = $q.defer(); + + mtpInvokeApi('auth.exportAuthorization', {dc_id: dcID}, {noErrorBox: true}).then(function (exportedAuth) { + mtpInvokeApi('auth.importAuthorization', { + id: exportedAuth.id, + bytes: exportedAuth.bytes + }, {dcID: dcID, noErrorBox: true}).then(function () { + exportDeferred.resolve(); + }, function (e) { + exportDeferred.reject(e); + }) + }, function (e) { + exportDeferred.reject(e) + }); + + cachedExportPromise[dcID] = exportDeferred.promise; + } + + cachedExportPromise[dcID].then(function () { + (cachedNetworker = networker).wrapApiCall(method, params, options).then(function (result) { + deferred.resolve(result); + }, function (error) { + rejectPromise(error); + }); + }, function (error) { + rejectPromise(error); + }); + } + else if (error.code == 303) { + var newDcID = error.type.match(/^(PHONE_MIGRATE_|NETWORK_MIGRATE_)(\d+)/)[2]; + if (newDcID != dcID) { + if (options.dcID) { + options.dcID = newDcID; + } else { + Storage.set({dc: baseDcID = newDcID}); + } + + mtpGetNetworker(newDcID, options).then(function (networker) { + networker.wrapApiCall(method, params, options).then(function (result) { + deferred.resolve(result); + }, function (error) { + rejectPromise(error); + }); + }); + } + } + else { + rejectPromise(error); + } + }); + }, function (error) { + rejectPromise(error); + }); + + if (!(stack = (stack || (new Error()).stack))) { + try {window.unexistingFunction();} catch (e) { + stack = e.stack || ''; + } + } + + return deferred.promise; + }; + + function mtpGetUserID () { + return Storage.get('user_auth').then(function (auth) { + return auth.id || 0; + }); + } + + function getBaseDcID () { + return baseDcID || false; + } + + return { + getBaseDcID: getBaseDcID, + getUserID: mtpGetUserID, + invokeApi: mtpInvokeApi, + setUserAuth: mtpSetUserAuth, + logOut: mtpLogOut + } +}) + +.factory('MtpApiFileManager', function (MtpApiManager, $q, FileManager, IdbFileStorage, TmpfsFileStorage, MemoryFileStorage) { + + var cachedFs = false; + var cachedFsPromise = false; + var apiUploadPromise = $q.when(); + var cachedSavePromises = {}; + var cachedDownloadPromises = {}; + var cachedDownloads = {}; + + var downloadPulls = {}; + var downloadActives = {}; + var downloadLimit = 5; + + function downloadRequest(dcID, cb, activeDelta) { + if (downloadPulls[dcID] === undefined) { + downloadPulls[dcID] = []; + downloadActives[dcID] = 0 + } + var downloadPull = downloadPulls[dcID]; + var deferred = $q.defer(); + downloadPull.push({cb: cb, deferred: deferred, activeDelta: activeDelta}); + downloadCheck(dcID); + + return deferred.promise; + }; + + var index = 0; + + function downloadCheck(dcID) { + var downloadPull = downloadPulls[dcID]; + + if (downloadActives[dcID] >= downloadLimit || !downloadPull || !downloadPull.length) { + return false; + } + + var data = downloadPull.shift(), + activeDelta = data.activeDelta || 1; + + downloadActives[dcID] += activeDelta; + + var a = index++; + data.cb() + .then(function (result) { + downloadActives[dcID] -= activeDelta; + downloadCheck(dcID); + + data.deferred.resolve(result); + + }, function (error) { + downloadActives[dcID] -= activeDelta; + downloadCheck(dcID); + + data.deferred.reject(error); + }) + }; + + function getFileName(location) { + switch (location._) { + case 'inputVideoFileLocation': + return 'video' + location.id + '.mp4'; + + case 'inputDocumentFileLocation': + return 'doc' + location.id; + + case 'inputAudioFileLocation': + return 'audio' + location.id; + } + + if (!location.volume_id) { + console.trace('Empty location', location); + } + + return location.volume_id + '_' + location.local_id + '_' + location.secret + '.jpg'; + }; + + function getTempFileName(file) { + var size = file.size || -1; + var random = nextRandomInt(0xFFFFFFFF); + return '_temp' + random + '_' + size; + }; + + function getCachedFile (location) { + if (!location) { + return false; + } + var fileName = getFileName(location); + + return cachedDownloads[fileName] || false; + } + + function getFileStorage () { + if (TmpfsFileStorage.isAvailable()) { + return TmpfsFileStorage; + } + if (IdbFileStorage.isAvailable()) { + return IdbFileStorage; + } + return MemoryFileStorage; + } + + function saveSmallFile (location, bytes) { + var fileName = getFileName(location), + mimeType = 'image/jpeg'; + + if (!cachedSavePromises[fileName]) { + cachedSavePromises[fileName] = getFileStorage().saveFile(fileName, bytes).then(function (blob) { + return cachedDownloads[fileName] = FileManager.getUrl(blob, mimeType); + }); + } + return cachedSavePromises[fileName]; + } + + function downloadSmallFile(location) { + // console.log('dload small', location); + var fileName = getFileName(location), + mimeType = 'image/jpeg', + cachedPromise = cachedSavePromises[fileName] || cachedDownloadPromises[fileName]; + + if (cachedPromise) { + return cachedPromise; + } + + var fileStorage = getFileStorage(); + + return cachedDownloadPromises[fileName] = fileStorage.getFile(fileName).then(function (blob) { + return cachedDownloads[fileName] = FileManager.getUrl(blob, mimeType); + }, function () { + var downloadPromise = downloadRequest(location.dc_id, function () { + // console.log('next small promise'); + return MtpApiManager.invokeApi('upload.getFile', { + location: angular.extend({}, location, {_: 'inputFileLocation'}), + offset: 0, + limit: 0 + }, { + dcID: location.dc_id, + fileDownload: true, + createNetworker: true + }); + }); + + return fileStorage.getFileWriter(fileName, mimeType).then(function (fileWriter) { + return downloadPromise.then(function (result) { + return FileManager.write(fileWriter, result.bytes).then(function () { + return cachedDownloads[fileName] = FileManager.getUrl(fileWriter.finalize(), mimeType); + }); + }); + }); + }); + } + + function downloadFile (dcID, location, size, options) { + options = options || {}; + + // console.log(dT(), 'Dload file', dcID, location, size); + var fileName = getFileName(location), + toFileEntry = options.toFileEntry || null, + cachedPromise = cachedSavePromises[fileName] || cachedDownloadPromises[fileName]; + + var fileStorage = getFileStorage(); + + // console.log(dT(), 'fs', fileStorage, fileName, cachedPromise); + + if (cachedPromise) { + if (toFileEntry) { + return cachedPromise.then(function (url) { + return fileStorage.getFile(fileName).then(function (blob) { + return FileManager.copy(blob, toFileEntry); + }); + }) + } + return cachedPromise; + } + + var deferred = $q.defer(), + canceled = false, + resolved = false, + mimeType = options.mime || 'image/jpeg', + cacheFileWriter, + errorHandler = function (error) { + deferred.reject(error); + errorHandler = angular.noop; + if (cacheFileWriter) cacheFileWriter.truncate(0); + }; + + + fileStorage.getFile(fileName).then(function (blob) { + if (toFileEntry) { + FileManager.copy(blob, toFileEntry).then(function () { + deferred.resolve(); + }, errorHandler); + } else { + deferred.resolve(cachedDownloads[fileName] = FileManager.getUrl(blob, mimeType)); + } + }, function () { + var fileWriterPromise = toFileEntry ? $q.when(toFileEntry) : fileStorage.getFileWriter(fileName, mimeType); + + fileWriterPromise.then(function (fileWriter) { + cacheFileWriter = fileWriter; + var limit = 524288, + writeFilePromise = $q.when(), + writeFileDeferred; + for (var offset = 0; offset < size; offset += limit) { + writeFileDeferred = $q.defer(); + (function (isFinal, offset, writeFileDeferred, writeFilePromise) { + return downloadRequest(dcID, function () { + if (canceled) { + return $q.when(); + } + return MtpApiManager.invokeApi('upload.getFile', { + location: location, + offset: offset, + limit: limit + }, { + dcID: dcID, + fileDownload: true, + createNetworker: true + }); + }, 6).then(function (result) { + writeFilePromise.then(function () { + if (canceled) { + return $q.when(); + } + return FileManager.write(fileWriter, result.bytes).then(function () { + writeFileDeferred.resolve(); + }, errorHandler).then(function () { + if (isFinal) { + resolved = true; + if (toFileEntry) { + deferred.resolve(); + } else { + deferred.resolve(cachedDownloads[fileName] = FileManager.getUrl(fileWriter.finalize(), mimeType)); + } + } else { + deferred.notify({done: offset + limit, total: size}); + }; + }); + }); + }); + })(offset + limit >= size, offset, writeFileDeferred, writeFilePromise); + writeFilePromise = writeFileDeferred.promise; + } + }); + }) + + deferred.promise.cancel = function () { + if (!canceled && !resolved) { + canceled = true; + delete cachedDownloadPromises[fileName]; + errorHandler({type: 'DOWNLOAD_CANCELED'}); + } + } + + if (!toFileEntry) { + cachedDownloadPromises[fileName] = deferred.promise; + } + + return deferred.promise; + } + + function uploadFile (file) { + var fileSize = file.size, + // partSize = fileSize > 102400 ? 65536 : 4096, + // partSize = fileSize > 102400 ? 524288 : 4096, + partSize = fileSize > 102400 ? 524288 : 32768, + isBigFile = fileSize >= 10485760, + totalParts = Math.ceil(fileSize / partSize), + canceled = false, + resolved = false, + doneParts = 0; + + if (totalParts > 1500) { + return $q.reject({type: 'FILE_TOO_BIG'}); + } + + var fileID = [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)], + deferred = $q.defer(), + errorHandler = function (error) { + // console.error('Up Error', error); + deferred.reject(error); + errorHandler = angular.noop; + }, + part = 0, + offset, + resultInputFile = { + _: isBigFile ? 'inputFileBig' : 'inputFile', + id: fileID, + parts: totalParts, + name: file.name, + md5_checksum: '' + }; + + + var fileReadPromise = $q.when(); + + for (offset = 0; offset < fileSize; offset += partSize) { + (function (offset, part) { + fileReadPromise = fileReadPromise.then(function () { + var fileReadDeferred = $q.defer(); + + var reader = new FileReader(); + var blob = file.slice(offset, offset + partSize); + + reader.onloadend = function (e) { + if (canceled || e.target.readyState != FileReader.DONE) { + return; + } + var apiCurPromise = apiUploadPromise = apiUploadPromise.then(function () { + return MtpApiManager.invokeApi('upload.saveFilePart', { + file_id: fileID, + file_part: part, + bytes: bytesFromArrayBuffer(e.target.result) + }, { + startMaxLength: partSize + 256, + fileUpload: true + }); + }, errorHandler); + + apiCurPromise.then(function (result) { + doneParts++; + fileReadDeferred.resolve(); + if (doneParts >= totalParts) { + deferred.resolve(resultInputFile); + resolved = true; + } else { + console.log(dT(), 'Progress', doneParts * partSize / fileSize); + deferred.notify({done: doneParts * partSize, total: fileSize}); + } + }, errorHandler); + }; + + reader.readAsArrayBuffer(blob); + + return fileReadDeferred.promise; + }); + })(offset, part++); + } + + deferred.promise.cancel = function () { + console.log('cancel upload', canceled, resolved); + if (!canceled && !resolved) { + canceled = true; + errorHandler({type: 'UPLOAD_CANCELED'}); + } + } + + return deferred.promise; + } + + return { + getCachedFile: getCachedFile, + downloadFile: downloadFile, + downloadSmallFile: downloadSmallFile, + saveSmallFile: saveSmallFile, + uploadFile: uploadFile + }; +}) diff --git a/app/js/lib/ng_utils.js b/app/js/lib/ng_utils.js new file mode 100644 index 00000000..be46726b --- /dev/null +++ b/app/js/lib/ng_utils.js @@ -0,0 +1,518 @@ +/*! + * Webogram v0.1.6 - messaging web application for MTProto + * https://github.com/zhukov/webogram + * Copyright (C) 2014 Igor Zhukov + * https://github.com/zhukov/webogram/blob/master/LICENSE + */ + +angular.module('izhukov.utils', []) + +.provider('Storage', function () { + + var keyPrefix = ''; + var cache = {}; + var useCs = !!(window.chrome && chrome.storage && chrome.storage.local); + var useLs = !useCs && !!window.localStorage; + + this.setPrefix = function (newPrefix) { + keyPrefix = newPrefix + }; + + this.$get = ['$q', function ($q) { + function getValue() { + var keys = Array.prototype.slice.call(arguments), + result = [], + single = keys.length == 1, + allFound = true; + + for (var i = 0; i < keys.length; i++) { + keys[i] = keyPrefix + keys[i]; + } + + angular.forEach(keys, function (key) { + if (cache[key] !== undefined) { + result.push(cache[key]); + } + else if (useLs) { + var value = localStorage.getItem(key); + value = (value === undefined || value === null) ? false : JSON.parse(value); + result.push(cache[key] = value); + } + else if (!useCs) { + result.push(cache[key] = false); + } + else { + allFound = false; + } + }); + + if (allFound) { + return $q.when(single ? result[0] : result); + } + + var deferred = $q.defer(); + + chrome.storage.local.get(keys, function (resultObj) { + result = []; + angular.forEach(keys, function (key) { + var value = resultObj[key]; + value = value === undefined || value === null ? false : JSON.parse(value); + result.push(cache[key] = value); + }); + + deferred.resolve(single ? result[0] : result); + }); + + return deferred.promise; + }; + + function setValue(obj) { + var keyValues = {}; + angular.forEach(obj, function (value, key) { + keyValues[keyPrefix + key] = JSON.stringify(value); + cache[keyPrefix + key] = value; + }); + + if (useLs) { + angular.forEach(keyValues, function (value, key) { + localStorage.setItem(key, value); + }); + return $q.when(); + } + + if (!useCs) { + return $q.when(); + } + + var deferred = $q.defer(); + + chrome.storage.local.set(keyValues, function () { + deferred.resolve(); + }); + + return deferred.promise; + }; + + function removeValue () { + var keys = Array.prototype.slice.call(arguments); + + for (var i = 0; i < keys.length; i++) { + keys[i] = keyPrefix + keys[i]; + } + + angular.forEach(keys, function(key){ + delete cache[key]; + }); + + if (useLs) { + angular.forEach(keys, function(key){ + localStorage.removeItem(key); + }); + + return $q.when(); + } + + if (!useCs) { + return $q.when(); + } + + var deferred = $q.defer(); + + chrome.storage.local.remove(keys, function () { + deferred.resolve(); + }); + + return deferred.promise; + }; + + return { + get: getValue, + set: setValue, + remove: removeValue + }; + }]; + +}) + +.service('FileManager', function ($window, $timeout, $q) { + + $window.URL = $window.URL || $window.webkitURL; + $window.BlobBuilder = $window.BlobBuilder || $window.WebKitBlobBuilder || $window.MozBlobBuilder; + + function fileCopyTo (fromFileEntry, toFileEntry) { + var deferred = $q.defer(); + + toFileEntry.createWriter(function (fileWriter) { + fileWriteData(fileWriter, fromFileEntry).then(function () { + deferred.resolve(fileWriter); + }, function (e) { + deferred.reject(e); + fileWriter.truncate(0); + }); + }, function (e) { + deferred.reject(e); + }); + + return deferred.promise; + } + + function fileWriteData(fileWriter, bytes) { + var deferred = $q.defer(); + + fileWriter.onwriteend = function(e) { + deferred.resolve(); + }; + fileWriter.onerror = function (e) { + deferred.reject(); + }; + + if (bytes instanceof Blob) { // is file bytes + fileWriter.write(bytes); + } else { + fileWriter.write(new Blob([bytesToArrayBuffer(bytes)])); + } + + return deferred.promise; + } + + function chooseSaveFile (fileName, ext, mimeType) { + if (!$window.chrome || !chrome.fileSystem || !chrome.fileSystem.chooseEntry) { + return $q.reject(); + }; + var deferred = $q.defer(); + + chrome.fileSystem.chooseEntry({ + type: 'saveFile', + suggestedName: fileName, + accepts: [{ + mimeTypes: [mimeType], + extensions: [ext] + }] + }, function (writableFileEntry) { + deferred.resolve(writableFileEntry); + }); + + return deferred.promise; + } + + function getFakeFileWriter (mimeType, saveFileCallback) { + var blobParts = [], + fakeFileWriter = { + write: function (blob) { + blobParts.push(blob); + $timeout(function () { + if (fakeFileWriter.onwriteend) { + fakeFileWriter.onwriteend(); + } + }); + }, + truncate: function () { + blobParts = []; + }, + finalize: function () { + var blob; + try { + blob = new Blob(blobParts, {type: mimeType}); + } catch (e) { + var bb = new BlobBuilder; + angular.forEach(blobParts, function(blobPart) { + bb.append(blobPart); + }); + blob = bb.getBlob(mimeType); + } + if (saveFileCallback) { + saveFileCallback(blob); + } + return blob; + } + }; + + return fakeFileWriter; + }; + + function getUrl (fileData, mimeType) { + // console.log(dT(), 'get url', fileData, mimeType, fileData.toURL !== undefined, fileData instanceof Blob); + if (fileData.toURL !== undefined) { + return fileData.toURL(mimeType); + } + if (fileData instanceof Blob) { + return URL.createObjectURL(fileData); + } + return 'data:' + mimeType + ';base64,' + bytesToBase64(fileData); + } + + function downloadFile (url, mimeType, fileName) { + var anchor = $('Download') + .css({position: 'absolute', top: 1, left: 1}) + .attr('href', url) + .attr('target', '_blank') + .attr('download', fileName) + .appendTo('body'); + + anchor[0].dataset.downloadurl = [mimeType, fileName, url].join(':'); + anchor[0].click(); + $timeout(function () { + anchor.remove(); + }, 100); + } + + return { + copy: fileCopyTo, + write: fileWriteData, + getFakeFileWriter: getFakeFileWriter, + chooseSave: chooseSaveFile, + getUrl: getUrl, + download: downloadFile + }; +}) + +.service('IdbFileStorage', function ($q, $window, FileManager) { + + $window.indexedDB = $window.indexedDB || $window.webkitIndexedDB || $window.mozIndexedDB || $window.OIndexedDB || $window.msIndexedDB; + $window.IDBTransaction = $window.IDBTransaction || $window.webkitIDBTransaction || $window.OIDBTransaction || $window.msIDBTransaction; + + var dbName = 'cachedFiles', + dbStoreName = 'files', + dbVersion = 1, + openDbPromise, + storageIsAvailable = $window.indexedDB !== undefined && $window.IDBTransaction !== undefined; + + function isAvailable (argument) { + return storageIsAvailable; + } + + function openDatabase() { + if (openDbPromise) { + return openDbPromise; + } + + var request = indexedDB.open(dbName, dbVersion), + deferred = $q.defer(), + createObjectStore = function (db) { + db.createObjectStore(dbStoreName); + }; + + request.onsuccess = function (event) { + db = request.result; + + db.onerror = function (event) { + storageIsAvailable = false; + console.error("Error creating/accessing IndexedDB database", event); + deferred.reject(event); + }; + + // Interim solution for Google Chrome to create an objectStore. Will be deprecated + if (db.setVersion) { + if (db.version != dbVersion) { + db.setVersion(dbVersion).onsuccess = function () { + createObjectStore(db); + deferred.resolve(db); + }; + } + else { + deferred.resolve(db); + } + } + else { + deferred.resolve(db); + } + }; + + request.onupgradeneeded = function (event) { + createObjectStore(event.target.result); + }; + + return openDbPromise = deferred.promise; + }; + + function saveFile (fileName, blob) { + return openDatabase().then(function (db) { + try { + var deferred = $q.defer(), + objectStore = db.transaction([dbStoreName], IDBTransaction.READ_WRITE || 'readwrite').objectStore(dbStoreName), + request = objectStore.put(blob, fileName); + } catch (error) { + storageIsAvailable = false; + return $q.reject(error); + } + + request.onsuccess = function (event) { + deferred.resolve(blob); + }; + + request.onerror = function (error) { + deferred.reject(error); + }; + + return deferred.promise; + }); + }; + + function getFile (fileName) { + return openDatabase().then(function (db) { + var deferred = $q.defer(), + objectStore = db.transaction([dbStoreName], IDBTransaction.READ || 'readonly').objectStore(dbStoreName), + request = objectStore.get(fileName); + + request.onsuccess = function (event) { + if (event.target.result === undefined) { + deferred.reject(); + } else { + deferred.resolve(event.target.result); + } + }; + + request.onerror = function (error) { + deferred.reject(error); + }; + + return deferred.promise; + }); + } + + function getFileWriter (fileName, mimeType) { + var fakeWriter = FileManager.getFakeFileWriter(mimeType, function (blob) { + saveFile(fileName, blob); + }); + return $q.when(fakeWriter); + } + + return { + isAvailable: isAvailable, + saveFile: saveFile, + getFile: getFile, + getFileWriter: getFileWriter + }; +}) + + +.service('TmpfsFileStorage', function ($q, $window, FileManager) { + + $window.requestFileSystem = $window.requestFileSystem || $window.webkitRequestFileSystem; + + var reqFsPromise, + fileSystem, + storageIsAvailable = $window.requestFileSystem !== undefined; + + function requestFS () { + if (reqFsPromise) { + return reqFsPromise; + } + + if (!$window.requestFileSystem) { + return reqFsPromise = $q.reject({type: 'FS_BROWSER_UNSUPPORTED', description: 'requestFileSystem not present'}); + } + + var deferred = $q.defer(); + + $window.requestFileSystem($window.TEMPORARY, 5*1024*1024, function (fs) { + cachedFs = fs; + deferred.resolve(); + }, function (e) { + storageIsAvailable = false; + deferred.reject(e); + }); + + return reqFsPromise = deferred.promise; + }; + + function isAvailable () { + return storageIsAvailable; + } + + function getFile (fileName, size) { + size = size || 1; + return requestFS().then(function () { + // console.log(dT(), 'get file', fileName); + var deferred = $q.defer(); + cachedFs.root.getFile(fileName, {create: false}, function(fileEntry) { + fileEntry.file(function(file) { + // console.log(dT(), 'aa', file); + if (file.size >= size) { + deferred.resolve(fileEntry); + } else { + deferred.reject(new Error('FILE_NOT_FOUND')); + } + }, function (error) { + console.log(dT(), 'error', error); + deferred.reject(error); + }); + }, function () { + deferred.reject(new Error('FILE_NOT_FOUND')); + }); + return deferred.promise; + }); + } + + function saveFile (fileName, blob) { + return getFileWriter(fileName).then(function (fileWriter) { + return FileManager.write(fileWriter, blob).then(function () { + return fileWriter.finalize(); + }) + }); + } + + function getFileWriter (fileName) { + // console.log(dT(), 'get file writer', fileName); + return requestFS().then(function () { + var deferred = $q.defer(); + cachedFs.root.getFile(fileName, {create: true}, function (fileEntry) { + fileEntry.createWriter(function (fileWriter) { + fileWriter.finalize = function () { + return fileEntry; + } + // console.log(dT(), 'got writer'); + deferred.resolve(fileWriter); + }, function (error) { + deferred.reject(error); + }); + }, function (error) { + deferred.reject(error); + }); + + return deferred.promise; + }) + } + + return { + isAvailable: isAvailable, + saveFile: saveFile, + getFile: getFile, + getFileWriter: getFileWriter + }; +}) + +.service('MemoryFileStorage', function ($q, FileManager) { + + var storage = {}; + + function isAvailable () { + return true; + } + + function getFile (fileName, size) { + if (storage[fileName]) { + return $q.when(storage[fileName]); + } + return $q.reject(new Error('FILE_NOT_FOUND')); + } + + function saveFile (fileName, blob) { + return $q.when(storage[fileName] = blob); + } + + function getFileWriter (fileName, mimeType) { + var fakeWriter = FileManager.getFakeFileWriter(mimeType, function (blob) { + saveFile(fileName, blob); + }); + return $q.when(fakeWriter); + } + + return { + isAvailable: isAvailable, + saveFile: saveFile, + getFile: getFile, + getFileWriter: getFileWriter + }; +}) + diff --git a/app/js/lib/tl_utils.js b/app/js/lib/tl_utils.js new file mode 100644 index 00000000..0dd629fd --- /dev/null +++ b/app/js/lib/tl_utils.js @@ -0,0 +1,571 @@ +/*! + * Webogram v0.1.6 - messaging web application for MTProto + * https://github.com/zhukov/webogram + * Copyright (C) 2014 Igor Zhukov + * https://github.com/zhukov/webogram/blob/master/LICENSE + */ + + function TLSerialization (options) { + options = options || {}; + this.maxLength = options.startMaxLength || 2048; // 2Kb + this.offset = 0; // in bytes + + this.createBuffer(); + + // this.debug = options.debug !== undefined ? options.debug : Config.Modes.debug; + this.mtproto = options.mtproto || false; + return this; +} + +TLSerialization.prototype.createBuffer = function () { + this.buffer = new ArrayBuffer(this.maxLength); + this.intView = new Int32Array(this.buffer); + this.byteView = new Uint8Array(this.buffer); +}; + +TLSerialization.prototype.getArray = function () { + var resultBuffer = new ArrayBuffer(this.offset); + var resultArray = new Int32Array(resultBuffer); + + resultArray.set(this.intView.subarray(0, this.offset / 4)); + + return resultArray; +}; + +TLSerialization.prototype.getBuffer = function () { + return this.getArray().buffer; +}; + +TLSerialization.prototype.getBytes = function () { + var bytes = []; + for (var i = 0; i < this.offset; i++) { + bytes.push(this.byteView[i]); + } + return bytes; +}; + +TLSerialization.prototype.checkLength = function (needBytes) { + if (this.offset + needBytes < this.maxLength) { + return; + } + + console.trace('Increase buffer', this.offset, needBytes, this.maxLength); + this.maxLength = Math.ceil(Math.max(this.maxLength * 2, this.offset + needBytes + 16) / 4) * 4; + var previousBuffer = this.buffer, + previousArray = new Int32Array(previousBuffer); + + this.createBuffer(); + + new Int32Array(this.buffer).set(previousArray); +}; + +TLSerialization.prototype.writeInt = function (i, field) { + this.debug && console.log('>>>', i.toString(16), i, field); + + this.checkLength(4); + this.intView[this.offset / 4] = i; + this.offset += 4; +}; + +TLSerialization.prototype.storeInt = function (i, field) { + this.writeInt(i, (field || '') + ':int'); +}; + +TLSerialization.prototype.storeBool = function (i, field) { + if (i) { + this.writeInt(0x997275b5, (field || '') + ':bool'); + } else { + this.writeInt(0xbc799737, (field || '') + ':bool'); + } +}; + +TLSerialization.prototype.storeLongP = function (iHigh, iLow, field) { + this.writeInt(iLow, (field || '') + ':long[low]'); + this.writeInt(iHigh, (field || '') + ':long[high]'); +}; + +TLSerialization.prototype.storeLong = function (sLong, field) { + if (angular.isArray(sLong)) { + if (sLong.length == 2) { + return this.storeLongP(sLong[0], sLong[1], field); + } else { + return this.storeIntBytes(sLong, 64, field); + } + } + + var divRem = bigStringInt(sLong).divideAndRemainder(bigint(0x100000000)); + + this.writeInt(intToUint(divRem[1].intValue()), (field || '') + ':long[low]'); + this.writeInt(intToUint(divRem[0].intValue()), (field || '') + ':long[high]'); +}; + +TLSerialization.prototype.storeDouble = function (f) { + var buffer = new ArrayBuffer(8); + var intView = new Int32Array(buffer); + var doubleView = new Float64Array(buffer); + + doubleView[0] = f; + + this.writeInt(intView[0], (field || '') + ':double[low]'); + this.writeInt(intView[1], (field || '') + ':double[high]'); +}; + +TLSerialization.prototype.storeString = function (s, field) { + this.debug && console.log('>>>', s, (field || '') + ':string'); + + var sUTF8 = unescape(encodeURIComponent(s)); + + this.checkLength(sUTF8.length + 8); + + + var len = sUTF8.length; + if (len <= 253) { + this.byteView[this.offset++] = len; + } else { + this.byteView[this.offset++] = 254; + this.byteView[this.offset++] = len & 0xFF; + this.byteView[this.offset++] = (len & 0xFF00) >> 8; + this.byteView[this.offset++] = (len & 0xFF0000) >> 16; + } + for (var i = 0; i < len; i++) { + this.byteView[this.offset++] = sUTF8.charCodeAt(i); + } + + // Padding + while (this.offset % 4) { + this.byteView[this.offset++] = 0; + } +} + + +TLSerialization.prototype.storeBytes = function (bytes, field) { + this.debug && console.log('>>>', bytesToHex(bytes), (field || '') + ':bytes'); + + this.checkLength(bytes.length + 8); + + var len = bytes.length; + if (len <= 253) { + this.byteView[this.offset++] = len; + } else { + this.byteView[this.offset++] = 254; + this.byteView[this.offset++] = len & 0xFF; + this.byteView[this.offset++] = (len & 0xFF00) >> 8; + this.byteView[this.offset++] = (len & 0xFF0000) >> 16; + } + for (var i = 0; i < len; i++) { + this.byteView[this.offset++] = bytes[i]; + } + + // Padding + while (this.offset % 4) { + this.byteView[this.offset++] = 0; + } +} + +TLSerialization.prototype.storeIntBytes = function (bytes, bits, field) { + var len = bytes.length; + if ((bits % 32) || (len * 8) != bits) { + throw new Error('Invalid bits: ' + bits + ', ' + bytes.length); + } + + this.debug && console.log('>>>', bytesToHex(bytes), (field || '') + ':int' + bits); + this.checkLength(len); + + for (var i = 0; i < len; i++) { + this.byteView[this.offset++] = bytes[i]; + } +}; + +TLSerialization.prototype.storeRawBytes = function (bytes, field) { + var len = bytes.length; + + this.debug && console.log('>>>', bytesToHex(bytes), (field || '')); + this.checkLength(len); + + for (var i = 0; i < len; i++) { + this.byteView[this.offset++] = bytes[i]; + } +}; + + +TLSerialization.prototype.storeMethod = function (methodName, params) { + var schema = this.mtproto ? Config.Schema.MTProto : Config.Schema.API, + methodData = false, + i; + + for (i = 0; i < schema.methods.length; i++) { + if (schema.methods[i].method == methodName) { + methodData = schema.methods[i]; + break + } + } + if (!methodData) { + throw new Error('No method ' + methodName + ' found'); + } + + this.storeInt(intToUint(methodData.id), methodName + '[id]'); + + var self = this; + angular.forEach(methodData.params, function (param) { + self.storeObject(params[param.name], param.type, methodName + '[' + param.name + ']'); + }); + + return methodData.type; +}; + +TLSerialization.prototype.storeObject = function (obj, type, field) { + switch (type) { + case 'int': return this.storeInt(obj, field); + case 'long': return this.storeLong(obj, field); + case 'int128': return this.storeIntBytes(obj, 128, field); + case 'int256': return this.storeIntBytes(obj, 256, field); + case 'int512': return this.storeIntBytes(obj, 512, field); + case 'string': return this.storeString(obj, field); + case 'bytes': return this.storeBytes(obj, field); + case 'double': return this.storeDouble(obj, field); + case 'Bool': return this.storeBool(obj, field); + } + + if (angular.isArray(obj)) { + if (type.substr(0, 6) == 'Vector') { + this.writeInt(0x1cb5c415, field + '[id]'); + } + else if (type.substr(0, 6) != 'vector') { + throw new Error('Invalid vector type ' + type); + } + var itemType = type.substr(7, type.length - 8); // for "Vector" + this.writeInt(obj.length, field + '[count]'); + for (var i = 0; i < obj.length; i++) { + this.storeObject(obj[i], itemType, field + '[' + i + ']'); + } + return true; + } + else if (type.substr(0, 6).toLowerCase() == 'vector') { + throw new Error('Invalid vector object'); + } + + if (!angular.isObject(obj)) { + throw new Error('Invalid object for type ' + type); + } + + var schema = this.mtproto ? Config.Schema.MTProto : Config.Schema.API, + predicate = obj['_'], + isBare = false, + constructorData = false, + i; + + if (isBare = (type.charAt(0) == '%')) { + type = type.substr(1); + } + + for (i = 0; i < schema.constructors.length; i++) { + if (schema.constructors[i].predicate == predicate) { + constructorData = schema.constructors[i]; + break + } + } + if (!constructorData) { + throw new Error('No predicate ' + predicate + ' found'); + } + + if (predicate == type) { + isBare = true; + } + + if (!isBare) { + this.writeInt(intToUint(constructorData.id), field + '[' + predicate + '][id]'); + } + + var self = this; + angular.forEach(constructorData.params, function (param) { + self.storeObject(obj[param.name], param.type, field + '[' + predicate + '][' + param.name + ']'); + }); + + return constructorData.type; +}; + + + +function TLDeserialization (buffer, options) { + options = options || {}; + + this.offset = 0; // in bytes + this.override = options.override || {}; + + this.buffer = buffer; + this.intView = new Uint32Array(this.buffer); + this.byteView = new Uint8Array(this.buffer); + + // this.debug = options.debug !== undefined ? options.debug : Config.Modes.debug; + this.mtproto = options.mtproto || false; + return this; +} + +TLDeserialization.prototype.readInt = function (field) { + if (this.offset >= this.intView.length * 4) { + throw new Error('Nothing to fetch: ' + field); + } + + var i = this.intView[this.offset / 4]; + + this.debug && console.log('<<<', i.toString(16), i, field); + + this.offset += 4; + + return i; +}; + +TLDeserialization.prototype.fetchInt = function (field) { + return this.readInt((field || '') + ':int'); +} + +TLDeserialization.prototype.fetchDouble = function (field) { + var buffer = new ArrayBuffer(8); + var intView = new Int32Array(buffer); + var doubleView = new Float64Array(buffer); + + intView[0] = this.readInt((field || '') + ':double[low]'), + intView[1] = this.readInt((field || '') + ':double[high]'); + + return doubleView[0]; +}; + +TLDeserialization.prototype.fetchLong = function (field) { + var iLow = this.readInt((field || '') + ':long[low]'), + iHigh = this.readInt((field || '') + ':long[high]'); + + var longDec = bigint(iHigh).shiftLeft(32).add(bigint(iLow)).toString(); + + return longDec; +} + +TLDeserialization.prototype.fetchBool = function (field) { + var i = this.readInt((field || '') + ':bool'); + if (i == 0x997275b5) { + return true; + } else if (i == 0xbc799737) { + return false + } + + this.offset -= 4; + return this.fetchObject('Object', field); +} + +TLDeserialization.prototype.fetchString = function (field) { + var len = this.byteView[this.offset++]; + + if (len == 254) { + var len = this.byteView[this.offset++] | + (this.byteView[this.offset++] << 8) | + (this.byteView[this.offset++] << 16); + } + + var sUTF8 = ''; + for (var i = 0; i < len; i++) { + sUTF8 += String.fromCharCode(this.byteView[this.offset++]); + } + + // Padding + while (this.offset % 4) { + this.offset++; + } + + try { + var s = decodeURIComponent(escape(sUTF8)); + } catch (e) { + var s = sUTF8; + } + + this.debug && console.log('<<<', s, (field || '') + ':string'); + + return s; +} + + +TLDeserialization.prototype.fetchBytes = function (field) { + var len = this.byteView[this.offset++]; + + if (len == 254) { + var len = this.byteView[this.offset++] | + (this.byteView[this.offset++] << 8) | + (this.byteView[this.offset++] << 16); + } + + var bytes = []; + for (var i = 0; i < len; i++) { + bytes.push(this.byteView[this.offset++]); + } + + // Padding + while (this.offset % 4) { + this.offset++; + } + + this.debug && console.log('<<<', bytesToHex(bytes), (field || '') + ':bytes'); + + return bytes; +} + +TLDeserialization.prototype.fetchIntBytes = function (bits, field) { + if (bits % 32) { + throw new Error('Invalid bits: ' + bits); + } + + var len = bits / 8; + var bytes = []; + for (var i = 0; i < len; i++) { + bytes.push(this.byteView[this.offset++]); + } + + this.debug && console.log('<<<', bytesToHex(bytes), (field || '') + ':int' + bits); + + return bytes; +}; + + +TLDeserialization.prototype.fetchRawBytes = function (len, field) { + if (len === false) { + len = this.readInt((field || '') + '_length'); + } + + var bytes = []; + for (var i = 0; i < len; i++) { + bytes.push(this.byteView[this.offset++]); + } + + this.debug && console.log('<<<', bytesToHex(bytes), (field || '')); + + return bytes; +}; + +TLDeserialization.prototype.fetchObject = function (type, field) { + switch (type) { + case 'int': return this.fetchInt(field); + case 'long': return this.fetchLong(field); + case 'int128': return this.fetchIntBytes(128, field); + case 'int256': return this.fetchIntBytes(256, field); + case 'int512': return this.fetchIntBytes(512, field); + case 'string': return this.fetchString(field); + case 'bytes': return this.fetchBytes(field); + case 'double': return this.fetchDouble(field); + case 'Bool': return this.fetchBool(field); + } + + field = field || type || 'Object'; + + if (type.substr(0, 6) == 'Vector' || type.substr(0, 6) == 'vector') { + if (type.charAt(0) == 'V') { + var constructor = this.readInt(field + '[id]'); + if (constructor != 0x1cb5c415) { + throw new Error('Invalid vector constructor ' + constructor); + } + } + var len = this.readInt(field + '[count]'); + var result = []; + if (len > 0) { + var itemType = type.substr(7, type.length - 8); // for "Vector" + for (var i = 0; i < len; i++) { + result.push(this.fetchObject(itemType, field + '[' + i + ']')) + } + } + + return result; + } + + var schema = this.mtproto ? Config.Schema.MTProto : Config.Schema.API, + predicate = false, + constructorData = false; + + if (type.charAt(0) == '%') { + var checkType = type.substr(1); + for (i = 0; i < schema.constructors.length; i++) { + if (schema.constructors[i].type == checkType) { + constructorData = schema.constructors[i]; + break + } + } + if (!constructorData) { + throw new Error('Constructor not found for type: ' + type); + } + } + else if (type.charAt(0) >= 97 && type.charAt(0) <= 122) { + for (i = 0; i < schema.constructors.length; i++) { + if (schema.constructors[i].predicate == type) { + constructorData = schema.constructors[i]; + break + } + } + if (!constructorData) { + throw new Error('Constructor not found for predicate: ' + type); + } + } + else { + var constructor = this.readInt(field + '[id]'), + constructorCmp = uintToInt(constructor); + + if (constructorCmp == 0x3072cfa1) { // Gzip packed + var compressed = this.fetchBytes(field + '[packed_string]'), + uncompressed = gzipUncompress(compressed), + buffer = bytesToArrayBuffer(uncompressed), + newDeserializer = (new TLDeserialization(buffer)); + + return newDeserializer.fetchObject(type, field); + } + + for (i = 0; i < schema.constructors.length; i++) { + if (schema.constructors[i].id == constructorCmp) { + constructorData = schema.constructors[i]; + break; + } + } + + var fallback = false; + if (!constructorData && this.mtproto) { + var schemaFallback = Config.Schema.API; + for (i = 0; i < schemaFallback.constructors.length; i++) { + if (schemaFallback.constructors[i].id == constructorCmp) { + constructorData = schemaFallback.constructors[i]; + + delete this.mtproto; + fallback = true; + break; + } + } + } + if (!constructorData) { + throw new Error('Constructor not found: ' + constructor); + } + } + + predicate = constructorData.predicate; + + var result = {'_': predicate}, + overrideKey = (this.mtproto ? 'mt_' : '') + predicate, + self = this; + + + if (this.override[overrideKey]) { + this.override[overrideKey].apply(this, [result, field + '[' + predicate + ']']); + } else { + angular.forEach(constructorData.params, function (param) { + result[param.name] = self.fetchObject(param.type, field + '[' + predicate + '][' + param.name + ']'); + }); + } + + if (fallback) { + this.mtproto = true; + } + + return result; +}; + +TLDeserialization.prototype.getOffset = function () { + return this.offset; +}; + +TLDeserialization.prototype.fetchEnd = function () { + if (this.offset != this.byteView.length) { + throw new Error('Fetch end with non-empty buffer'); + } + return true; +}; diff --git a/app/js/util.js b/app/js/lib/utils.js similarity index 100% rename from app/js/util.js rename to app/js/lib/utils.js diff --git a/app/js/services.js b/app/js/services.js index 987d70c4..4baed141 100644 --- a/app/js/services.js +++ b/app/js/services.js @@ -11,125 +11,6 @@ angular.module('myApp.services', []) -.service('AppConfigManager', function ($q) { - var testPrefix = Config.Modes.test ? 't_' : ''; - var cache = {}; - var useCs = !!(window.chrome && chrome.storage && chrome.storage.local); - var useLs = !useCs && !!window.localStorage; - - function getValue() { - var keys = Array.prototype.slice.call(arguments), - result = [], - single = keys.length == 1, - allFound = true; - - for (var i = 0; i < keys.length; i++) { - keys[i] = testPrefix + keys[i]; - } - - angular.forEach(keys, function (key) { - if (cache[key] !== undefined) { - result.push(cache[key]); - } - else if (useLs) { - var value = localStorage.getItem(key); - value = (value === undefined || value === null) ? false : JSON.parse(value); - result.push(cache[key] = value); - } - else if (!useCs) { - result.push(cache[key] = false); - } - else { - allFound = false; - } - }); - - if (allFound) { - return $q.when(single ? result[0] : result); - } - - var deferred = $q.defer(); - - chrome.storage.local.get(keys, function (resultObj) { - result = []; - angular.forEach(keys, function (key) { - var value = resultObj[key]; - value = value === undefined || value === null ? false : JSON.parse(value); - result.push(cache[key] = value); - }); - - deferred.resolve(single ? result[0] : result); - }); - - return deferred.promise; - }; - - function setValue(obj) { - var keyValues = {}; - angular.forEach(obj, function (value, key) { - keyValues[testPrefix + key] = JSON.stringify(value); - cache[testPrefix + key] = value; - }); - - if (useLs) { - angular.forEach(keyValues, function (value, key) { - localStorage.setItem(key, value); - }); - return $q.when(); - } - - if (!useCs) { - return $q.when(); - } - - var deferred = $q.defer(); - - chrome.storage.local.set(keyValues, function () { - deferred.resolve(); - }); - - return deferred.promise; - }; - - function removeValue () { - var keys = Array.prototype.slice.call(arguments); - - for (var i = 0; i < keys.length; i++) { - keys[i] = testPrefix + keys[i]; - } - - angular.forEach(keys, function(key){ - delete cache[key]; - }); - - if (useLs) { - angular.forEach(keys, function(key){ - localStorage.removeItem(key); - }); - - return $q.when(); - } - - if (!useCs) { - return $q.when(); - } - - var deferred = $q.defer(); - - chrome.storage.local.remove(keys, function () { - deferred.resolve(); - }); - - return deferred.promise; - }; - - return { - get: getValue, - set: setValue, - remove: removeValue - }; -}) - .service('AppUsersManager', function ($rootScope, $modal, $modalStack, $filter, $q, MtpApiFileManager, MtpApiManager, RichTextProcessor, SearchIndexManager, ErrorService) { var users = {}, cachedPhotoLocations = {}, @@ -2176,7 +2057,7 @@ angular.module('myApp.services', []) } }) -.service('AppPhotosManager', function ($modal, $window, $timeout, $rootScope, MtpApiManager, MtpApiFileManager, AppUsersManager) { +.service('AppPhotosManager', function ($modal, $window, $timeout, $rootScope, MtpApiManager, MtpApiFileManager, AppUsersManager, FileManager) { var photos = {}; function savePhoto (apiPhoto) { @@ -2373,48 +2254,25 @@ angular.module('myApp.services', []) secret: fullPhotoSize.location.secret }; - if (window.chrome && chrome.fileSystem && chrome.fileSystem.chooseEntry) { - - chrome.fileSystem.chooseEntry({ - type: 'saveFile', - suggestedName: fileName, - accepts: [{ - mimeTypes: [mimeType], - extensions: [ext] - }] - }, function (writableFileEntry) { - var downloadPromise = MtpApiFileManager.downloadFile(fullPhotoSize.location.dc_id, inputFileLocation, fullPhotoSize.size, { - mime: mimeType, - toFileEntry: writableFileEntry - }); - downloadPromise.then(function (url) { - console.log('file save done'); - }, function (e) { - console.log('photo download failed', e); - }); - + FileManager.chooseSave(fileName, ext, mimeType).then(function (writableFileEntry) { + MtpApiFileManager.downloadFile( + fullPhotoSize.location.dc_id, inputFileLocation, fullPhotoSize.size, { + mime: mimeType, + toFileEntry: writableFileEntry + }).then(function (url) { + console.log('file save done'); + }, function (e) { + console.log('photo download failed', e); }); - } else { - var downloadPromise = MtpApiFileManager.downloadFile(fullPhotoSize.location.dc_id, inputFileLocation, fullPhotoSize.size, {mime: mimeType}); - - downloadPromise.then(function (url) { - - var a = $('Download') - .css({position: 'absolute', top: 1, left: 1}) - .attr('href', url) - .attr('target', '_blank') - .attr('download', fileName) - .appendTo('body'); - - a[0].dataset.downloadurl = [mimeType, fileName, url].join(':'); - a[0].click(); - $timeout(function () { - a.remove(); - }, 100); + }, function () { + MtpApiFileManager.downloadFile( + fullPhotoSize.location.dc_id, inputFileLocation, fullPhotoSize.size, {mime: mimeType} + ).then(function (url) { + FileManager.download(url, mimeType, fileName); }, function (e) { console.log('photo download failed', e); }); - } + }); }; $rootScope.openPhoto = openPhoto; @@ -2433,7 +2291,7 @@ angular.module('myApp.services', []) }) -.service('AppVideoManager', function ($rootScope, $modal, $window, $timeout, MtpApiFileManager, AppUsersManager) { +.service('AppVideoManager', function ($rootScope, $modal, $window, $timeout, MtpApiFileManager, AppUsersManager, FileManager) { var videos = {}; var videosForHistory = {}; @@ -2551,31 +2409,21 @@ angular.module('myApp.services', []) mimeType = 'video/mpeg4', fileName = 'video' + videoID + '.' + ext; - if (window.chrome && chrome.fileSystem && chrome.fileSystem.chooseEntry) { - - chrome.fileSystem.chooseEntry({ - type: 'saveFile', - suggestedName: fileName, - accepts: [{ - mimeTypes: [mimeType], - extensions: [ext] - }] - }, function (writableFileEntry) { - var downloadPromise = MtpApiFileManager.downloadFile(video.dc_id, inputFileLocation, video.size, { - mime: mimeType, - toFileEntry: writableFileEntry - }); - downloadPromise.then(function (url) { - delete historyVideo.progress; - console.log('file save done'); - }, function (e) { - console.log('video download failed', e); - historyVideo.progress.enabled = false; - }, updateDownloadProgress); - - historyVideo.progress.cancel = downloadPromise.cancel; + FileManager.chooseSave(fileName, ext, mimeType).then(function (writableFileEntry) { + var downloadPromise = MtpApiFileManager.downloadFile(video.dc_id, inputFileLocation, video.size, { + mime: mimeType, + toFileEntry: writableFileEntry }); - } else { + downloadPromise.then(function (url) { + delete historyVideo.progress; + console.log('file save done'); + }, function (e) { + console.log('video download failed', e); + historyVideo.progress.enabled = false; + }, updateDownloadProgress); + + historyVideo.progress.cancel = downloadPromise.cancel; + }, function () { var downloadPromise = MtpApiFileManager.downloadFile(video.dc_id, inputFileLocation, video.size, {mime: mimeType}); downloadPromise.then(function (url) { @@ -2586,25 +2434,14 @@ angular.module('myApp.services', []) return } - var a = $('Download') - .css({position: 'absolute', top: 1, left: 1}) - .attr('href', url) - .attr('target', '_blank') - .attr('download', fileName) - .appendTo('body'); - - a[0].dataset.downloadurl = [mimeType, fileName, url].join(':'); - a[0].click(); - $timeout(function () { - a.remove(); - }, 100); + FileManager.download(url, mimeType, fileName); }, function (e) { console.log('video download failed', e); historyVideo.progress.enabled = false; }, updateDownloadProgress); historyVideo.progress.cancel = downloadPromise.cancel; - } + }); }; $rootScope.openVideo = openVideo; @@ -2618,7 +2455,7 @@ angular.module('myApp.services', []) } }) -.service('AppDocsManager', function ($rootScope, $modal, $window, $timeout, MtpApiFileManager) { +.service('AppDocsManager', function ($rootScope, $modal, $window, $timeout, MtpApiFileManager, FileManager) { var docs = {}; var docsForHistory = {}; @@ -2693,34 +2530,23 @@ angular.module('myApp.services', []) $rootScope.$broadcast('history_update'); } + var ext = (doc.file_name.split('.', 2) || [])[1] || ''; + FileManager.chooseSave(doc.file_name, ext, doc.mime_type).then(function (writableFileEntry) { + var downloadPromise = MtpApiFileManager.downloadFile(doc.dc_id, inputFileLocation, doc.size, { + mime: doc.mime_type, + toFileEntry: writableFileEntry + }); - if (window.chrome && chrome.fileSystem && chrome.fileSystem.chooseEntry) { - var ext = (doc.file_name.split('.', 2) || [])[1] || ''; - - chrome.fileSystem.chooseEntry({ - type: 'saveFile', - suggestedName: doc.file_name, - accepts: [{ - mimeTypes: [doc.mime_type], - extensions: [ext] - }] - }, function (writableFileEntry) { - var downloadPromise = MtpApiFileManager.downloadFile(doc.dc_id, inputFileLocation, doc.size, { - mime: doc.mime_type, - toFileEntry: writableFileEntry - }); - - downloadPromise.then(function (url) { - delete historyDoc.progress; - console.log('file save done'); - }, function (e) { - console.log('document download failed', e); - historyDoc.progress.enabled = false; - }, updateDownloadProgress); + downloadPromise.then(function (url) { + delete historyDoc.progress; + console.log('file save done'); + }, function (e) { + console.log('document download failed', e); + historyDoc.progress.enabled = false; + }, updateDownloadProgress); - historyDoc.progress.cancel = downloadPromise.cancel; - }); - } else { + historyDoc.progress.cancel = downloadPromise.cancel; + }, function () { var downloadPromise = MtpApiFileManager.downloadFile(doc.dc_id, inputFileLocation, doc.size, {mime: doc.mime_type}); downloadPromise.then(function (url) { @@ -2734,18 +2560,7 @@ angular.module('myApp.services', []) break; default: - var a = $('Download') - .css({position: 'absolute', top: 1, left: 1}) - .attr('href', url) - .attr('target', '_blank') - .attr('download', doc.file_name) - .appendTo('body'); - - a[0].dataset.downloadurl = [doc.mime_type, doc.file_name, url].join(':'); - a[0].click(); - $timeout(function () { - a.remove(); - }, 100); + FileManager.download(url, doc.mime_type, doc.file_name); } }, function (e) { console.log('document download failed', e); @@ -2753,7 +2568,7 @@ angular.module('myApp.services', []) }, updateDownloadProgress); historyDoc.progress.cancel = downloadPromise.cancel; - } + }); } $rootScope.downloadDoc = downloadDoc; @@ -3354,7 +3169,7 @@ angular.module('myApp.services', []) }) -.service('NotificationsManager', function ($rootScope, $window, $timeout, $interval, $q, MtpApiManager, AppPeersManager, IdleManager, AppConfigManager) { +.service('NotificationsManager', function ($rootScope, $window, $timeout, $interval, $q, MtpApiManager, AppPeersManager, IdleManager, Storage) { var notificationsUiSupport = 'Notification' in window; var notificationsShown = {}; @@ -3493,13 +3308,13 @@ angular.module('myApp.services', []) return false; } - AppConfigManager.get('notify_nosound', 'notify_volume').then(function (settings) { + Storage.get('notify_nosound', 'notify_volume').then(function (settings) { if (!settings[0] && settings[1] === false || settings[1] > 0) { playSound(settings[1] || 0.5); } }) - AppConfigManager.get('notify_nodesktop').then(function (noShow) { + Storage.get('notify_nodesktop').then(function (noShow) { if (noShow) { return; } @@ -3688,7 +3503,7 @@ angular.module('myApp.services', []) }) -.service('ChangelogNotifyService', function (AppConfigManager, $rootScope, $http, $modal) { +.service('ChangelogNotifyService', function (Storage, $rootScope, $http, $modal) { function versionCompare (ver1, ver2) { if (typeof ver1 !== 'string') { @@ -3718,12 +3533,12 @@ angular.module('myApp.services', []) } function checkUpdate () { - AppConfigManager.get('last_version').then(function (lastVersion) { + Storage.get('last_version').then(function (lastVersion) { if (lastVersion != Config.App.version) { if (lastVersion) { showChangelog(lastVersion); } - AppConfigManager.set({last_version: Config.App.version}); + Storage.set({last_version: Config.App.version}); } }) } diff --git a/app/webogram.appcache b/app/webogram.appcache index dd794be2..70b57f2a 100644 --- a/app/webogram.appcache +++ b/app/webogram.appcache @@ -1,6 +1,6 @@ CACHE MANIFEST -# 12 +# 15 NETWORK: *