/* * 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 * https://github.com/zhukov/webogram/blob/master/LICENSE */ import { formatPhoneNumber } from "../../components/misc"; import { MOUNT_CLASS_TO } from "../../config/debug"; import { tsNow } from "../../helpers/date"; import { safeReplaceObject, isObject } from "../../helpers/object"; import { InputUser, Update, User as MTUser, UserStatus } from "../../layer"; import I18n, { i18n, LangPackKey } from "../langPack"; //import apiManager from '../mtproto/apiManager'; import apiManager from '../mtproto/mtprotoworker'; import { REPLIES_PEER_ID } from "../mtproto/mtproto_config"; import serverTimeManager from "../mtproto/serverTimeManager"; import { RichTextProcessor } from "../richtextprocessor"; import rootScope from "../rootScope"; import searchIndexManager from "../searchIndexManager"; import apiUpdatesManager from "./apiUpdatesManager"; import appChatsManager from "./appChatsManager"; import appPeersManager from "./appPeersManager"; import appStateManager from "./appStateManager"; // TODO: updateUserBlocked export type User = MTUser.user; export class AppUsersManager { private storage = appStateManager.storages.users; private users: {[userId: number]: User} = {}; private usernames: {[username: string]: number} = {}; private contactsIndex = searchIndexManager.createIndex(); private contactsFillPromise: Promise>; private contactsList: Set = new Set(); private updatedContactsList = false; private getTopPeersPromise: Promise; constructor() { setInterval(this.updateUsersStatuses, 60000); rootScope.on('state_synchronized', this.updateUsersStatuses); rootScope.addMultipleEventsListeners({ updateUserStatus: (update) => { const userId = update.user_id; const user = this.users[userId]; if(user) { user.status = update.status; if(user.status) { if('expires' in user.status) { user.status.expires -= serverTimeManager.serverTimeOffset; } if('was_online' in user.status) { user.status.was_online -= serverTimeManager.serverTimeOffset; } } //user.sortStatus = this.getUserStatusForSort(user.status); rootScope.broadcast('user_update', userId); this.setUserToStateIfNeeded(user); } //////else console.warn('No user by id:', userId); }, updateUserPhoto: (update) => { const userId = update.user_id; const user = this.users[userId]; if(user) { this.forceUserOnline(userId); if(update.photo._ === 'userProfilePhotoEmpty') { delete user.photo; } else { user.photo = safeReplaceObject(user.photo, update.photo); } this.setUserToStateIfNeeded(user); rootScope.broadcast('user_update', userId); rootScope.broadcast('avatar_update', userId); } else console.warn('No user by id:', userId); }, updateUserName: (update) => { const userId = update.user_id; const user = this.users[userId]; if(user) { this.forceUserOnline(userId); this.saveApiUser(Object.assign({}, user, { first_name: update.first_name, last_name: update.last_name, username: update.username }), true); } } }); /* case 'updateContactLink': this.onContactUpdated(update.user_id, update.my_link._ === 'contactLinkContact'); break; */ rootScope.on('language_change', (e) => { const userId = this.getSelf().id; searchIndexManager.indexObject(userId, this.getUserSearchText(userId), this.contactsIndex); }); appStateManager.getState().then((state) => { const users = appStateManager.storagesResults.users; if(users.length) { this.users = {}; for(let i = 0, length = users.length; i < length; ++i) { const user = users[i]; if(user) { this.users[user.id] = user; } } } const contactsList = state.contactsList; if(contactsList && Array.isArray(contactsList)) { contactsList.forEach(userId => { this.pushContact(userId); }); if(contactsList.length) { this.contactsFillPromise = Promise.resolve(this.contactsList); } } appStateManager.addEventListener('peerNeeded', (peerId: number) => { if(peerId < 0 || this.storage.getFromCache(peerId)) { return; } this.storage.set({ [peerId]: this.getUser(peerId) }); }); appStateManager.addEventListener('peerUnneeded', (peerId: number) => { if(peerId < 0 || !this.storage.getFromCache(peerId)) { return; } this.storage.delete(peerId); }); }); } private onContactsModified() { const contactsList = [...this.contactsList]; appStateManager.pushToState('contactsList', contactsList); } public fillContacts() { if(this.contactsFillPromise && this.updatedContactsList) { return this.contactsFillPromise; } this.updatedContactsList = true; const promise = apiManager.invokeApi('contacts.getContacts').then((result) => { if(result._ === 'contacts.contacts') { this.saveApiUsers(result.users); result.contacts.forEach((contact) => { this.pushContact(contact.user_id); }); this.onContactsModified(); } this.contactsFillPromise = promise; return this.contactsList; }); return this.contactsFillPromise || (this.contactsFillPromise = promise); } public resolveUsername(username: string) { if(username[0] === '@') { username = username.slice(1); } username = username.toLowerCase(); if(this.usernames[username]) { return Promise.resolve(this.users[this.usernames[username]]); } return apiManager.invokeApi('contacts.resolveUsername', {username}).then(resolvedPeer => { this.saveApiUsers(resolvedPeer.users); appChatsManager.saveApiChats(resolvedPeer.chats); return appPeersManager.getPeer(appPeersManager.getPeerId(resolvedPeer.peer)); }); } public pushContact(userId: number) { this.contactsList.add(userId); searchIndexManager.indexObject(userId, this.getUserSearchText(userId), this.contactsIndex); appStateManager.requestPeer(userId, 'contacts'); } public getUserSearchText(id: number) { const user = this.users[id]; if(!user) { return ''; } const arr: string[] = [ user.first_name, user.last_name, user.phone, user.username, user.pFlags.self ? I18n.format('SavedMessages', true) : '', user.pFlags.self ? 'Saved Messages' : '' ]; return arr.filter(Boolean).join(' '); } public getContacts(query?: string, includeSaved = false) { return this.fillContacts().then(_contactsList => { let contactsList = [..._contactsList]; if(query) { const results = searchIndexManager.search(query, this.contactsIndex); const filteredContactsList = [...contactsList].filter(id => !!results[id]); contactsList = filteredContactsList; } contactsList.sort((userId1: number, userId2: number) => { const sortName1 = (this.users[userId1] || {}).sortName || ''; const sortName2 = (this.users[userId2] || {}).sortName || ''; return sortName1.localeCompare(sortName2); }); if(includeSaved) { if(this.testSelfSearch(query)) { contactsList.findAndSplice(p => p === rootScope.myId); contactsList.unshift(rootScope.myId); } } /* contactsList.sort((userId1: number, userId2: number) => { const sortName1 = (this.users[userId1] || {}).sortName || ''; const sortName2 = (this.users[userId2] || {}).sortName || ''; if(sortName1 === sortName2) { return 0; } return sortName1 > sortName2 ? 1 : -1; }); */ return contactsList; }); } public toggleBlock(peerId: number, block: boolean) { return apiManager.invokeApi(block ? 'contacts.block' : 'contacts.unblock', { id: appPeersManager.getInputPeerById(peerId) }).then(value => { if(value) { apiUpdatesManager.processUpdateMessage({ _: 'updateShort', update: { _: 'updatePeerBlocked', peer_id: appPeersManager.getOutputPeer(peerId), blocked: block } as Update.updatePeerBlocked }); } return value; }); } public testSelfSearch(query: string) { const user = this.getSelf(); const index = searchIndexManager.createIndex(); searchIndexManager.indexObject(user.id, this.getUserSearchText(user.id), index); return !!searchIndexManager.search(query, index)[user.id]; } public saveApiUsers(apiUsers: any[], override?: boolean) { apiUsers.forEach((user) => this.saveApiUser(user, override)); } public saveApiUser(user: MTUser, override?: boolean) { if(user._ === 'userEmpty') return; const userId = user.id; const oldUser = this.users[userId]; if(oldUser && !override) { return; } if(user.pFlags === undefined) { user.pFlags = {}; } if(user.pFlags.min && oldUser !== undefined) { return; } // * exclude from state // defineNotNumerableProperties(user, ['initials', 'num', 'rFirstName', 'rFullName', 'rPhone', 'sortName', 'sortStatus']); const fullName = user.first_name + ' ' + (user.last_name || ''); if(user.username) { const searchUsername = searchIndexManager.cleanUsername(user.username); this.usernames[searchUsername] = userId; } user.sortName = user.pFlags.deleted ? '' : searchIndexManager.cleanSearchText(fullName, false); user.initials = RichTextProcessor.getAbbreviation(fullName); if(user.status) { if((user.status as UserStatus.userStatusOnline).expires) { (user.status as UserStatus.userStatusOnline).expires -= serverTimeManager.serverTimeOffset; } if((user.status as UserStatus.userStatusOffline).was_online) { (user.status as UserStatus.userStatusOffline).was_online -= serverTimeManager.serverTimeOffset; } } //user.sortStatus = user.pFlags.bot ? -1 : this.getUserStatusForSort(user.status); let changedTitle = false; if(oldUser === undefined) { this.users[userId] = user; } else { if(user.first_name !== oldUser.first_name || user.last_name !== oldUser.last_name || user.username !== oldUser.username) { changedTitle = true; } safeReplaceObject(oldUser, user); rootScope.broadcast('user_update', userId); } if(changedTitle) { rootScope.broadcast('peer_title_edit', user.id); } this.setUserToStateIfNeeded(user); } public setUserToStateIfNeeded(user: User) { if(appStateManager.isPeerNeeded(user.id)) { this.storage.set({ [user.id]: user }); } } public formatUserPhone(phone: string) { return '+' + formatPhoneNumber(phone).formatted; } public getUserStatusForSort(status: User['status'] | number) { if(typeof(status) === 'number') { status = this.getUser(status).status; } if(status) { const expires = status._ === 'userStatusOnline' ? status.expires : (status._ === 'userStatusOffline' ? status.was_online : 0); if(expires) { return expires; } /* const timeNow = tsNow(true); switch(status._) { case 'userStatusRecently': return timeNow - 86400 * 3; case 'userStatusLastWeek': return timeNow - 86400 * 7; case 'userStatusLastMonth': return timeNow - 86400 * 30; } */ switch(status._) { case 'userStatusRecently': return 3; case 'userStatusLastWeek': return 2; case 'userStatusLastMonth': return 1; } } return 0; } public getUser(id: any): User { if(isObject(id)) { return id; } return this.users[id] || {id: id, pFlags: {deleted: true}, access_hash: ''} as User; } public getSelf() { return this.getUser(rootScope.myId); } public getUserStatusString(userId: number): HTMLElement { let key: LangPackKey; let args: any[]; switch(userId) { case REPLIES_PEER_ID: key = 'Peer.RepliesNotifications'; break; case 777000: key = 'Peer.ServiceNotifications'; break; default: { if(this.isBot(userId)) { key = 'Presence.bot'; break; } const user = this.getUser(userId); if(!user) { key = '' as any; break; } if(user.pFlags.support) { key = 'Presence.Support'; break; } switch(user.status?._) { case 'userStatusRecently': { key = 'Peer.Status.recently'; break; } case 'userStatusLastWeek': { key = 'Peer.Status.lastWeek'; break; } case 'userStatusLastMonth': { key = 'Peer.Status.lastMonth'; break; } case 'userStatusOffline': { const date = user.status.was_online; const now = Date.now() / 1000; if((now - date) < 60) { key = 'Peer.Status.justNow'; } else if((now - date) < 3600) { key = 'Peer.Status.minAgo'; const c = (now - date) / 60 | 0; args = [c]; } else if(now - date < 86400) { key = 'LastSeen.HoursAgo'; const c = (now - date) / 3600 | 0; args = [c]; } else { key = 'Peer.Status.LastSeenAt'; const d = new Date(date * 1000); args = [('0' + d.getDate()).slice(-2) + '.' + ('0' + (d.getMonth() + 1)).slice(-2), ('0' + d.getHours()).slice(-2) + ':' + ('0' + d.getMinutes()).slice(-2)]; } break; } case 'userStatusOnline': { key = 'Peer.Status.online'; break; } default: { key = 'Peer.Status.longTimeAgo'; break; } } break; } } return i18n(key, args); } public isBot(id: number) { return this.users[id] && this.users[id].pFlags.bot; } public isContact(id: number) { return this.contactsList.has(id); } public isRegularUser(id: number) { const user = this.users[id]; return user && !this.isBot(id) && !user.pFlags.deleted && !user.pFlags.support; } public isNonContactUser(id: number) { return this.isRegularUser(id) && !this.isContact(id) && id !== rootScope.myId; } public hasUser(id: number, allowMin?: boolean) { var user = this.users[id]; return isObject(user) && (allowMin || !user.pFlags.min); } public canSendToUser(id: number) { const user = this.getUser(id); return !user.pFlags.deleted && user.username !== 'replies'; } public getUserPhoto(id: number) { const user = this.getUser(id); return user && user.photo || { _: 'userProfilePhotoEmpty' }; } public getUserString(id: number) { const user = this.getUser(id); return 'u' + id + (user.access_hash ? '_' + user.access_hash : ''); } public getUserInput(id: number): InputUser { const user = this.getUser(id); if(user.pFlags && user.pFlags.self) { return {_: 'inputUserSelf'}; } return { _: 'inputUser', user_id: id, access_hash: user.access_hash }; } public updateUsersStatuses = () => { const timestampNow = tsNow(true); for(const i in this.users) { const user = this.users[i]; if(user.status && user.status._ === 'userStatusOnline' && user.status.expires < timestampNow) { user.status = {_: 'userStatusOffline', was_online: user.status.expires}; rootScope.broadcast('user_update', user.id); this.setUserToStateIfNeeded(user); } } }; public forceUserOnline(id: number, eventTimestamp?: number) { if(this.isBot(id)) { return; } const timestamp = tsNow(true); const onlineTimeFor = 60; if(eventTimestamp) { if((timestamp - eventTimestamp) >= onlineTimeFor) { return; } } else if(apiUpdatesManager.updatesState.syncLoading) { return; } const user = this.getUser(id); if(user && user.status && user.status._ !== 'userStatusOnline' && user.status._ !== 'userStatusEmpty' && !user.pFlags.support && !user.pFlags.deleted) { user.status = { _: 'userStatusOnline', expires: timestamp + onlineTimeFor }; //user.sortStatus = this.getUserStatusForSort(user.status); rootScope.broadcast('user_update', id); this.setUserToStateIfNeeded(user); } } /* function importContact (phone, firstName, lastName) { return MtpApiManager.invokeApi('contacts.importContacts', { contacts: [{ _: 'inputPhoneContact', client_id: '1', phone: phone, first_name: firstName, last_name: lastName }], replace: false }).then(function (importedContactsResult) { saveApiUsers(importedContactsResult.users) var foundUserID = false angular.forEach(importedContactsResult.imported, function (importedContact) { onContactUpdated(foundUserID = importedContact.user_id, true) }) return foundUserID || false }) } function importContacts (contacts) { var inputContacts = [], i var j for (i = 0; i < contacts.length; i++) { for (j = 0; j < contacts[i].phones.length; j++) { inputContacts.push({ _: 'inputPhoneContact', client_id: (i << 16 | j).toString(10), phone: contacts[i].phones[j], first_name: contacts[i].first_name, last_name: contacts[i].last_name }) } } return MtpApiManager.invokeApi('contacts.importContacts', { contacts: inputContacts, replace: false }).then(function (importedContactsResult) { saveApiUsers(importedContactsResult.users) var result = [] angular.forEach(importedContactsResult.imported, function (importedContact) { onContactUpdated(importedContact.user_id, true) result.push(importedContact.user_id) }) return result }) } */ /* public deleteContacts(userIds: number[]) { var ids: any[] = []; userIds.forEach((userId) => { ids.push(this.getUserInput(userId)); }) return apiManager.invokeApi('contacts.deleteContacts', { id: ids }).then(() => { userIds.forEach((userId) => { this.onContactUpdated(userId, false); }); }); } */ public getTopPeers(): Promise { if(this.getTopPeersPromise) return this.getTopPeersPromise; return this.getTopPeersPromise = appStateManager.getState().then((state) => { if(state?.topPeers?.length) { return state.topPeers; } return apiManager.invokeApi('contacts.getTopPeers', { correspondents: true, offset: 0, limit: 15, hash: 0, }).then((result) => { let peerIds: number[] = []; if(result._ === 'contacts.topPeers') { //console.log(result); this.saveApiUsers(result.users); appChatsManager.saveApiChats(result.chats); if(result.categories.length) { peerIds = result.categories[0].peers.map((topPeer) => { const peerId = appPeersManager.getPeerId(topPeer.peer); appStateManager.requestPeer(peerId, 'topPeer'); return peerId; }); } } appStateManager.pushToState('topPeers', peerIds); return peerIds; }); }); } public getBlocked(offset = 0, limit = 0) { return apiManager.invokeApi('contacts.getBlocked', {offset, limit}).then(contactsBlocked => { this.saveApiUsers(contactsBlocked.users); appChatsManager.saveApiChats(contactsBlocked.chats); const count = contactsBlocked._ === 'contacts.blocked' ? contactsBlocked.users.length + contactsBlocked.chats.length : contactsBlocked.count; const peerIds = contactsBlocked.users.map(u => u.id).concat(contactsBlocked.chats.map(c => -c.id)); return {count, peerIds}; }); } /* public searchContacts(query: string, limit = 20) { return Promise.all([ this.getContacts(query), apiManager.invokeApi('contacts.search', { q: query, limit }) ]).then(results => { const [myContacts, peers] = results; this.saveApiUsers(peers.users); appChatsManager.saveApiChats(peers.chats); // * contacts.search returns duplicates in my_results const myResults = new Set(myContacts.concat(peers.my_results.map(p => appPeersManager.getPeerID(p)))); const out = { my_results: [...myResults].slice(0, limit), results: peers.results.map(p => appPeersManager.getPeerID(p)) }; return out; }); } */ public searchContacts(query: string, limit = 20) { return apiManager.invokeApi('contacts.search', { q: query, limit }).then(peers => { this.saveApiUsers(peers.users); appChatsManager.saveApiChats(peers.chats); const out = { my_results: [...new Set(peers.my_results.map(p => appPeersManager.getPeerId(p)))], // ! contacts.search returns duplicates in my_results results: peers.results.map(p => appPeersManager.getPeerId(p)) }; return out; }); } private onContactUpdated(userId: number, isContact: boolean) { const curIsContact = this.isContact(userId); if(isContact !== curIsContact) { if(isContact) { this.pushContact(userId); } else { this.contactsList.delete(userId); } this.onContactsModified(); rootScope.broadcast('contacts_update', userId); } } public updateUsername(username: string) { return apiManager.invokeApi('account.updateUsername', { username }).then((user) => { this.saveApiUser(user); }); } public setUserStatus(userId: number, offline: boolean) { if(this.isBot(userId)) { return; } const user = this.users[userId]; if(user) { const status: any = offline ? { _: 'userStatusOffline', was_online: tsNow(true) } : { _: 'userStatusOnline', expires: tsNow(true) + 500 }; user.status = status; //user.sortStatus = this.getUserStatusForSort(user.status); rootScope.broadcast('user_update', userId); } } public addContact(userId: number, first_name: string, last_name: string, phone: string, showPhone?: true) { return apiManager.invokeApi('contacts.addContact', { id: this.getUserInput(userId), first_name, last_name, phone, add_phone_privacy_exception: showPhone }).then((updates) => { apiUpdatesManager.processUpdateMessage(updates, {override: true}); this.onContactUpdated(userId, true); }); } public deleteContacts(userIds: number[]) { return apiManager.invokeApi('contacts.deleteContacts', { id: userIds.map(userId => this.getUserInput(userId)) }).then((updates) => { apiUpdatesManager.processUpdateMessage(updates, {override: true}); userIds.forEach(userId => { this.onContactUpdated(userId, false); }); }); } } const appUsersManager = new AppUsersManager(); MOUNT_CLASS_TO.appUsersManager = appUsersManager; export default appUsersManager