/*! * Webogram v0.4.9 - 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 () { this.setPrefix = function (newPrefix) { ConfigStorage.prefix(newPrefix); }; this.$get = ['$q', function ($q) { var methods = {}; angular.forEach(['get', 'set', 'remove'], function (methodName) { methods[methodName] = function () { var deferred = $q.defer(), args = Array.prototype.slice.call(arguments); args.push(function (result) { deferred.resolve(result); }); ConfigStorage[methodName].apply(ConfigStorage, args); return deferred.promise; }; }); return methods; }]; }) .service('qSync', function () { return { when: function (result) { return {then: function (cb) { return cb(result); }}; }, reject: function (result) { return {then: function (cb, badcb) { if (badcb) { return badcb(result); } }}; } } }) .service('FileManager', function ($window, $q, $timeout, qSync) { $window.URL = $window.URL || $window.webkitURL; $window.BlobBuilder = $window.BlobBuilder || $window.WebKitBlobBuilder || $window.MozBlobBuilder; var buggyUnknownBlob = navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1; var blobSupported = true; try { blobConstruct([], ''); } catch (e) { blobSupported = false; } function isBlobAvailable () { return blobSupported; } function fileCopyTo (fromFileEntry, toFileEntry) { return getFileWriter(toFileEntry).then(function (fileWriter) { return fileWriteData(fileWriter, fromFileEntry).then(function () { return fileWriter; }, function (error) { return $q.reject(error); fileWriter.truncate(0); }); }); } function fileWriteData(fileWriter, bytes) { var deferred = $q.defer(); fileWriter.onwriteend = function(e) { deferred.resolve(); }; fileWriter.onerror = function (e) { deferred.reject(e); }; if (bytes.file) { bytes.file(function (file) { fileWriter.write(file); }, function (error) { deferred.reject(error); }) } else if (bytes instanceof Blob) { // is file bytes fileWriter.write(bytes); } else { try { var blob = blobConstruct([bytesToArrayBuffer(bytes)]); fileWriter.write(blob); } catch (e) { deferred.reject(e); } } return deferred.promise; } function chooseSaveFile (fileName, ext, mimeType) { if (!$window.chrome || !chrome.fileSystem || !chrome.fileSystem.chooseEntry) { return qSync.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 getFileWriter (fileEntry) { var deferred = $q.defer(); fileEntry.createWriter(function (fileWriter) { deferred.resolve(fileWriter); }, function (error) { deferred.reject(error); }); return deferred.promise; } function getFakeFileWriter (mimeType, saveFileCallback) { var blobParts = [], fakeFileWriter = { write: function (blob) { if (!blobSupported) { if (fakeFileWriter.onerror) { fakeFileWriter.onerror(new Error('Blob not supported by browser')); } return false; } blobParts.push(blob); setZeroTimeout(function () { if (fakeFileWriter.onwriteend) { fakeFileWriter.onwriteend(); } }); }, truncate: function () { blobParts = []; }, finalize: function () { var blob = blobConstruct(blobParts, 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 getByteArray(fileData) { if (fileData instanceof Blob) { var deferred = $q.defer(); try { var reader = new FileReader(); reader.onloadend = function (e) { deferred.resolve(new Uint8Array(e.target.result)); }; reader.onerror = function (e) { deferred.reject(e); }; reader.readAsArrayBuffer(fileData); return deferred.promise; } catch (e) { return $q.reject(e); } } else if (fileData.file) { var deferred = $q.defer(); fileData.file(function (blob) { getByteArray(blob).then(function (result) { deferred.resolve(result); }, function (error) { deferred.reject(error); }) }, function (error) { deferred.reject(error); }); return deferred.promise; } return $q.when(fileData); } function getDataUrl(blob) { var deferred; try { var reader = new FileReader(); reader.onloadend = function() { deferred.resolve(reader.result); } reader.readAsDataURL(blob); } catch (e) { return $q.reject(e); } deferred = $q.defer(); return deferred.promise; } function getFileCorrectUrl(blob, mimeType) { if (buggyUnknownBlob && blob instanceof Blob) { var mimeType = blob.type || blob.mimeType || mimeType || ''; if (!mimeType.match(/image\/(jpeg|gif|png|bmp)|video\/quicktime/)) { return getDataUrl(blob); } } return qSync.when(getUrl(blob, mimeType)); } function downloadFile (blob, mimeType, fileName) { if (window.navigator && navigator.msSaveBlob !== undefined) { window.navigator.msSaveBlob(blob, fileName); return false; } if (window.navigator && navigator.getDeviceStorage) { var storageName = 'sdcard'; var subdir = 'telegram/'; switch (mimeType.split('/')[0]) { case 'video': storageName = 'videos'; break; case 'audio': storageName = 'music'; break; case 'image': storageName = 'pictures'; break; } var deviceStorage = navigator.getDeviceStorage(storageName); var request = deviceStorage.addNamed(blob, subdir + fileName); request.onsuccess = function () { console.log('Device storage save result', this.result); }; request.onerror = function () { }; return; } var popup = false; if (window.safari) { popup = window.open(); } getFileCorrectUrl(blob, mimeType).then(function (url) { if (popup) { try { popup.location.href = url; return; } catch (e) {} } var anchor = document.createElementNS('http://www.w3.org/1999/xhtml', 'a'); anchor.href = url; anchor.target = '_blank'; anchor.download = fileName; if (anchor.dataset) { anchor.dataset.downloadurl = ["video/quicktime", fileName, url].join(':'); } $(anchor).css({position: 'absolute', top: 1, left: 1}).appendTo('body'); try { var clickEvent = document.createEvent('MouseEvents'); clickEvent.initMouseEvent( 'click', true, false, window, 0, 0, 0, 0, 0 , false, false, false, false, 0, null ); anchor.dispatchEvent(clickEvent); } catch (e) { console.error('Download click error', e); try { anchor[0].click(); } catch (e) { window.open(url, '_blank'); } } $timeout(function () { $(anchor).remove(); }, 100); }); } return { isAvailable: isBlobAvailable, copy: fileCopyTo, write: fileWriteData, getFileWriter: getFileWriter, getFakeFileWriter: getFakeFileWriter, chooseSave: chooseSaveFile, getUrl: getUrl, getDataUrl: getDataUrl, getByteArray: getByteArray, getFileCorrectUrl: getFileCorrectUrl, 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'; var dbStoreName = 'files'; var dbVersion = 2; var openDbPromise; var storageIsAvailable = $window.indexedDB !== undefined && $window.IDBTransaction !== undefined; // IndexedDB is REALLY slow without blob support in Safari 8, no point in it if (storageIsAvailable && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1 && navigator.userAgent.match(/Version\/[678]/) ) { storageIsAvailable = false; } var storeBlobsAvailable = storageIsAvailable || false; function isAvailable () { return storageIsAvailable; } function openDatabase() { if (openDbPromise) { return openDbPromise; } try { var request = indexedDB.open(dbName, dbVersion), deferred = $q.defer(), createObjectStore = function (db) { db.createObjectStore(dbStoreName); }; if (!request) { throw new Exception(); } } catch (error) { storageIsAvailable = false; return $q.reject(error); } request.onsuccess = function (event) { db = request.result; db.onerror = function (error) { storageIsAvailable = false; console.error('Error creating/accessing IndexedDB database', error); deferred.reject(error); }; deferred.resolve(db); }; request.onerror = function (event) { storageIsAvailable = false; console.error('Error creating/accessing IndexedDB database', event); deferred.reject(event); } request.onupgradeneeded = function (event) { console.warn('performing idb upgrade from', event.oldVersion, 'to', event.newVersion); var db = event.target.result; if (event.oldVersion == 1) { db.deleteObjectStore(dbStoreName); } createObjectStore(db); }; return openDbPromise = deferred.promise; }; function saveFile (fileName, blob) { return openDatabase().then(function (db) { if (!storeBlobsAvailable) { return saveFileBase64(db, fileName, blob); } if (!(blob instanceof Blob)) { blob = blobConstruct([blob]); } try { var objectStore = db.transaction([dbStoreName], IDBTransaction.READ_WRITE || 'readwrite').objectStore(dbStoreName), request = objectStore.put(blob, fileName); } catch (error) { if (storeBlobsAvailable) { storeBlobsAvailable = false; return saveFileBase64(db, fileName, blob); } storageIsAvailable = false; return $q.reject(error); } var deferred = $q.defer(); request.onsuccess = function (event) { deferred.resolve(blob); }; request.onerror = function (error) { deferred.reject(error); }; return deferred.promise; }); }; function saveFileBase64(db, fileName, blob) { if (getBlobSize(blob) > 10 * 1024 * 1024) { return $q.reject(); } try { var reader = new FileReader(); reader.readAsDataURL(blob); } catch (e) { storageIsAvailable = false; return $q.reject(); } var deferred = $q.defer(); reader.onloadend = function() { try { var objectStore = db.transaction([dbStoreName], IDBTransaction.READ_WRITE || 'readwrite').objectStore(dbStoreName), request = objectStore.put(reader.result, fileName); } catch (error) { storageIsAvailable = false; deferred.reject(error); return; }; request.onsuccess = function (event) { deferred.resolve(blob); }; request.onerror = function (error) { deferred.reject(error); }; } return deferred.promise; } function getBlobSize (blob) { return blob.size || blob.byteLength || blob.length; } 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) { var result = event.target.result; if (result === undefined) { deferred.reject(); } else if (typeof result === 'string' && result.substr(0, 5) === 'data:') { deferred.resolve(dataUrlToBlob(result)); } else { deferred.resolve(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); } openDatabase(); 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, 500 * 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) { FileManager.getFileWriter(fileEntry).then(function (fileWriter) { fileWriter.finalize = function () { return fileEntry; } deferred.resolve(fileWriter); }, function (error) { storageIsAvailable = false; deferred.reject(error); }); }, function (error) { storageIsAvailable = false; deferred.reject(error); }); return deferred.promise; }) } requestFS(); 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 }; }) .service('WebpManager', function (qSync, $q) { var nativeWebpSupport = false; var image = new Image(); image.onload = function () { nativeWebpSupport = this.width === 2 && this.height === 1; }; image.onerror = function () { nativeWebpSupport = false; }; image.src = 'data:image/webp;base64,UklGRjIAAABXRUJQVlA4ICYAAACyAgCdASoCAAEALmk0mk0iIiIiIgBoSygABc6zbAAA/v56QAAAAA=='; var canvas, context; function getCanvasFromWebp(data) { var start = tsNow(); var decoder = new WebPDecoder(); var config = decoder.WebPDecoderConfig; var buffer = config.j || config.output; var bitstream = config.input; if (!decoder.WebPInitDecoderConfig(config)) { console.error('[webpjs] Library version mismatch!'); return false; } // console.log('[webpjs] status code', decoder.VP8StatusCode); var StatusCode = decoder.VP8StatusCode; status = decoder.WebPGetFeatures(data, data.length, bitstream); if (status != (StatusCode.VP8_STATUS_OK || 0)) { console.error('[webpjs] status error', status, StatusCode); } var mode = decoder.WEBP_CSP_MODE; buffer.colorspace = mode.MODE_RGBA; buffer.J = 4; try { status = decoder.WebPDecode(data, data.length, config); } catch (e) { status = e; } ok = (status == 0); if (!ok) { console.error('[webpjs] decoding failed', status, StatusCode); return false; } // console.log('[webpjs] decoded: ', buffer.width, buffer.height, bitstream.has_alpha, 'Now saving...'); var bitmap = buffer.c.RGBA.ma; // console.log('[webpjs] done in ', tsNow() - start); if (!bitmap) { return false; } var biHeight = buffer.height; var biWidth = buffer.width; if (!canvas || !context) { canvas = document.createElement('canvas'); context = canvas.getContext('2d'); } else { context.clearRect(0, 0, canvas.width, canvas.height); } canvas.height = biHeight; canvas.width = biWidth; var output = context.createImageData(canvas.width, canvas.height); var outputData = output.data; for (var h = 0; h < biHeight; h++) { for (var w = 0; w < biWidth; w++) { outputData[0+w*4+(biWidth*4)*h] = bitmap[1+w*4+(biWidth*4)*h]; outputData[1+w*4+(biWidth*4)*h] = bitmap[2+w*4+(biWidth*4)*h]; outputData[2+w*4+(biWidth*4)*h] = bitmap[3+w*4+(biWidth*4)*h]; outputData[3+w*4+(biWidth*4)*h] = bitmap[0+w*4+(biWidth*4)*h]; }; } context.putImageData(output, 0, 0); return true; } function getPngBlobFromWebp (data) { if (!getCanvasFromWebp(data)) { return $q.reject({type: 'WEBP_PROCESS_FAILED'}); } if (canvas.toBlob === undefined) { return qSync.when(dataUrlToBlob(canvas.toDataURL('image/png'))); } var deferred = $q.defer(); canvas.toBlob(function (blob) { deferred.resolve(blob); }, 'image/png'); return deferred.promise; } return { isWebpSupported: function () { return nativeWebpSupport; }, getPngBlobFromWebp: getPngBlobFromWebp } }) .service('CryptoWorker', function ($timeout, $q) { var webWorker = false, naClEmbed = false, taskID = 0, awaiting = {}, webCrypto = Config.Modes.webcrypto && window.crypto && (window.crypto.subtle || window.crypto.webkitSubtle)/* || window.msCrypto && window.msCrypto.subtle*/, useSha1Crypto = webCrypto && webCrypto.digest !== undefined, useSha256Crypto = webCrypto && webCrypto.digest !== undefined, finalizeTask = function (taskID, result) { var deferred = awaiting[taskID]; if (deferred !== undefined) { // console.log(dT(), 'CW done'); deferred.resolve(result); delete awaiting[taskID]; } }; if (Config.Modes.nacl && navigator.mimeTypes && navigator.mimeTypes['application/x-pnacl'] !== undefined) { var listener = $('
').appendTo($('body'))[0]; listener.addEventListener('load', function (e) { naClEmbed = listener.firstChild; console.log(dT(), 'NaCl ready'); }, true); listener.addEventListener('message', function (e) { finalizeTask(e.data.taskID, e.data.result); }, true); listener.addEventListener('error', function (e) { console.error('NaCl error', e); }, true); } if (window.Worker) { var tmpWorker = new Worker('js/lib/crypto_worker.js'); tmpWorker.onmessage = function (e) { if (!webWorker) { webWorker = tmpWorker; } else { finalizeTask(e.data.taskID, e.data.result); } }; tmpWorker.onerror = function(error) { console.error('CW error', error, error.stack); webWorker = false; }; } function performTaskWorker (task, params, embed) { // console.log(dT(), 'CW start', task); var deferred = $q.defer(); awaiting[taskID] = deferred; params.task = task; params.taskID = taskID; (embed || webWorker).postMessage(params); taskID++; return deferred.promise; } return { sha1Hash: function (bytes) { if (useSha1Crypto) { // We don't use buffer since typedArray.subarray(...).buffer gives the whole buffer and not sliced one. webCrypto.digest supports typed array var deferred = $q.defer(), bytesTyped = Array.isArray(bytes) ? convertToUint8Array(bytes) : bytes; // console.log(dT(), 'Native sha1 start'); webCrypto.digest({name: 'SHA-1'}, bytesTyped).then(function (digest) { // console.log(dT(), 'Native sha1 done'); deferred.resolve(digest); }, function (e) { console.error('Crypto digest error', e); useSha1Crypto = false; deferred.resolve(sha1HashSync(bytes)); }); return deferred.promise; } return $timeout(function () { return sha1HashSync(bytes); }); }, sha256Hash: function (bytes) { if (useSha256Crypto) { var deferred = $q.defer(), bytesTyped = Array.isArray(bytes) ? convertToUint8Array(bytes) : bytes; // console.log(dT(), 'Native sha1 start'); webCrypto.digest({name: 'SHA-256'}, bytesTyped).then(function (digest) { // console.log(dT(), 'Native sha1 done'); deferred.resolve(digest); }, function (e) { console.error('Crypto digest error', e); useSha256Crypto = false; deferred.resolve(sha256HashSync(bytes)); }); return deferred.promise; } return $timeout(function () { return sha256HashSync(bytes); }); }, aesEncrypt: function (bytes, keyBytes, ivBytes) { if (naClEmbed) { return performTaskWorker('aes-encrypt', { bytes: addPadding(convertToArrayBuffer(bytes)), keyBytes: convertToArrayBuffer(keyBytes), ivBytes: convertToArrayBuffer(ivBytes) }, naClEmbed); } return $timeout(function () { return convertToArrayBuffer(aesEncryptSync(bytes, keyBytes, ivBytes)); }); }, aesDecrypt: function (encryptedBytes, keyBytes, ivBytes) { if (naClEmbed) { return performTaskWorker('aes-decrypt', { encryptedBytes: addPadding(convertToArrayBuffer(encryptedBytes)), keyBytes: convertToArrayBuffer(keyBytes), ivBytes: convertToArrayBuffer(ivBytes) }, naClEmbed); } return $timeout(function () { return convertToArrayBuffer(aesDecryptSync(encryptedBytes, keyBytes, ivBytes)); }); }, factorize: function (bytes) { bytes = convertToByteArray(bytes); if (naClEmbed && bytes.length <= 8) { return performTaskWorker('factorize', {bytes: bytes}, naClEmbed); } if (webWorker) { return performTaskWorker('factorize', {bytes: bytes}); } return $timeout(function () { return pqPrimeFactorization(bytes); }); }, modPow: function (x, y, m) { if (webWorker) { return performTaskWorker('mod-pow', { x: x, y: y, m: m }); } return $timeout(function () { return bytesModPow(x, y, m); }); }, }; }) .service('ExternalResourcesManager', function ($q, $http) { var urlPromises = {}; function downloadImage (url) { if (urlPromises[url] !== undefined) { return urlPromises[url]; } return urlPromises[url] = $http.get(url, {responseType: 'blob', transformRequest: null}) .then(function (response) { window.URL = window.URL || window.webkitURL; return window.URL.createObjectURL(response.data); }); } return { downloadImage: downloadImage } }) .service('IdleManager', function ($rootScope, $window, $timeout) { $rootScope.idle = {isIDLE: false}; var toPromise, started = false; var hidden = 'hidden'; var visibilityChange = 'visibilitychange'; if (typeof document.hidden !== 'undefined') { // default } else if (typeof document.mozHidden !== 'undefined') { hidden = 'mozHidden'; visibilityChange = 'mozvisibilitychange'; } else if (typeof document.msHidden !== 'undefined') { hidden = 'msHidden'; visibilityChange = 'msvisibilitychange'; } else if (typeof document.webkitHidden !== 'undefined') { hidden = 'webkitHidden'; visibilityChange = 'webkitvisibilitychange'; } return { start: start }; function start () { if (!started) { started = true; $($window).on(visibilityChange + ' blur focus keydown mousedown touchstart', onEvent); setTimeout(function () { onEvent({type: 'blur'}); }, 0); } } function onEvent (e) { // console.log('event', e.type); if (e.type == 'mousemove') { var e = e.originalEvent || e; if (e && e.movementX === 0 && e.movementY === 0) { return; } $($window).off('mousemove', onEvent); } var isIDLE = e.type == 'blur' || e.type == 'timeout' ? true : false; if (hidden && document[hidden]) { isIDLE = true; } $timeout.cancel(toPromise); if (!isIDLE) { // console.log('update timeout'); toPromise = $timeout(function () { onEvent({type: 'timeout'}); }, 30000); } if (e.type == 'focus' && !$rootScope.idle.afterFocus) { $rootScope.idle.afterFocus = true; setTimeout(function () { delete $rootScope.idle.afterFocus; }, 10); } if ($rootScope.idle.isIDLE == isIDLE) { return; } // console.log('IDLE changed', isIDLE); $rootScope.$apply(function () { $rootScope.idle.isIDLE = isIDLE; }); if (isIDLE && e.type == 'timeout') { $($window).on('mousemove', onEvent); } } }) .service('AppRuntimeManager', function ($window) { return { reload: function () { try { location.reload(); } catch (e) {}; if ($window.chrome && chrome.runtime && chrome.runtime.reload) { chrome.runtime.reload(); }; }, close: function () { try { $window.close(); } catch (e) {} }, focus: function () { if (window.navigator.mozApps && document.hidden) { // Get app instance and launch it to bring app to foreground window.navigator.mozApps.getSelf().onsuccess = function() { this.result.launch(); }; } else { if (window.chrome && chrome.app && chrome.app.window) { chrome.app.window.current().focus(); } window.focus(); } } } }) .service('RichTextProcessor', function ($sce, $sanitize) { var emojiMap = {}, emojiData = Config.Emoji, emojiIconSize = 18, emojiSupported = navigator.userAgent.search(/OS X|iPhone|iPad|iOS|Android/i) != -1, emojiCode; var emojiRegExp = "\\u0023\\u20E3|\\u00a9|\\u00ae|\\u203c|\\u2049|\\u2139|[\\u2194-\\u2199]|\\u21a9|\\u21aa|\\u231a|\\u231b|\\u23e9|[\\u23ea-\\u23ec]|\\u23f0|\\u24c2|\\u25aa|\\u25ab|\\u25b6|\\u2611|\\u2614|\\u26fd|\\u2705|\\u2709|[\\u2795-\\u2797]|\\u27a1|\\u27b0|\\u27bf|\\u2934|\\u2935|[\\u2b05-\\u2b07]|\\u2b1b|\\u2b1c|\\u2b50|\\u2b55|\\u3030|\\u303d|\\u3297|\\u3299|[\\uE000-\\uF8FF\\u270A-\\u2764\\u2122\\u25C0\\u25FB-\\u25FE\\u2615\\u263a\\u2648-\\u2653\\u2660-\\u2668\\u267B\\u267F\\u2693\\u261d\\u26A0-\\u26FA\\u2708\\u2702\\u2601\\u260E]|[\\u2600\\u26C4\\u26BE\\u23F3\\u2764]|\\uD83D[\\uDC00-\\uDFFF]|\\uD83C[\\uDDE8-\\uDDFA\uDDEC]\\uD83C[\\uDDEA-\\uDDFA\uDDE7]|[0-9]\\u20e3|\\uD83C[\\uDC00-\\uDFFF]"; for (emojiCode in emojiData) { emojiMap[emojiData[emojiCode][0]] = emojiCode; } var alphaCharsRegExp = "a-z" + "\\u00c0-\\u00d6\\u00d8-\\u00f6\\u00f8-\\u00ff" + // Latin-1 "\\u0100-\\u024f" + // Latin Extended A and B "\\u0253\\u0254\\u0256\\u0257\\u0259\\u025b\\u0263\\u0268\\u026f\\u0272\\u0289\\u028b" + // IPA Extensions "\\u02bb" + // Hawaiian "\\u0300-\\u036f" + // Combining diacritics "\\u1e00-\\u1eff" + // Latin Extended Additional (mostly for Vietnamese) "\\u0400-\\u04ff\\u0500-\\u0527" + // Cyrillic "\\u2de0-\\u2dff\\ua640-\\ua69f" + // Cyrillic Extended A/B "\\u0591-\\u05bf\\u05c1-\\u05c2\\u05c4-\\u05c5\\u05c7" + "\\u05d0-\\u05ea\\u05f0-\\u05f4" + // Hebrew "\\ufb1d-\\ufb28\\ufb2a-\\ufb36\\ufb38-\\ufb3c\\ufb3e\\ufb40-\\ufb41" + "\\ufb43-\\ufb44\\ufb46-\\ufb4f" + // Hebrew Pres. Forms "\\u0610-\\u061a\\u0620-\\u065f\\u066e-\\u06d3\\u06d5-\\u06dc" + "\\u06de-\\u06e8\\u06ea-\\u06ef\\u06fa-\\u06fc\\u06ff" + // Arabic "\\u0750-\\u077f\\u08a0\\u08a2-\\u08ac\\u08e4-\\u08fe" + // Arabic Supplement and Extended A "\\ufb50-\\ufbb1\\ufbd3-\\ufd3d\\ufd50-\\ufd8f\\ufd92-\\ufdc7\\ufdf0-\\ufdfb" + // Pres. Forms A "\\ufe70-\\ufe74\\ufe76-\\ufefc" + // Pres. Forms B "\\u200c" + // Zero-Width Non-Joiner "\\u0e01-\\u0e3a\\u0e40-\\u0e4e" + // Thai "\\u1100-\\u11ff\\u3130-\\u3185\\uA960-\\uA97F\\uAC00-\\uD7AF\\uD7B0-\\uD7FF" + // Hangul (Korean) "\\u3003\\u3005\\u303b" + // Kanji/Han iteration marks "\\uff21-\\uff3a\\uff41-\\uff5a" + // full width Alphabet "\\uff66-\\uff9f" + // half width Katakana "\\uffa1-\\uffdc"; // half width Hangul (Korean) var alphaNumericRegExp = "0-9\_" + alphaCharsRegExp; var domainAddChars = "\u00b7"; // Based on Regular Expression for URL validation by Diego Perini var urlRegExp = "((?:https?|ftp)://|mailto:)?" + // user:pass authentication "(?:\\S{1,64}(?::\\S{0,64})?@)?" + "(?:" + // sindresorhus/ip-regexp "(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){3}" + "|" + // host name "[" + alphaCharsRegExp + "0-9][" + alphaCharsRegExp + domainAddChars + "0-9\-]{0,64}" + // domain name "(?:\\.[" + alphaCharsRegExp + "0-9][" + alphaCharsRegExp + domainAddChars + "0-9\-]{0,64}){0,10}" + // TLD identifier "(?:\\.(xn--[0-9a-z]{2,16}|[" + alphaCharsRegExp + "]{2,24}))" + ")" + // port number "(?::\\d{2,5})?" + // resource path "(?:/(?:\\S{0,255}[^\\s.;,(\\[\\]{}<>\"'])?)?"; var usernameRegExp = "[a-zA-Z\\d_]{5,32}"; var botCommandRegExp = "\\/([a-zA-Z\\d_]{1,32})(?:@(" + usernameRegExp + "))?(\\b|$)" var fullRegExp = new RegExp('(^| )(@)(' + usernameRegExp + ')|(' + urlRegExp + ')|(\\n)|(' + emojiRegExp + ')|(^|\\s)(#[' + alphaNumericRegExp + ']{2,64})|(^|\\s)' + botCommandRegExp, 'i'); var emailRegExp = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; var youtubeRegExp = /^(?:https?:\/\/)?(?:www\.)?youtu(?:|\.be|be\.com|\.b)(?:\/v\/|\/watch\\?v=|e\/|(?:\/\??#)?\/watch(?:.+)v=)(.{11})(?:\&[^\s]*)?/; var vimeoRegExp = /^(?:https?:\/\/)?(?:www\.)?vimeo\.com\/(\d+)/; var instagramRegExp = /^https?:\/\/(?:instagr\.am\/p\/|instagram\.com\/p\/)([a-zA-Z0-9\-\_]+)/i; var vineRegExp = /^https?:\/\/vine\.co\/v\/([a-zA-Z0-9\-\_]+)/i; var twitterRegExp = /^https?:\/\/twitter\.com\/.+?\/status\/\d+/i; var facebookRegExp = /^https?:\/\/(?:www\.|m\.)?facebook\.com\/(?:.+?\/posts\/\d+|(?:story\.php|permalink\.php)\?story_fbid=(\d+)(?:&substory_index=\d+)?&id=(\d+))/i; var gplusRegExp = /^https?:\/\/plus\.google\.com\/\d+\/posts\/[a-zA-Z0-9\-\_]+/i; var soundcloudRegExp = /^https?:\/\/(?:soundcloud\.com|snd\.sc)\/([a-zA-Z0-9%\-\_]+)\/([a-zA-Z0-9%\-\_]+)/i; var spotifyRegExp = /(https?:\/\/(open\.spotify\.com|play\.spotify\.com|spoti\.fi)\/(.+)|spotify:(.+))/i; var markdownRegExp = /(^|\s)(````?)([\s\S]+?)(````?)([\s\.,:?!;]|$)|(^|\s)`([^\n]+?)`([\s\.,:?!;]|$)/; var siteHashtags = { Telegram: '#/im?q=%23{1}', Twitter: 'https://twitter.com/hashtag/{1}', Instagram: 'https://instagram.com/explore/tags/{1}/', 'Google Plus': 'https://plus.google.com/explore/{1}' }; var siteMentions = { Telegram: '#/im?p=%40{1}', Twitter: 'https://twitter.com/{1}', Instagram: 'https://instagram.com/{1}/', GitHub: 'https://github.com/{1}' }; return { wrapRichText: wrapRichText, wrapPlainText: wrapPlainText, parseEntities: parseEntities, parseMarkdown: parseMarkdown, mergeEntities: mergeEntities }; function getEmojiSpritesheetCoords(emojiCode) { var i, row, column, totalColumns; for (var cat = 0; cat < Config.EmojiCategories.length; cat++) { totalColumns = Config.EmojiCategorySpritesheetDimens[cat][1]; i = Config.EmojiCategories[cat].indexOf(emojiCode); if (i > -1) { row = Math.floor(i / totalColumns); column = (i % totalColumns); return { category: cat, row: row, column: column }; } } console.error('emoji not found in spritesheet', emojiCode); return null; } function parseEntities (text, options) { options = options || {}; var match, raw = text, url, entities = [], emojiCode, emojiCoords, matchIndex, rawOffset = 0; // var start = tsNow(); while ((match = raw.match(fullRegExp))) { matchIndex = rawOffset + match.index; if (match[3]) { // mentions entities.push({ _: 'messageEntityMention', offset: matchIndex + match[1].length, length: match[2].length + match[3].length }); } else if (match[4]) { if (emailRegExp.test(match[4])) { // email entities.push({ _: 'messageEntityEmail', offset: matchIndex, length: match[4].length }); } else { var url = false, protocol = match[5], tld = match[6], excluded = ''; if (tld) { // URL if (!protocol && (tld.substr(0, 4) === 'xn--' || Config.TLD.indexOf(tld.toLowerCase()) !== -1)) { protocol = 'http://'; } if (protocol) { var balanced = checkBrackets(match[4]); if (balanced.length !== match[4].length) { excluded = match[4].substring(balanced.length); match[4] = balanced; } url = (match[5] ? '' : protocol) + match[4]; } var tgMeMatch; if (tld == 'me' && (tgMeMatch = url.match(/^https?:\/\/telegram\.me\/(.+)/))) { var path = tgMeMatch[1].split('/'); switch (path[0]) { case 'joinchat': url = 'tg://join?invite=' + path[1]; break; case 'addstickers': url = 'tg://addstickers?set=' + path[1]; break; default: var domainQuery = path[0].split('?'); url = 'tg://resolve?domain=' + domainQuery[0] + (domainQuery[1] ? '&' + domainQuery[1] : ''); } } } else { // IP address url = (match[5] ? '' : 'http://') + match[4]; } if (url) { entities.push({ _: 'messageEntityUrl', offset: matchIndex, length: match[4].length }); } } } else if (match[7]) { // New line entities.push({ _: 'messageEntityLinebreak', offset: matchIndex, length: 1 }); } else if (match[8]) { // Emoji if ((emojiCode = emojiMap[match[8]]) && (emojiCoords = getEmojiSpritesheetCoords(emojiCode))) { entities.push({ _: 'messageEntityEmoji', offset: matchIndex, length: match[0].length, coords: emojiCoords, title: emojiData[emojiCode][1][0] }); } } else if (match[10]) { // Hashtag entities.push({ _: 'messageEntityHashtag', offset: matchIndex + match[9].length, length: match[10].length }); } else if (match[12]) { // Bot command entities.push({ _: 'messageEntityBotCommand', offset: matchIndex + match[11].length, length: 1 + match[12].length + (match[13] ? 1 + match[13].length : 0) }); } raw = raw.substr(match.index + match[0].length); rawOffset += match.index + match[0].length; } // if (entities.length) { // console.log('parse entities', text, entities.slice()); // } return entities; } function parseMarkdown (text, entities) { if (text.indexOf('`') == -1) { return text; } var raw = text; var match; var newText = []; var rawOffset = 0; var matchIndex; while (match = raw.match(markdownRegExp)) { matchIndex = rawOffset + match.index; newText.push(raw.substr(0, match.index)); var text = (match[3] || match[7]).replace(/^\s+|\s+$/g, ''); if (text.match(/^`*$/)) { newText.push(match[0]); } else if (match[3]) { // pre newText.push(match[1] + text + match[5]); entities.push({ _: 'messageEntityPre', language: '', offset: matchIndex + match[1].length, length: text.length }); rawOffset -= match[2].length + match[4].length; } else { // code newText.push(match[6] + text + match[8]); entities.push({ _: 'messageEntityCode', offset: matchIndex + match[6].length, length: text.length }); rawOffset -= 2; } raw = raw.substr(match.index + match[0].length); rawOffset += match.index + match[0].length; } newText.push(raw); newText = newText.join(''); if (!newText.replace(/\s+/g, '').length) { newText = text; entities.splice(0, entities.length); } return newText; } function mergeEntities (currentEntities, newEntities, fromApi) { var totalEntities = newEntities.slice(); var i, len = currentEntities.length; var j, len2 = newEntities.length; var startJ = 0; var curEntity, newEntity; var start, end, cStart, cEnd, bad; for (i = 0; i < len; i++) { curEntity = currentEntities[i]; if (fromApi && curEntity._ != 'messageEntityLinebreak' && curEntity._ != 'messageEntityEmoji') { continue; } // console.log('s', curEntity, newEntities); start = curEntity.offset; end = start + curEntity.length; bad = false; for (j = startJ; j < len2; j++) { newEntity = newEntities[j]; cStart = newEntity.offset; cEnd = cStart + newEntity.length; if (cStart <= start) { startJ = j; } if (start >= cStart && start < cEnd || end > cStart && end <= cEnd) { // console.log('bad', curEntity, newEntity); if (fromApi && start >= cStart && end <= cEnd) { if (newEntity.nested === undefined) { newEntity.nested = []; } curEntity.offset -= cStart; newEntity.nested.push(angular.copy(curEntity)); } bad = true; break; } if (cStart >= end) { break; } } if (bad) { continue; } totalEntities.push(curEntity); } totalEntities.sort(function (a, b) { return a.offset - b.offset; }); // console.log('merge', currentEntities, newEntities, totalEntities); return totalEntities; } function wrapRichNestedText (text, nested, options) { if (nested === undefined) { return encodeEntities(text); } return wrapRichText(text, {entities: nested, nested: true}); } function wrapRichText (text, options) { if (!text || !text.length) { return ''; } options = options || {}; var entities = options.entities, contextSite = options.contextSite || 'Telegram', contextExternal = contextSite != 'Telegram', emojiFound = false; if (entities === undefined) { entities = parseEntities(text, options); } var i = 0; var len = entities.length; var entity; var entityText; var skipEntity; var url; var html = []; var lastOffset = 0; for (i = 0; i < len; i++) { entity = entities[i]; if (entity.offset > lastOffset) { html.push( encodeEntities(text.substr(lastOffset, entity.offset - lastOffset)) ); } else if (entity.offset < lastOffset) { continue; } skipEntity = false; entityText = text.substr(entity.offset, entity.length); switch (entity._) { case 'messageEntityMention': var contextUrl = !options.noLinks && siteMentions[contextSite]; if (!contextUrl) { skipEntity = true; break; } var username = entityText.substr(1); var attr = ''; if (options.highlightUsername && options.highlightUsername.toLowerCase() == username.toLowerCase()) { attr = 'class="im_message_mymention"'; } html.push( '', encodeEntities(entityText), '' ); break; case 'messageEntityHashtag': var contextUrl = !options.noLinks && siteHashtags[contextSite]; if (!contextUrl) { skipEntity = true; break; } var hashtag = entityText.substr(1); html.push( '', encodeEntities(entityText), '' ); break; case 'messageEntityEmail': if (options.noLinks) { skipEntity = true; break; } html.push( '', encodeEntities(entityText), '' ); break; case 'messageEntityUrl': case 'messageEntityTextUrl': if (options.noLinks) { skipEntity = true; break; } var url = entity.url || entityText; if (!url.match(/^https?:\/\//i)) { url = 'http://' + url; } var tgMeMatch; if (entity._ == 'messageEntityTextUrl') { url = 'tg://unsafe_url?url=' + encodeURIComponent(url); } else if ((tgMeMatch = url.match(/^https?:\/\/telegram\.me\/(.+)/))) { var path = tgMeMatch[1].split('/'); switch (path[0]) { case 'joinchat': url = 'tg://join?invite=' + path[1]; break; case 'addstickers': url = 'tg://addstickers?set=' + path[1]; break; default: var domainQuery = path[0].split('?'); url = 'tg://resolve?domain=' + domainQuery[0] + (domainQuery[1] ? '&' + domainQuery[1] : ''); } } html.push( '', wrapRichNestedText(entityText, entity.nested, options), '' ); break; case 'messageEntityLinebreak': html.push(options.noLinebreaks ? ' ' : '
'); break; case 'messageEntityEmoji': html.push( '', ':', entity.title, ':' ); emojiFound = true; break; case 'messageEntityBotCommand': if (options.noLinks || options.noCommands || contextExternal) { skipEntity = true; break; } var command = entityText.substr(1); var bot, atPos; if ((atPos = command.indexOf('@')) != -1) { bot = command.substr(atPos); command = command.substr(0, atPos); } else { bot = options.fromBot; } html.push( '', encodeEntities(entityText), '' ); break; case 'messageEntityBold': html.push( '', wrapRichNestedText(entityText, entity.nested, options), '' ); break; case 'messageEntityItalic': html.push( '', wrapRichNestedText(entityText, entity.nested, options), '' ); break; case 'messageEntityCode': html.push( '', encodeEntities(entityText), '' ); break; case 'messageEntityPre': html.push( '
',
            encodeEntities(entityText),
            '
' ); break; default: skipEntity = true; } lastOffset = entity.offset + (skipEntity ? 0 : entity.length); } html.push(encodeEntities(text.substr(lastOffset))); text = $sanitize(html.join('')); if (emojiFound && !options.nested) { text = text.replace(/\ufe0f|️|�|‍/g, '', text); text = text.replace(//g, ''); } return $sce.trustAs('html', text); } function checkBrackets(url) { var urlLength = url.length, urlOpenBrackets = url.split('(').length - 1, urlCloseBrackets = url.split(')').length - 1; while (urlCloseBrackets > urlOpenBrackets && url.charAt(urlLength - 1) === ')') { url = url.substr(0, urlLength - 1); urlCloseBrackets--; urlLength--; } if (urlOpenBrackets > urlCloseBrackets) { url = url.replace(/\)+$/, ''); } return url; } function wrapPlainText (text, options) { if (emojiSupported) { return text; } if (!text || !text.length) { return ''; } options = options || {}; text = text.replace(/\ufe0f/g, '', text); var match, raw = text, text = [], emojiTitle; while ((match = raw.match(fullRegExp))) { text.push(raw.substr(0, match.index)); if (match[8]) { if ((emojiCode = emojiMap[match[8]]) && (emojiTitle = emojiData[emojiCode][1][0])) { text.push(':' + emojiTitle + ':'); } else { text.push(match[0]); } } else { text.push(match[0]); } raw = raw.substr(match.index + match[0].length); } text.push(raw); return text.join(''); } })