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.
608 lines
17 KiB
608 lines
17 KiB
import { safeReplaceObject, isObject, tsNow, copy, $rootScope } from "../utils"; |
|
import { RichTextProcessor } from "../richtextprocessor"; |
|
import appChatsManager from "./appChatsManager"; |
|
//import apiManager from '../mtproto/apiManager'; |
|
import apiManager from '../mtproto/mtprotoworker'; |
|
import serverTimeManager from "../mtproto/serverTimeManager"; |
|
import { formatPhoneNumber } from "../../components/misc"; |
|
import searchIndexManager from "../searchIndexManager"; |
|
import appPeersManager from "./appPeersManager"; |
|
import appStateManager from "./appStateManager"; |
|
|
|
export type User = { |
|
_: 'user', |
|
access_hash: string, |
|
first_name: string, |
|
last_name: string, |
|
username: string, |
|
flags: number, |
|
id: number, |
|
phone: string, |
|
photo: any, |
|
|
|
status?: Partial<{ |
|
_: 'userStatusOffline' | 'userStatusOnline' | 'userStatusRecently' | 'userStatusLastWeek' | 'userStatusLastMonth' | 'userStatusEmpty', |
|
wasStatus: any, |
|
was_online: number, |
|
expires: number |
|
}>, |
|
|
|
initials?: string, |
|
num?: number, |
|
pFlags: Partial<{verified: boolean, support: boolean, self: boolean, bot: boolean, min: number, deleted: boolean}>, |
|
rFirstName?: string, |
|
rFullName?: string, |
|
rPhone?: string, |
|
sortName?: string, |
|
sortStatus?: number, |
|
}; |
|
|
|
export class AppUsersManager { |
|
public users: {[userID: number]: User} = {}; |
|
public usernames: {[username: string]: number} = {}; |
|
public userAccess: {[userID: number]: string} = {}; |
|
public cachedPhotoLocations: any = {}; |
|
public contactsIndex = searchIndexManager.createIndex(); |
|
public contactsFillPromise: Promise<Set<number>>; |
|
public contactsList: Set<number> = new Set(); |
|
public myID: number; |
|
|
|
public getPeersPromise: Promise<number[]>; |
|
|
|
constructor() { |
|
apiManager.getUserID().then((id) => { |
|
this.myID = id; |
|
}); |
|
|
|
$rootScope.$on('user_auth', (e: CustomEvent) => { |
|
let userAuth = e.detail; |
|
this.myID = userAuth ? userAuth.id : 0; |
|
}); |
|
|
|
setInterval(this.updateUsersStatuses.bind(this), 60000); |
|
|
|
$rootScope.$on('stateSynchronized', this.updateUsersStatuses.bind(this)); |
|
|
|
$rootScope.$on('apiUpdate', (e: CustomEvent) => { |
|
let update = e.detail; |
|
//console.log('on apiUpdate', update); |
|
switch(update._) { |
|
case 'updateUserStatus': |
|
var userID = update.user_id; |
|
var user = this.users[userID]; |
|
if(user) { |
|
user.status = update.status; |
|
if(user.status) { |
|
if(user.status.expires) { |
|
user.status.expires -= serverTimeManager.serverTimeOffset; |
|
} |
|
|
|
if(user.status.was_online) { |
|
user.status.was_online -= serverTimeManager.serverTimeOffset; |
|
} |
|
} |
|
|
|
user.sortStatus = this.getUserStatusForSort(user.status); |
|
$rootScope.$broadcast('user_update', userID); |
|
} //////else console.warn('No user by id:', userID); |
|
break; |
|
|
|
case 'updateUserPhoto': |
|
var userID = update.user_id; |
|
var user = this.users[userID]; |
|
if(user) { |
|
this.forceUserOnline(userID); |
|
if(!user.photo) { |
|
user.photo = update.photo; |
|
} else { |
|
safeReplaceObject(user.photo, update.photo); |
|
} |
|
|
|
if(this.cachedPhotoLocations[userID] !== undefined) { |
|
safeReplaceObject(this.cachedPhotoLocations[userID], update.photo ? |
|
update.photo : {empty: true}); |
|
} |
|
|
|
$rootScope.$broadcast('user_update', userID); |
|
$rootScope.$broadcast('avatar_update', userID); |
|
} else console.warn('No user by id:', userID); |
|
break |
|
|
|
case 'updateContactLink': |
|
this.onContactUpdated(update.user_id, update.my_link._ == 'contactLinkContact'); |
|
break; |
|
} |
|
}); |
|
} |
|
|
|
public fillContacts() { |
|
if(this.contactsFillPromise) { |
|
return this.contactsFillPromise; |
|
} |
|
|
|
return this.contactsFillPromise = apiManager.invokeApi('contacts.getContacts', { |
|
hash: 0 |
|
}).then((result: any) => { |
|
this.saveApiUsers(result.users); |
|
|
|
result.contacts.forEach((contact: any) => { |
|
this.pushContact(contact.user_id); |
|
}); |
|
|
|
return this.contactsList; |
|
}); |
|
} |
|
|
|
public pushContact(userID: number) { |
|
this.contactsList.add(userID); |
|
searchIndexManager.indexObject(userID, this.getUserSearchText(userID), this.contactsIndex); |
|
} |
|
|
|
public getUserSearchText(id: number) { |
|
const user = this.users[id]; |
|
if(!user) { |
|
return ''; |
|
} |
|
|
|
const serviceText = user.pFlags.self ? 'user_name_saved_msgs_raw' : ''; |
|
return (user.first_name || '') + |
|
' ' + (user.last_name || '') + |
|
' ' + (user.phone || '') + |
|
' ' + (user.username || '') + |
|
' ' + serviceText; |
|
} |
|
|
|
public getContacts(query?: string) { |
|
return this.fillContacts().then(_contactsList => { |
|
let contactsList = [..._contactsList]; |
|
if(query) { |
|
const results: any = 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); |
|
}); |
|
|
|
/* 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 resolveUsername(username: string) { |
|
return this.usernames[username] || 0; |
|
} |
|
|
|
public saveApiUsers(apiUsers: any[]) { |
|
apiUsers.forEach((user) => this.saveApiUser(user)); |
|
} |
|
|
|
public saveApiUser(apiUser: any, noReplace?: boolean) { |
|
if(!isObject(apiUser) || |
|
noReplace && isObject(this.users[apiUser.id]) && this.users[apiUser.id].first_name) { |
|
return; |
|
} |
|
|
|
var userID = apiUser.id; |
|
var result = this.users[userID]; |
|
|
|
if(apiUser.pFlags === undefined) { |
|
apiUser.pFlags = {}; |
|
} |
|
|
|
if(apiUser.pFlags.min) { |
|
if(result !== undefined) { |
|
return; |
|
} |
|
} |
|
|
|
if(apiUser.phone) { |
|
apiUser.rPhone = '+' + formatPhoneNumber(apiUser.phone).formatted; |
|
} |
|
|
|
if(apiUser.first_name) { |
|
apiUser.rFirstName = RichTextProcessor.wrapRichText(apiUser.first_name, {noLinks: true, noLinebreaks: true}) |
|
apiUser.rFullName = apiUser.last_name ? RichTextProcessor.wrapRichText(apiUser.first_name + ' ' + (apiUser.last_name || ''), {noLinks: true, noLinebreaks: true}) : apiUser.rFirstName; |
|
} else { |
|
apiUser.rFirstName = RichTextProcessor.wrapRichText(apiUser.last_name, {noLinks: true, noLinebreaks: true}) || apiUser.rPhone || 'user_first_name_deleted'; |
|
apiUser.rFullName = RichTextProcessor.wrapRichText(apiUser.last_name, {noLinks: true, noLinebreaks: true}) || apiUser.rPhone || 'user_name_deleted'; |
|
} |
|
|
|
if(apiUser.username) { |
|
var searchUsername = searchIndexManager.cleanUsername(apiUser.username); |
|
this.usernames[searchUsername] = userID; |
|
} |
|
|
|
apiUser.sortName = apiUser.pFlags.deleted ? '' : searchIndexManager.cleanSearchText(apiUser.first_name + ' ' + (apiUser.last_name || ''), false); |
|
|
|
var nameWords = apiUser.sortName.split(' '); |
|
var firstWord = nameWords.shift(); |
|
var lastWord = nameWords.pop(); |
|
apiUser.initials = firstWord.charAt(0) + (lastWord ? lastWord.charAt(0) : ''); |
|
|
|
if(apiUser.status) { |
|
if(apiUser.status.expires) { |
|
apiUser.status.expires -= serverTimeManager.serverTimeOffset |
|
} |
|
|
|
if(apiUser.status.was_online) { |
|
apiUser.status.was_online -= serverTimeManager.serverTimeOffset |
|
} |
|
} |
|
|
|
if(apiUser.pFlags.bot) { |
|
apiUser.sortStatus = -1; |
|
} else { |
|
apiUser.sortStatus = this.getUserStatusForSort(apiUser.status); |
|
} |
|
|
|
var result = this.users[userID]; |
|
if(result === undefined) { |
|
result = this.users[userID] = apiUser; |
|
} else { |
|
safeReplaceObject(result, apiUser); |
|
} |
|
|
|
$rootScope.$broadcast('user_update', userID); |
|
|
|
if(this.cachedPhotoLocations[userID] !== undefined) { |
|
safeReplaceObject(this.cachedPhotoLocations[userID], apiUser && |
|
apiUser.photo ? apiUser.photo : {empty: true}); |
|
} |
|
} |
|
|
|
public saveUserAccess(id: number, accessHash: string) { |
|
this.userAccess[id] = accessHash; |
|
} |
|
|
|
public getUserStatusForSort(status: User['status']) { |
|
if(status) { |
|
var expires = status.expires || status.was_online; |
|
if(expires) { |
|
return expires; |
|
} |
|
var timeNow = tsNow(true); |
|
switch (status._) { |
|
case 'userStatusRecently': |
|
return timeNow - 86400 * 3; |
|
case 'userStatusLastWeek': |
|
return timeNow - 86400 * 7; |
|
case 'userStatusLastMonth': |
|
return timeNow - 86400 * 30; |
|
} |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
public getUser(id: any): User { |
|
if(isObject(id)) { |
|
return id; |
|
} |
|
|
|
return this.users[id] || {id: id, pFlags: {deleted: true}, access_hash: this.userAccess[id]} as User; |
|
} |
|
|
|
public getSelf() { |
|
return this.getUser(this.myID); |
|
} |
|
|
|
public getUserStatusString(userID: number) { |
|
if(this.isBot(userID)) { |
|
return 'bot'; |
|
} |
|
|
|
const user = this.getUser(userID); |
|
if(!user) { |
|
return ''; |
|
} |
|
|
|
let str = ''; |
|
switch(user.status?._) { |
|
case 'userStatusRecently': { |
|
str = 'last seen recently'; |
|
break; |
|
} |
|
|
|
case 'userStatusLastWeek': { |
|
str = 'last seen last week'; |
|
break; |
|
} |
|
|
|
case 'userStatusLastMonth': { |
|
str = 'last seen last month'; |
|
break; |
|
} |
|
|
|
case 'userStatusOffline': { |
|
str = 'last seen '; |
|
|
|
const date = user.status.was_online; |
|
const now = Date.now() / 1000; |
|
|
|
if((now - date) < 60) { |
|
str += ' just now'; |
|
} else if((now - date) < 3600) { |
|
const c = (now - date) / 60 | 0; |
|
str += c + ' ' + (c == 1 ? 'minute' : 'minutes') + ' ago'; |
|
} else if(now - date < 86400) { |
|
const c = (now - date) / 3600 | 0; |
|
str += c + ' ' + (c == 1 ? 'hour' : 'hours') + ' ago'; |
|
} else { |
|
const d = new Date(date * 1000); |
|
str += ('0' + d.getDate()).slice(-2) + '.' + ('0' + (d.getMonth() + 1)).slice(-2) + ' at ' + |
|
('0' + d.getHours()).slice(-2) + ':' + ('0' + d.getMinutes()).slice(-2); |
|
} |
|
|
|
break; |
|
} |
|
|
|
case 'userStatusOnline': { |
|
str = 'online'; |
|
break; |
|
} |
|
|
|
default: { |
|
str = 'last seen a long time ago'; |
|
break; |
|
} |
|
} |
|
|
|
return str; |
|
} |
|
|
|
public isBot(id: number) { |
|
return this.users[id] && this.users[id].pFlags.bot; |
|
} |
|
|
|
public hasUser(id: number, allowMin?: boolean) { |
|
var user = this.users[id]; |
|
return isObject(user) && (allowMin || !user.pFlags.min); |
|
} |
|
|
|
public getUserPhoto(id: number) { |
|
var user = this.getUser(id); |
|
|
|
if(this.cachedPhotoLocations[id] === undefined) { |
|
this.cachedPhotoLocations[id] = user && user.photo ? user.photo : {empty: true}; |
|
} |
|
|
|
return this.cachedPhotoLocations[id]; |
|
} |
|
|
|
public getUserString(id: number) { |
|
var user = this.getUser(id); |
|
return 'u' + id + (user.access_hash ? '_' + user.access_hash : ''); |
|
} |
|
|
|
public getUserInput(id: number) { |
|
var user = this.getUser(id); |
|
if(user.pFlags && user.pFlags.self) { |
|
return {_: 'inputUserSelf'}; |
|
} |
|
|
|
return { |
|
_: 'inputUser', |
|
user_id: id, |
|
access_hash: user.access_hash || 0 |
|
}; |
|
} |
|
|
|
public updateUsersStatuses() { |
|
var timestampNow = tsNow(true); |
|
for(let i in this.users) { |
|
let user = this.users[i]; |
|
|
|
if(user.status && |
|
user.status._ == 'userStatusOnline' && |
|
user.status.expires < timestampNow) { |
|
|
|
user.status = user.status.wasStatus || {_: 'userStatusOffline', was_online: user.status.expires}; |
|
delete user.status.wasStatus; |
|
$rootScope.$broadcast('user_update', user.id); |
|
} |
|
} |
|
} |
|
|
|
public forceUserOnline(id: number) { |
|
if(this.isBot(id)) { |
|
return; |
|
} |
|
|
|
var user = this.getUser(id); |
|
if(user && |
|
user.status && |
|
user.status._ != 'userStatusOnline' && |
|
user.status._ != 'userStatusEmpty') { |
|
var wasStatus; |
|
if(user.status._ != 'userStatusOffline') { |
|
delete user.status.wasStatus |
|
wasStatus = copy(user.status); |
|
} |
|
|
|
user.status = { |
|
_: 'userStatusOnline', |
|
expires: tsNow(true) + 60, |
|
wasStatus: wasStatus |
|
}; |
|
user.sortStatus = this.getUserStatusForSort(user.status); |
|
$rootScope.$broadcast('user_update', id); |
|
} |
|
} |
|
|
|
/* 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<number[]> { |
|
if(this.getPeersPromise) return this.getPeersPromise; |
|
|
|
return this.getPeersPromise = appStateManager.getState().then((state) => { |
|
if(state?.topPeers?.length) { |
|
return state.topPeers; |
|
} |
|
|
|
return apiManager.invokeApi('contacts.getTopPeers', { |
|
flags: 1, |
|
correspondents: true, |
|
offset: 0, |
|
limit: 30, |
|
hash: 0, |
|
}).then((result: any) => { |
|
//console.log(result); |
|
this.saveApiUsers(result.users); |
|
appChatsManager.saveApiChats(result.chats); |
|
|
|
const peerIDs = result.categories[0].peers.map((topPeer: { |
|
_: 'topPeer', |
|
peer: any, |
|
rating: number |
|
}) => { |
|
const peerID = appPeersManager.getPeerID(topPeer.peer); |
|
appStateManager.pushPeer(peerID); |
|
return peerID; |
|
}); |
|
|
|
appStateManager.pushToState('topPeers', peerIDs); |
|
|
|
return peerIDs; |
|
}); |
|
}); |
|
} |
|
|
|
public searchContacts(query: string, limit = 20) { |
|
return apiManager.invokeApi('contacts.search', { |
|
q: query, |
|
limit |
|
}).then((peers: any) => { |
|
//console.log(peers); |
|
this.saveApiUsers(peers.users); |
|
appChatsManager.saveApiChats(peers.chats); |
|
|
|
return peers; |
|
}); |
|
} |
|
|
|
public onContactUpdated(userID: number, isContact: boolean) { |
|
userID = parseInt('' + userID); |
|
|
|
if(Array.isArray(this.contactsList)) { |
|
var curPos = this.contactsList.indexOf(userID); |
|
var curIsContact = curPos != -1; |
|
|
|
if(isContact != curIsContact) { |
|
if(isContact) { |
|
this.contactsList.push(userID) |
|
searchIndexManager.indexObject(userID, this.getUserSearchText(userID), this.contactsIndex); |
|
} else { |
|
this.contactsList.splice(curPos, 1); |
|
} |
|
|
|
$rootScope.$broadcast('contacts_update', userID); |
|
} |
|
} |
|
} |
|
|
|
public setUserStatus(userID: number, offline: boolean) { |
|
if(this.isBot(userID)) { |
|
return; |
|
} |
|
|
|
var user = this.users[userID]; |
|
if(user) { |
|
var 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); |
|
} |
|
} |
|
} |
|
|
|
export default new AppUsersManager();
|
|
|