Telegram Web, preconfigured for usage in I2P. http://web.telegram.i2p/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

2225 lines
66 KiB

/*!
* Webogram v0.7 - messaging web application for MTProto
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* 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', 'clear'], function (methodName) {
methods[methodName] = function () {
var deferred = $q.defer()
var args = Array.prototype.slice.call(arguments)
args.push(function (result) {
deferred.resolve(result)
})
ConfigStorage[methodName].apply(ConfigStorage, args)
return deferred.promise
}
})
methods.noPrefix = function () {
ConfigStorage.noPrefix()
}
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 isSafari = 'safari' in window
var safariVersion = parseFloat(isSafari && (navigator.userAgent.match(/Version\/(\d+\.\d+).* Safari/) || [])[1])
var safariWithDownload = isSafari && safariVersion >= 11.0
var buggyUnknownBlob = isSafari && !safariWithDownload
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) {
try {
fileWriter.truncate(0)
} catch (e) {}
return $q.reject(error)
})
})
}
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 = []
var 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) {
var safeMimeType = blobSafeMimeType(mimeType)
// console.log(dT(), 'get url', fileData, mimeType, fileData.toURL !== undefined, fileData instanceof Blob)
if (fileData.toURL !== undefined) {
return fileData.toURL(safeMimeType)
}
if (fileData instanceof Blob) {
return URL.createObjectURL(fileData)
}
return 'data:' + safeMimeType + ';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 (isSafari && !safariWithDownload) {
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
if (!safariWithDownload) {
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)
var deferred = $q.defer()
var createObjectStore = function (db) {
db.createObjectStore(dbStoreName)
}
if (!request) {
throw new Exception()
}
} catch (error) {
console.error('error opening db', error.message)
storageIsAvailable = false
return $q.reject(error)
}
var finished = false
setTimeout(function () {
if (!finished) {
request.onerror({type: 'IDB_CREATE_TIMEOUT'})
}
}, 3000)
request.onsuccess = function (event) {
finished = true
var 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) {
finished = true
storageIsAvailable = false
console.error('Error creating/accessing IndexedDB database', event)
deferred.reject(event)
}
request.onupgradeneeded = function (event) {
finished = true
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)
var 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()
}
if (!(blob instanceof Blob)) {
var safeMimeType = blobSafeMimeType(blob.type || 'image/jpeg')
var address = 'data:' + safeMimeType + ';base64,' + bytesToBase64(blob)
return storagePutB64String(db, fileName, address).then(function () {
return blob
})
}
try {
var reader = new FileReader()
} catch (e) {
storageIsAvailable = false
return $q.reject()
}
var deferred = $q.defer()
reader.onloadend = function () {
storagePutB64String(db, fileName, reader.result).then(function () {
deferred.resolve(blob)
}, function (error) {
deferred.reject(error)
})
}
reader.onerror = function (error) {
deferred.reject(error)
}
try {
reader.readAsDataURL(blob)
} catch (e) {
storageIsAvailable = false
return $q.reject()
}
return deferred.promise
}
function storagePutB64String (db, fileName, b64string) {
try {
var objectStore = db.transaction([dbStoreName], IDBTransaction.READ_WRITE || 'readwrite').objectStore(dbStoreName)
var request = objectStore.put(b64string, fileName)
} catch (error) {
storageIsAvailable = false
return $q.reject(error)
}
var deferred = $q.defer()
request.onsuccess = function (event) {
deferred.resolve()
}
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()
var objectStore = db.transaction([dbStoreName], IDBTransaction.READ || 'readonly').objectStore(dbStoreName)
var 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 {
name: 'IndexedDB',
isAvailable: isAvailable,
saveFile: saveFile,
getFile: getFile,
getFileWriter: getFileWriter
}
})
.service('TmpfsFileStorage', function ($q, $window, FileManager) {
$window.requestFileSystem = $window.requestFileSystem || $window.webkitRequestFileSystem
var reqFsPromise,
fileSystem
var 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 Config.allow_tmpfs && 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 {
name: 'TmpFS',
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 {
name: 'Memory',
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
var 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
var naClEmbed = false
var taskID = 0
var awaiting = {}
var webCrypto = Config.Modes.webcrypto && window.crypto && (window.crypto.subtle || window.crypto.webkitSubtle) /* || window.msCrypto && window.msCrypto.subtle*/
var useSha1Crypto = webCrypto && webCrypto.digest !== undefined
var useSha256Crypto = webCrypto && webCrypto.digest !== undefined
var 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 = $('<div id="nacl_listener"><embed id="mtproto_crypto" width="0" height="0" src="nacl/mtproto_crypto.nmf" type="application/x-pnacl" /></div>').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()
var 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()
var 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, $sce) {
var urlPromises = {}
function downloadByURL (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
var url = window.URL.createObjectURL(response.data)
return $sce.trustAsResourceUrl(url)
}, function (error) {
if (!Config.Modes.chrome_packed) {
return $q.when($sce.trustAsResourceUrl(url))
}
return $q.reject(error)
})
}
return {
downloadByURL: downloadByURL
}
})
.service('IdleManager', function ($rootScope, $window, $timeout) {
$rootScope.idle = {isIDLE: false, initial: true}
var toPromise
var debouncePromise
var 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', fake_initial: true})
}, 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)
}
var debounceTimeout = $rootScope.idle.initial ? 0 : 1000
if (e && !e.fake_initial) {
delete $rootScope.idle.initial
}
$timeout.cancel(debouncePromise)
if ($rootScope.idle.isIDLE == isIDLE) {
return
}
debouncePromise = $timeout(function () {
// console.log(dT(), 'IDLE changed', isIDLE)
$rootScope.idle.isIDLE = isIDLE
if (isIDLE && e.type == 'timeout') {
$($window).on('mousemove', onEvent)
}
}, debounceTimeout)
}
})
.service('GeoLocationManager', function ($q) {
var lastCoords = false
function isAvailable () {
return navigator.geolocation !== undefined
}
function getPosition (force) {
if (!force && lastCoords) {
return $q.when(lastCoords)
}
if (!isAvailable()) {
return $q.reject()
}
var deferred = $q.defer()
navigator.geolocation.getCurrentPosition(function (position) {
lastCoords = {
lat: position.coords.latitude,
long: position.coords.longitude
}
deferred.resolve(lastCoords)
}, function (error) {
deferred.reject(error)
})
return deferred.promise
}
return {
getPosition: getPosition,
isAvailable: isAvailable
}
})
.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 emojiData = Config.Emoji
var emojiIconSize = 18
var 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]'
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 markdownTestRegExp = /[`_*@]/
var markdownRegExp = /(^|\s|\n)(````?)([\s\S]+?)(````?)([\s\n\.,:?!;]|$)|(^|\s)(`|\*\*|__)([^\n]+?)\7([\s\.,:?!;]|$)|@(\d+)\s*\((.+?)\)/m
var siteHashtags = {
Telegram: 'tg://search_hashtag?hashtag={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}'
}
var markdownEntities = {
'`': 'messageEntityCode',
'**': 'messageEntityBold',
'__': 'messageEntityItalic'
}
return {
wrapRichText: wrapRichText,
wrapPlainText: wrapPlainText,
wrapDraftText: wrapDraftText,
wrapUrl: wrapUrl,
parseEntities: parseEntities,
parseMarkdown: parseMarkdown,
parseEmojis: parseEmojis,
mergeEntities: mergeEntities
}
function getEmojiSpritesheetCoords (emojiCode) {
var i
var row, column
var 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
var raw = text,
url
var entities = [],
emojiCode,
emojiCoords,
matchIndex
var 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
var protocol = match[5]
var tld = match[6]
var 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]
}
} 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 = EmojiHelper.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 parseEmojis (text) {
return text.replace(/:([a-z0-9\-\+\*_]+?):/gi, function (all, shortcut) {
var emojiCode = EmojiHelper.shortcuts[shortcut]
if (emojiCode !== undefined) {
return EmojiHelper.emojis[emojiCode][0]
}
return all
})
}
function parseMarkdown (text, entities, noTrim) {
    if (!markdownTestRegExp.test(text)) {
return noTrim ? text : text.trim()
}
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[8] || match[11])
rawOffset -= text.length
text = text.replace(/^\s+|\s+$/g, '')
rawOffset += text.length
if (text.match(/^`*$/)) {
newText.push(match[0])
}
else if (match[3]) { // pre
if (match[5] == '\n') {
match[5] = ''
rawOffset -= 1
}
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 if (match[7]) { // code|italic|bold
newText.push(match[6] + text + match[9])
entities.push({
_: markdownEntities[match[7]],
offset: matchIndex + match[6].length,
length: text.length
})
rawOffset -= match[7].length * 2
} else if (match[11]) { // custom mention
newText.push(text)
entities.push({
_: 'messageEntityMentionName',
user_id: match[10],
offset: matchIndex,
length: text.length
})
rawOffset -= match[0].length - text.length
}
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)
}
if (!entities.length && !noTrim) {
newText = newText.trim()
}
return newText
}
function mergeEntities (currentEntities, newEntities, fromApi) {
var totalEntities = newEntities.slice()
var i
var len = currentEntities.length
var j
var len2 = newEntities.length
var startJ = 0
var curEntity
var newEntity
var start, end
var cStart, cEnd
var 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)
}
options.hasNested = true
return wrapRichText(text, {entities: nested, nested: true})
}
function wrapRichText (text, options) {
if (!text || !text.length) {
return ''
}
options = options || {}
var entities = options.entities
var contextSite = options.contextSite || 'Telegram'
var contextExternal = contextSite != 'Telegram'
var 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
var curEmojiSize = options.emojiIconSize || emojiIconSize
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(
'<a ',
attr,
contextExternal ? ' target="_blank" rel="noopener noreferrer" ' : '',
' href="',
contextUrl.replace('{1}', encodeURIComponent(username)),
'">',
encodeEntities(entityText),
'</a>'
)
break
case 'messageEntityMentionName':
if (options.noLinks) {
skipEntity = true
break
}
html.push(
'<a href="#/im?p=u',
encodeURIComponent(entity.user_id),
'">',
encodeEntities(entityText),
'</a>'
)
break
case 'messageEntityHashtag':
var contextUrl = !options.noLinks && siteHashtags[contextSite]
if (!contextUrl) {
skipEntity = true
break
}
var hashtag = entityText.substr(1)
html.push(
'<a ',
contextExternal ? ' target="_blank" rel="noopener noreferrer" ' : '',
'href="',
contextUrl.replace('{1}', encodeURIComponent(hashtag))
,
'">',
encodeEntities(entityText),
'</a>'
)
break
case 'messageEntityEmail':
if (options.noLinks) {
skipEntity = true
break
}
html.push(
'<a href="',
encodeEntities('mailto:' + entityText),
'" target="_blank" rel="noopener noreferrer">',
encodeEntities(entityText),
'</a>'
)
break
case 'messageEntityUrl':
case 'messageEntityTextUrl':
var inner
if (entity._ == 'messageEntityTextUrl') {
url = entity.url
url = wrapUrl(url, true)
inner = wrapRichNestedText(entityText, entity.nested, options)
} else {
url = wrapUrl(entityText, false)
inner = encodeEntities(replaceUrlEncodings(entityText))
}
if (options.noLinks) {
html.push(inner);
} else {
html.push(
'<a href="',
encodeEntities(url),
'" target="_blank" rel="noopener noreferrer">',
inner,
'</a>'
)
}
break
case 'messageEntityLinebreak':
html.push(options.noLinebreaks ? ' ' : '<br/>')
break
case 'messageEntityEmoji':
html.push(
'<span class="emoji emoji-',
entity.coords.category,
'-',
(curEmojiSize * entity.coords.column),
'-',
(curEmojiSize * entity.coords.row),
'" ',
'title="', entity.title, '">',
':', entity.title, ':</span>'
)
emojiFound = true
break
case 'messageEntityBotCommand':
if (options.noLinks || options.noCommands || contextExternal) {
skipEntity = true
break
}
var command = entityText.substr(1)
var bot
var atPos
if ((atPos = command.indexOf('@')) != -1) {
bot = command.substr(atPos + 1)
command = command.substr(0, atPos)
} else {
bot = options.fromBot
}
html.push(
'<a href="',
encodeEntities('tg://bot_command?command=' + encodeURIComponent(command) + (bot ? '&bot=' + encodeURIComponent(bot) : '')),
'">',
encodeEntities(entityText),
'</a>'
)
break
case 'messageEntityBold':
html.push(
'<strong>',
wrapRichNestedText(entityText, entity.nested, options),
'</strong>'
)
break
case 'messageEntityItalic':
html.push(
'<em>',
wrapRichNestedText(entityText, entity.nested, options),
'</em>'
)
break
case 'messageEntityCode':
html.push(
'<code>',
encodeEntities(entityText),
'</code>'
)
break
case 'messageEntityPre':
html.push(
'<pre><code', (entity.language ? ' class="language-' + encodeEntities(entity.language) + '"' : ''), '>',
encodeEntities(entityText),
'</code></pre>'
)
break
default:
skipEntity = true
}
lastOffset = entity.offset + (skipEntity ? 0 : entity.length)
}
html.push(encodeEntities(text.substr(lastOffset)))
text = $sanitize(html.join(''))
if (!options.nested && (emojiFound || options.hasNested)) {
text = text.replace(/\ufe0f|&#65039;|&#65533;|&#8205;/g, '', text)
var emojiSizeClass = curEmojiSize == 18 ? '' : (' emoji-w' + curEmojiSize)
text = text.replace(/<span((?: [^>]*)?) class="emoji emoji-(\d)-(\d+)-(\d+)"(.+?)<\/span>/g,
'<span$1 class="emoji ' + emojiSizeClass + ' emoji-spritesheet-$2" style="background-position: -$3px -$4px;" $5</span>')
}
return $sce.trustAs('html', text)
}
function wrapDraftText (text, options) {
if (!text || !text.length) {
return ''
}
options = options || {}
var entities = options.entities
if (entities === undefined) {
entities = parseEntities(text, options)
}
var i = 0
var len = entities.length
var entity
var entityText
var skipEntity
var code = []
var lastOffset = 0
for (i = 0; i < len; i++) {
entity = entities[i]
if (entity.offset > lastOffset) {
code.push(
text.substr(lastOffset, entity.offset - lastOffset)
)
}
else if (entity.offset < lastOffset) {
continue
}
skipEntity = false
entityText = text.substr(entity.offset, entity.length)
switch (entity._) {
case 'messageEntityEmoji':
code.push(
':',
entity.title,
':'
)
break
case 'messageEntityCode':
code.push(
'`', entityText, '`'
)
break
case 'messageEntityBold':
code.push(
'**', entityText, '**'
)
break
case 'messageEntityItalic':
code.push(
'__', entityText, '__'
)
break
case 'messageEntityPre':
code.push(
'```', entityText, '```'
)
break
case 'messageEntityMentionName':
code.push(
'@', entity.user_id, ' (', entityText, ')'
)
break
default:
skipEntity = true
}
lastOffset = entity.offset + (skipEntity ? 0 : entity.length)
}
code.push(text.substr(lastOffset))
return code.join('')
}
function checkBrackets (url) {
var urlLength = url.length
var urlOpenBrackets = url.split('(').length - 1
var 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 replaceUrlEncodings(urlWithEncoded) {
return urlWithEncoded.replace(/(%[A-Z\d]{2})+/g, function (str) {
try {
return decodeURIComponent(str)
} catch (e) {
return str
}
})
}
function wrapPlainText (text, options) {
if (emojiSupported) {
return text
}
if (!text || !text.length) {
return ''
}
options = options || {}
text = text.replace(/\ufe0f/g, '', text)
var match
var raw = text
var text = [],
emojiTitle
while ((match = raw.match(fullRegExp))) {
text.push(raw.substr(0, match.index))
if (match[8]) {
if ((emojiCode = EmojiHelper.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('')
}
function wrapUrl (url, unsafe) {
if (!url.match(/^(https?|tg):\/\//i)) {
url = 'http://' + url
}
var tgMeMatch
var telescoPeMatch
if (unsafe == 2) {
url = 'tg://unsafe_url?url=' + encodeURIComponent(url)
}
else if ((tgMeMatch = url.match(/^https?:\/\/t(?:elegram)?\.me\/(.+)/))) {
var fullPath = tgMeMatch[1]
var path = fullPath.split('/')
switch (path[0]) {
case 'joinchat':
url = 'tg://join?invite=' + path[1]
break
case 'addstickers':
url = 'tg://addstickers?set=' + path[1]
break
default:
if (path[1] && path[1].match(/^\d+$/)) {
url = 'tg://resolve?domain=' + path[0] + '&post=' + path[1]
}
else if (path.length == 1) {
var domainQuery = path[0].split('?')
var domain = domainQuery[0]
var query = domainQuery[1]
if (domain == 'iv') {
var match = (query || '').match(/url=([^&=]+)/)
if (match) {
url = match[1]
try {
url = decodeURIComponent(url)
} catch (e) {}
return wrapUrl(url, unsafe)
}
}
url = 'tg://resolve?domain=' + domain + (query ? '&' + query : '')
}
}
}
else if ((telescoPeMatch = url.match(/^https?:\/\/telesco\.pe\/([^/?]+)\/(\d+)/))) {
url = 'tg://resolve?domain=' + telescoPeMatch[1] + '&post=' + telescoPeMatch[2]
}
else if (unsafe) {
url = 'tg://unsafe_url?url=' + encodeURIComponent(url)
}
return url
}
})
.service('ServerTimeManager', function (Storage) {
var timestampNow = tsNow(true)
var midnightNoOffset = timestampNow - (timestampNow % 86400)
var midnightOffseted = new Date()
midnightOffseted.setHours(0)
midnightOffseted.setMinutes(0)
midnightOffseted.setSeconds(0)
var midnightOffset = midnightNoOffset - (Math.floor(+midnightOffseted / 1000))
var serverTimeOffset = 0
var timeParams = {
midnightOffset: midnightOffset,
serverTimeOffset: serverTimeOffset
}
Storage.get('server_time_offset').then(function (to) {
if (to) {
serverTimeOffset = to
timeParams.serverTimeOffset = to
}
})
return timeParams
})
.service('WebPushApiManager', function ($window, $timeout, $q, $rootScope, _, AppRuntimeManager) {
var isAvailable = true
var isPushEnabled = false
var localNotificationsAvailable = true
var started = false
var settings = {}
var isAliveTO
var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1
var userVisibleOnly = isFirefox ? false : true
if (!('PushManager' in window) ||
!('Notification' in window) ||
!('serviceWorker' in navigator)) {
console.warn('Push messaging is not supported.')
isAvailable = false
localNotificationsAvailable = false
}
if (isAvailable &&
Notification.permission === 'denied') {
console.warn('The user has blocked notifications.')
}
function start() {
if (!started) {
started = true
getSubscription()
setUpServiceWorkerChannel()
}
}
function setLocalNotificationsDisabled() {
localNotificationsAvailable = false
}
function getSubscription() {
if (!isAvailable) {
return
}
navigator.serviceWorker.ready.then(function(reg) {
reg.pushManager.getSubscription().then(function(subscription) {
isPushEnabled = subscription ? true : false
pushSubscriptionNotify('init', subscription)
})
.catch(function(err) {
console.log('Error during getSubscription()', err)
})
})
}
function subscribe() {
if (!isAvailable) {
return
}
navigator.serviceWorker.ready.then(function(reg) {
reg.pushManager.subscribe({userVisibleOnly: userVisibleOnly}).then(function(subscription) {
// The subscription was successful
isPushEnabled = true
pushSubscriptionNotify('subscribe', subscription)
})
.catch(function(e) {
if (Notification.permission === 'denied') {
console.log('Permission for Notifications was denied')
} else {
console.log('Unable to subscribe to push.', e)
if (!userVisibleOnly) {
userVisibleOnly = true
setTimeout(subscribe, 0)
}
}
})
})
}
function unsubscribe() {
if (!isAvailable) {
return
}
navigator.serviceWorker.ready.then(function(reg) {
reg.pushManager.getSubscription().then(function (subscription) {
isPushEnabled = false
if (subscription) {
pushSubscriptionNotify('unsubscribe', subscription)
setTimeout(function() {
subscription.unsubscribe().then(function(successful) {
isPushEnabled = false
}).catch(function(e) {
console.error('Unsubscription error: ', e)
})
}, 3000)
}
}).catch(function(e) {
console.error('Error thrown while unsubscribing from ' +
'push messaging.', e)
})
})
}
function forceUnsubscribe() {
if (!isAvailable) {
return
}
navigator.serviceWorker.ready.then(function(reg) {
reg.pushManager.getSubscription().then(function (subscription) {
console.warn('force unsubscribe', subscription)
if (subscription) {
subscription.unsubscribe().then(function(successful) {
console.warn('force unsubscribe successful', successful)
isPushEnabled = false
}).catch(function(e) {
console.error('Unsubscription error: ', e)
})
}
}).catch(function(e) {
console.error('Error thrown while unsubscribing from ' +
'push messaging.', e)
})
})
}
function isAliveNotify() {
if (!isAvailable ||
$rootScope.idle && $rootScope.idle.deactivated) {
return
}
settings.baseUrl = (location.href || '').replace(/#.*$/, '') + '#/im'
var eventData = {
type: 'ping',
localNotifications: localNotificationsAvailable,
lang: {
push_action_mute1d: _(Config.Mobile
? 'push_action_mute1d_mobile_raw'
: 'push_action_mute1d_raw'
),
push_action_settings: _(Config.Mobile
? 'push_action_settings_mobile_raw'
: 'push_action_settings_raw'
),
push_message_nopreview: _('push_message_nopreview_raw'),
},
settings: settings
}
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage(eventData)
}
isAliveTO = setTimeout(isAliveNotify, 10000)
}
function setSettings(newSettings) {
settings = angular.copy(newSettings)
clearTimeout(isAliveTO)
isAliveNotify()
}
function hidePushNotifications() {
if (!isAvailable) {
return
}
if (navigator.serviceWorker.controller) {
var eventData = {type: 'notifications_clear'}
navigator.serviceWorker.controller.postMessage(eventData)
}
}
function setUpServiceWorkerChannel() {
if (!isAvailable) {
return
}
navigator.serviceWorker.addEventListener('message', function(event) {
if (event.data &&
event.data.type == 'push_click') {
if ($rootScope.idle && $rootScope.idle.deactivated) {
AppRuntimeManager.reload()
return
}
$rootScope.$emit('push_notification_click', event.data.data)
}
})
navigator.serviceWorker.ready.then(isAliveNotify)
}
function pushSubscriptionNotify(event, subscription) {
if (subscription) {
var subscriptionObj = subscription.toJSON()
if (!subscriptionObj ||
!subscriptionObj.endpoint ||
!subscriptionObj.keys ||
!subscriptionObj.keys.p256dh ||
!subscriptionObj.keys.auth) {
console.warn(dT(), 'Invalid push subscription', subscriptionObj)
unsubscribe()
isAvailable = false
return pushSubscriptionNotify(event, false)
}
console.warn(dT(), 'Push', event, subscriptionObj)
$rootScope.$emit('push_' + event, {
tokenType: 10,
tokenValue: JSON.stringify(subscriptionObj)
})
} else {
console.warn(dT(), 'Push', event, false)
$rootScope.$emit('push_' + event, false)
}
}
return {
isAvailable: isAvailable,
start: start,
isPushEnabled: isPushEnabled,
subscribe: subscribe,
unsubscribe: unsubscribe,
forceUnsubscribe: forceUnsubscribe,
hidePushNotifications: hidePushNotifications,
setLocalNotificationsDisabled: setLocalNotificationsDisabled,
setSettings: setSettings
}
})