Telegram Web K with changes to work inside I2P
https://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.
430 lines
12 KiB
430 lines
12 KiB
// just to include |
|
import {secureRandom} from '../polyfill'; |
|
secureRandom; |
|
|
|
import apiManager from "./apiManager"; |
|
import AppStorage from '../storage'; |
|
import cryptoWorker from "../crypto/cryptoworker"; |
|
import networkerFactory from "./networkerFactory"; |
|
import apiFileManager, { DownloadOptions } from './apiFileManager'; |
|
import { getFileNameByLocation } from '../bin_utils'; |
|
import { logger, LogLevels } from '../logger'; |
|
import { isSafari } from '../../helpers/userAgent'; |
|
|
|
const log = logger('SW'/* , LogLevels.error */); |
|
|
|
const ctx = self as any as ServiceWorkerGlobalScope; |
|
|
|
//console.error('INCLUDE !!!', new Error().stack); |
|
|
|
function isObject(object: any) { |
|
return typeof(object) === 'object' && object !== null; |
|
} |
|
|
|
/* function fillTransfer(transfer: any, obj: any) { |
|
if(!obj) return; |
|
|
|
if(obj instanceof ArrayBuffer) { |
|
transfer.add(obj); |
|
} else if(obj.buffer && obj.buffer instanceof ArrayBuffer) { |
|
transfer.add(obj.buffer); |
|
} else if(isObject(obj)) { |
|
for(var i in obj) { |
|
fillTransfer(transfer, obj[i]); |
|
} |
|
} else if(Array.isArray(obj)) { |
|
obj.forEach(value => { |
|
fillTransfer(transfer, value); |
|
}); |
|
} |
|
} */ |
|
|
|
/** |
|
* Respond to request |
|
*/ |
|
function respond(client: Client | ServiceWorker | MessagePort, ...args: any[]) { |
|
// отключил для всего потому что не успел пофиксить transfer detached |
|
//if(isSafari(self)/* || true */) { |
|
// @ts-ignore |
|
client.postMessage(...args); |
|
/* } else { |
|
var transfer = new Set(); |
|
fillTransfer(transfer, arguments); |
|
|
|
//console.log('reply', transfer, [...transfer]); |
|
ctx.postMessage(...arguments, [...transfer]); |
|
//console.log('reply', transfer, [...transfer]); |
|
} */ |
|
} |
|
|
|
/** |
|
* Broadcast Notification |
|
*/ |
|
function notify(...args: any[]) { |
|
ctx.clients.matchAll({includeUncontrolled: false, type: 'window'}).then((listeners) => { |
|
if(!listeners.length) { |
|
//console.trace('no listeners?', self, listeners); |
|
return; |
|
} |
|
|
|
listeners.forEach(listener => { |
|
// @ts-ignore |
|
listener.postMessage(...args); |
|
}); |
|
}); |
|
} |
|
|
|
networkerFactory.setUpdatesProcessor((obj, bool) => { |
|
notify({update: {obj, bool}}); |
|
}); |
|
|
|
ctx.addEventListener('message', async(e) => { |
|
const taskID = e.data.taskID; |
|
|
|
log.debug('got message:', taskID, e, e.data); |
|
|
|
if(e.data.useLs) { |
|
AppStorage.finishTask(e.data.taskID, e.data.args); |
|
return; |
|
} else if(e.data.type == 'convertWebp') { |
|
const {fileName, bytes} = e.data.payload; |
|
const deferred = apiFileManager.webpConvertPromises[fileName]; |
|
if(deferred) { |
|
deferred.resolve(bytes); |
|
delete apiFileManager.webpConvertPromises[fileName]; |
|
} |
|
} |
|
|
|
switch(e.data.task) { |
|
case 'computeSRP': |
|
case 'gzipUncompress': |
|
// @ts-ignore |
|
return cryptoWorker[e.data.task].apply(cryptoWorker, e.data.args).then(result => { |
|
respond(e.source, {taskID: taskID, result: result}); |
|
}); |
|
|
|
case 'cancelDownload': |
|
case 'downloadFile': { |
|
/* // @ts-ignore |
|
return apiFileManager.downloadFile(...e.data.args); */ |
|
|
|
try { |
|
// @ts-ignore |
|
let result = apiFileManager[e.data.task].apply(apiFileManager, e.data.args); |
|
|
|
if(result instanceof Promise) { |
|
result = await result; |
|
} |
|
|
|
respond(e.source, {taskID: taskID, result: result}); |
|
} catch(err) { |
|
respond(e.source, {taskID: taskID, error: err}); |
|
} |
|
} |
|
|
|
default: { |
|
try { |
|
// @ts-ignore |
|
let result = apiManager[e.data.task].apply(apiManager, e.data.args); |
|
|
|
if(result instanceof Promise) { |
|
result = await result; |
|
} |
|
|
|
respond(e.source, {taskID: taskID, result: result}); |
|
} catch(err) { |
|
respond(e.source, {taskID: taskID, error: err}); |
|
} |
|
|
|
//throw new Error('Unknown task: ' + e.data.task); |
|
} |
|
} |
|
}); |
|
|
|
/** |
|
* Service Worker Installation |
|
*/ |
|
ctx.addEventListener('install', (event: ExtendableEvent) => { |
|
log('installing'); |
|
|
|
/* initCache(); |
|
|
|
event.waitUntil( |
|
initNetwork(), |
|
); */ |
|
event.waitUntil(ctx.skipWaiting()); // Activate worker immediately |
|
}); |
|
|
|
/** |
|
* Service Worker Activation |
|
*/ |
|
ctx.addEventListener('activate', (event) => { |
|
log('activating', ctx); |
|
|
|
/* if (!ctx.cache) initCache(); |
|
if (!ctx.network) initNetwork(); */ |
|
|
|
event.waitUntil(ctx.clients.claim()); |
|
}); |
|
|
|
function timeout(delay: number): Promise<Response> { |
|
return new Promise(((resolve) => { |
|
setTimeout(() => { |
|
resolve(new Response('', { |
|
status: 408, |
|
statusText: 'Request timed out.', |
|
})); |
|
}, delay); |
|
})); |
|
} |
|
|
|
ctx.addEventListener('error', (error) => { |
|
log.error('error:', error); |
|
}); |
|
|
|
/** |
|
* Fetch requests |
|
*/ |
|
ctx.addEventListener('fetch', (event: FetchEvent): void => { |
|
const [, url, scope, params] = /http[:s]+\/\/.*?(\/(.*?)(?:$|\/(.*)$))/.exec(event.request.url) || []; |
|
|
|
log.debug('[fetch]:', event); |
|
|
|
switch(scope) { |
|
case 'download': |
|
case 'thumb': |
|
case 'document': |
|
case 'photo': { |
|
const info: DownloadOptions = JSON.parse(decodeURIComponent(params)); |
|
const fileName = getFileNameByLocation(info.location); |
|
|
|
/* event.request.signal.addEventListener('abort', (e) => { |
|
console.log('[SW] user aborted request:', fileName); |
|
cancellablePromise.cancel(); |
|
}); |
|
|
|
event.request.signal.onabort = (e) => { |
|
console.log('[SW] user aborted request:', fileName); |
|
cancellablePromise.cancel(); |
|
}; |
|
|
|
if(fileName == '5452060085729624717') { |
|
setInterval(() => { |
|
console.log('[SW] request status:', fileName, event.request.signal.aborted); |
|
}, 1000); |
|
} */ |
|
|
|
const cancellablePromise = apiFileManager.downloadFile(info); |
|
cancellablePromise.notify = (progress: {done: number, total: number, offset: number}) => { |
|
notify({progress: {fileName, ...progress}}); |
|
}; |
|
|
|
log.debug('[fetch] file:', /* info, */fileName); |
|
|
|
const promise = cancellablePromise.then(b => new Response(b)); |
|
event.respondWith(Promise.race([ |
|
timeout(45 * 1000), |
|
promise |
|
])); |
|
|
|
break; |
|
} |
|
|
|
case 'stream': { |
|
const [offset, end] = parseRange(event.request.headers.get('Range')); |
|
|
|
const info: DownloadOptions = JSON.parse(decodeURIComponent(params)); |
|
//const fileName = getFileNameByLocation(info.location); |
|
|
|
log.debug('[stream]', url, offset, end); |
|
|
|
event.respondWith(new Promise((resolve, reject) => { |
|
// safari workaround |
|
if(offset === 0 && end === 1) { |
|
resolve(new Response(new Uint8Array(2).buffer, { |
|
status: 206, |
|
statusText: 'Partial Content', |
|
headers: { |
|
'Accept-Ranges': 'bytes', |
|
'Content-Range': `bytes 0-1/${info.size || '*'}`, |
|
'Content-Length': '2', |
|
'Content-Type': info.mimeType || 'video/mp4', |
|
}, |
|
})); |
|
|
|
return; |
|
} |
|
|
|
const limit = end && end < STREAM_CHUNK_UPPER_LIMIT ? alignLimit(end - offset + 1) : STREAM_CHUNK_UPPER_LIMIT; |
|
const alignedOffset = alignOffset(offset, limit); |
|
|
|
//log.debug('[stream] requestFilePart:', info.dcID, info.location, alignedOffset, limit); |
|
|
|
apiFileManager.requestFilePart(info.dcID, info.location, alignedOffset, limit).then(result => { |
|
let ab = result.bytes; |
|
|
|
//log.debug('[stream] requestFilePart result:', result); |
|
|
|
const headers: Record<string, string> = { |
|
'Accept-Ranges': 'bytes', |
|
'Content-Range': `bytes ${alignedOffset}-${alignedOffset + ab.byteLength - 1}/${info.size || '*'}`, |
|
'Content-Length': `${ab.byteLength}`, |
|
}; |
|
|
|
if(info.mimeType) headers['Content-Type'] = info.mimeType; |
|
|
|
if(isSafari) { |
|
ab = ab.slice(offset - alignedOffset, end - alignedOffset + 1); |
|
headers['Content-Range'] = `bytes ${offset}-${offset + ab.byteLength - 1}/${info.size || '*'}`; |
|
headers['Content-Length'] = `${ab.byteLength}`; |
|
} |
|
|
|
resolve(new Response(ab, { |
|
status: 206, |
|
statusText: 'Partial Content', |
|
headers, |
|
})); |
|
}); |
|
})); |
|
break; |
|
} |
|
|
|
/* case 'download': { |
|
const info: DownloadOptions = JSON.parse(decodeURIComponent(params)); |
|
|
|
const promise = new Promise<Response>((resolve) => { |
|
const headers: Record<string, string> = { |
|
'Content-Disposition': `attachment; filename="${info.fileName}"`, |
|
}; |
|
|
|
if(info.size) headers['Content-Length'] = info.size.toString(); |
|
if(info.mimeType) headers['Content-Type'] = info.mimeType; |
|
|
|
log('[download] file:', info); |
|
|
|
const stream = new ReadableStream({ |
|
start(controller: ReadableStreamDefaultController) { |
|
const limitPart = DOWNLOAD_CHUNK_LIMIT; |
|
|
|
apiFileManager.downloadFile({ |
|
...info, |
|
limitPart, |
|
processPart: (bytes, offset) => { |
|
log('[download] file processPart:', bytes, offset); |
|
|
|
controller.enqueue(new Uint8Array(bytes)); |
|
|
|
const isFinal = offset + limitPart >= info.size; |
|
if(isFinal) { |
|
controller.close(); |
|
} |
|
|
|
return Promise.resolve(); |
|
} |
|
}).catch(err => { |
|
log.error('[download] error:', err); |
|
controller.error(err); |
|
}); |
|
}, |
|
|
|
cancel() { |
|
log.error('[download] file canceled:', info); |
|
} |
|
}); |
|
|
|
resolve(new Response(stream, {headers})); |
|
}); |
|
|
|
event.respondWith(promise); |
|
|
|
break; |
|
} */ |
|
|
|
case 'upload': { |
|
if(event.request.method == 'POST') { |
|
event.respondWith(event.request.blob().then(blob => { |
|
return apiFileManager.uploadFile(blob).then(v => new Response(JSON.stringify(v), {headers: {'Content-Type': 'application/json'}})); |
|
})); |
|
} |
|
|
|
break; |
|
} |
|
|
|
/* default: { |
|
|
|
break; |
|
} |
|
case 'documents': |
|
case 'photos': |
|
case 'profiles': |
|
// direct download |
|
if (event.request.method === 'POST') { |
|
event.respondWith(// download(url, 'unknown file.txt', getFilePartRequest)); |
|
event.request.text() |
|
.then((text) => { |
|
const [, filename] = text.split('='); |
|
return download(url, filename ? filename.toString() : 'unknown file', getFilePartRequest); |
|
}), |
|
); |
|
|
|
// inline |
|
} else { |
|
event.respondWith( |
|
ctx.cache.match(url).then((cached) => { |
|
if (cached) return cached; |
|
|
|
return Promise.race([ |
|
timeout(45 * 1000), // safari fix |
|
new Promise<Response>((resolve) => { |
|
fetchRequest(url, resolve, getFilePartRequest, ctx.cache, fileProgress); |
|
}), |
|
]); |
|
}), |
|
); |
|
} |
|
break; |
|
|
|
case 'stream': { |
|
const [offset, end] = parseRange(event.request.headers.get('Range') || ''); |
|
|
|
log('stream', url, offset, end); |
|
|
|
event.respondWith(new Promise((resolve) => { |
|
fetchStreamRequest(url, offset, end, resolve, getFilePartRequest); |
|
})); |
|
break; |
|
} |
|
|
|
case 'stripped': |
|
case 'cached': { |
|
const bytes = getThumb(url) || null; |
|
event.respondWith(new Response(bytes, { headers: { 'Content-Type': 'image/jpg' } })); |
|
break; |
|
} |
|
|
|
default: |
|
if (url && url.endsWith('.tgs')) event.respondWith(fetchTGS(url)); |
|
else event.respondWith(fetch(event.request.url)); */ |
|
} |
|
}); |
|
|
|
const DOWNLOAD_CHUNK_LIMIT = 512 * 1024; |
|
const STREAM_CHUNK_UPPER_LIMIT = 256 * 1024; |
|
const SMALLEST_CHUNK_LIMIT = 256 * 4; |
|
|
|
function parseRange(header: string): [number, number] { |
|
if(!header) return [0, 0]; |
|
const [, chunks] = header.split('='); |
|
const ranges = chunks.split(', '); |
|
const [offset, end] = ranges[0].split('-'); |
|
|
|
return [+offset, +end || 0]; |
|
} |
|
|
|
function alignOffset(offset: number, base = SMALLEST_CHUNK_LIMIT) { |
|
return offset - (offset % base); |
|
} |
|
|
|
function alignLimit(limit: number) { |
|
return 2 ** Math.ceil(Math.log(limit) / Math.log(2)); |
|
}
|
|
|