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.
 
 
 
 
 

337 lines
9.4 KiB

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*
* Originally from:
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
import { Database } from "../../config/databases";
import DATABASE_STATE from "../../config/databases/state";
import { IS_FIREFOX } from "../../environment/userAgent";
import deepEqual from "../../helpers/object/deepEqual";
import IDBStorage from "../files/idb";
import { log, serviceMessagePort } from "./index.service";
import { ServicePushPingTaskPayload } from "./serviceMessagePort";
const ctx = self as any as ServiceWorkerGlobalScope;
const defaultBaseUrl = location.protocol + '//' + location.hostname + location.pathname.split('/').slice(0, -1).join('/') + '/';
export type PushNotificationObject = {
loc_key: string,
loc_args: string[],
//user_id: number, // should be number
custom: {
channel_id?: string, // should be number
chat_id?: string, // should be number
from_id?: string, // should be number
msg_id: string,
peerId?: string // should be number
},
sound?: string,
random_id: number,
badge?: string, // should be number
description: string,
mute: string, // should be number
title: string,
action?: 'mute1d' | 'push_settings', // will be set before postMessage to main thread
};
class SomethingGetter<T extends Database<any>, Storage extends Record<string, any>> {
private cache: Partial<Storage> = {};
private storage: IDBStorage<T>;
constructor(
db: T,
storeName: typeof db['stores'][number]['name'],
private defaults: {
[Property in keyof Storage]: ((value: Storage[Property]) => Storage[Property]) | Storage[Property]
}
) {
this.storage = new IDBStorage<T>(db, storeName);
}
public async get<T extends keyof Storage>(key: T) {
if(this.cache[key] !== undefined) {
return this.cache[key];
}
let value: Storage[T];
try {
value = await this.storage.get(key as string);
} catch(err) {
}
if(this.cache[key] !== undefined) {
return this.cache[key];
}
if(value === undefined) {
const callback = this.defaults[key];
value = typeof(callback) === 'function' ? callback() : callback;
}
return this.cache[key] = value;
}
public async set<T extends keyof Storage>(key: T, value: Storage[T]) {
const cached = this.cache[key] ?? this.defaults[key];
if(deepEqual(cached, value)) {
return;
}
this.cache[key] = value;
try {
this.storage.save(key as string, value);
} catch(err) {
}
}
}
type PushStorage = {
push_mute_until: number,
push_lang: Partial<ServicePushPingTaskPayload['lang']>
push_settings: Partial<ServicePushPingTaskPayload['settings']>
};
const getter = new SomethingGetter<typeof DATABASE_STATE, PushStorage>(DATABASE_STATE, 'session', {
push_mute_until: 0,
push_lang: {
push_message_nopreview: 'You have a new message',
push_action_mute1d: 'Mute for 24H',
push_action_settings: 'Settings'
},
push_settings: {}
});
ctx.addEventListener('push', (event) => {
const obj: PushNotificationObject = event.data.json();
log('push', obj);
let hasActiveWindows = false;
const checksPromise = Promise.all([
getter.get('push_mute_until'),
ctx.clients.matchAll({type: 'window'})
]).then((result) => {
const [muteUntil, clientList] = result;
log('matched clients', clientList);
hasActiveWindows = clientList.length > 0;
if(hasActiveWindows) {
throw 'Supress notification because some instance is alive';
}
const nowTime = Date.now();
if(userInvisibleIsSupported() &&
muteUntil &&
nowTime < muteUntil) {
throw `Supress notification because mute for ${Math.ceil((muteUntil - nowTime) / 60000)} min`;
}
if(!obj.badge) {
throw 'No badge?';
}
});
checksPromise.catch((reason) => {
log(reason);
});
const notificationPromise = checksPromise.then(() => {
return Promise.all([getter.get('push_settings'), getter.get('push_lang')])
}).then((result) => {
return fireNotification(obj, result[0], result[1]);
});
const closePromise = notificationPromise.catch(() => {
log('Closing all notifications on push', hasActiveWindows);
if(userInvisibleIsSupported() || hasActiveWindows) {
return closeAllNotifications();
}
return ctx.registration.showNotification('Telegram', {
tag: 'unknown_peer'
}).then(() => {
if(hasActiveWindows) {
return closeAllNotifications();
}
setTimeout(() => closeAllNotifications(), hasActiveWindows ? 0 : 100);
}).catch((error) => {
log.error('Show notification error', error);
});
});
event.waitUntil(closePromise);
});
ctx.addEventListener('notificationclick', (event) => {
const notification = event.notification;
log('On notification click: ', notification.tag);
notification.close();
const action = event.action as PushNotificationObject['action'];
if(action === 'mute1d' && userInvisibleIsSupported()) {
log('[SW] mute for 1d');
getter.set('push_mute_until', Date.now() + 86400e3);
return;
}
const data: PushNotificationObject = notification.data;
if(!data) {
return;
}
const promise = ctx.clients.matchAll({
type: 'window'
}).then((clientList) => {
data.action = action;
pendingNotification = data;
for(let i = 0; i < clientList.length; i++) {
const client = clientList[i];
if('focus' in client) {
client.focus();
serviceMessagePort.invokeVoid('pushClick', pendingNotification, client);
pendingNotification = undefined;
return;
}
}
if(ctx.clients.openWindow) {
return getter.get('push_settings').then((settings) => {
return ctx.clients.openWindow(settings.baseUrl || defaultBaseUrl);
});
}
}).catch((error) => {
log.error('Clients.matchAll error', error);
})
event.waitUntil(promise);
});
ctx.addEventListener('notificationclose', onCloseNotification);
let notifications: Set<Notification> = new Set();
let pendingNotification: PushNotificationObject;
function pushToNotifications(notification: Notification) {
if(!notifications.has(notification)) {
notifications.add(notification);
// @ts-ignore
notification.onclose = onCloseNotification;
}
}
function onCloseNotification(event: NotificationEvent) {
removeFromNotifications(event.notification)
}
function removeFromNotifications(notification: Notification) {
notifications.delete(notification);
}
export function closeAllNotifications() {
for(const notification of notifications) {
try {
notification.close();
} catch(e) {}
}
let promise: Promise<void>;
if('getNotifications' in ctx.registration) {
promise = ctx.registration.getNotifications({}).then((notifications) => {
for(let i = 0, len = notifications.length; i < len; ++i) {
try {
notifications[i].close();
} catch(e) {}
}
}).catch((error) => {
log.error('Offline register SW error', error);
});
} else {
promise = Promise.resolve();
}
notifications.clear();
return promise;
}
function userInvisibleIsSupported() {
return IS_FIREFOX;
}
function fireNotification(obj: PushNotificationObject, settings: PushStorage['push_settings'], lang: PushStorage['push_lang']) {
const icon = 'assets/img/logo_filled_rounded.png';
let title = obj.title || 'Telegram';
let body = obj.description || '';
let peerId: string;
if(obj.custom) {
if(obj.custom.channel_id) {
peerId = '' + -obj.custom.channel_id;
} else if(obj.custom.chat_id) {
peerId = '' + -obj.custom.chat_id;
} else {
peerId = obj.custom.from_id || '';
}
}
obj.custom.peerId = '' + peerId;
let tag = 'peer' + peerId;
if(settings && settings.nopreview) {
title = 'Telegram';
body = lang.push_message_nopreview;
tag = 'unknown_peer';
}
log('show notify', title, body, icon, obj);
const actions: (Omit<NotificationAction, 'action'> & {action: PushNotificationObject['action']})[] = [{
action: 'mute1d',
title: lang.push_action_mute1d
}/* , {
action: 'push_settings',
title: lang.push_action_settings || 'Settings'
} */];
const notificationPromise = ctx.registration.showNotification(title, {
body,
icon,
tag,
data: obj,
actions
});
return notificationPromise.then((event) => {
// @ts-ignore
if(event?.notification) {
// @ts-ignore
pushToNotifications(event.notification);
}
}).catch((error) => {
log.error('Show notification promise', error);
});
}
export function onPing(payload: ServicePushPingTaskPayload, source?: MessageEventSource) {
if(pendingNotification && source) {
serviceMessagePort.invokeVoid('pushClick', pendingNotification, source);
pendingNotification = undefined;
}
if(payload.lang) {
getter.set('push_lang', payload.lang);
}
if(payload.settings) {
getter.set('push_settings', payload.settings);
}
}