tweb/src/lib/appManagers/appProfileManager.ts
Eduard Kuzmenko 2cb460ff3f some changes
2022-01-16 06:45:41 +04:00

707 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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 { MOUNT_CLASS_TO } from "../../config/debug";
import { tsNow } from "../../helpers/date";
import { numberThousandSplitter } from "../../helpers/number";
import { ChannelParticipantsFilter, ChannelsChannelParticipants, ChannelParticipant, Chat, ChatFull, ChatParticipants, ChatPhoto, ExportedChatInvite, InputChannel, InputFile, SendMessageAction, Update, UserFull } from "../../layer";
import { LangPackKey, i18n } from "../langPack";
//import apiManager from '../mtproto/apiManager';
import apiManager from '../mtproto/mtprotoworker';
import { RichTextProcessor } from "../richtextprocessor";
import rootScope from "../rootScope";
import SearchIndex from "../searchIndex";
import apiUpdatesManager from "./apiUpdatesManager";
import appChatsManager from "./appChatsManager";
import appMessagesIdsManager from "./appMessagesIdsManager";
import appNotificationsManager from "./appNotificationsManager";
import appPeersManager from "./appPeersManager";
import appPhotosManager from "./appPhotosManager";
import appUsersManager, { MyTopPeer, User } from "./appUsersManager";
export type UserTyping = Partial<{userId: UserId, action: SendMessageAction, timeout: number}>;
export class AppProfileManager {
//private botInfos: any = {};
public usersFull: {[id: UserId]: UserFull.userFull} = {};
public chatsFull: {[id: ChatId]: ChatFull} = {};
private typingsInPeer: {[peerId: PeerId]: UserTyping[]};
constructor() {
rootScope.addMultipleEventsListeners({
updateChatParticipants: (update) => {
const participants = update.participants;
if(participants._ === 'chatParticipants') {
const chatId = participants.chat_id;
const chatFull = this.chatsFull[chatId] as ChatFull.chatFull;
if(chatFull !== undefined) {
chatFull.participants = participants;
rootScope.dispatchEvent('chat_full_update', chatId);
}
}
},
updateChatParticipantAdd: (update) => {
const chatFull = this.chatsFull[update.chat_id] as ChatFull.chatFull;
if(chatFull !== undefined) {
const _participants = chatFull.participants as ChatParticipants.chatParticipants;
const participants = _participants.participants || [];
for(let i = 0, length = participants.length; i < length; i++) {
if(participants[i].user_id === update.user_id) {
return;
}
}
participants.push({
_: 'chatParticipant',
user_id: update.user_id,
inviter_id: update.inviter_id,
date: tsNow(true)
});
_participants.version = update.version;
rootScope.dispatchEvent('chat_full_update', update.chat_id);
}
},
updateChatParticipantDelete: (update) => {
const chatFull = this.chatsFull[update.chat_id] as ChatFull.chatFull;
if(chatFull !== undefined) {
const _participants = chatFull.participants as ChatParticipants.chatParticipants;
const participants = _participants.participants || [];
for(let i = 0, length = participants.length; i < length; i++) {
if(participants[i].user_id === update.user_id) {
participants.splice(i, 1);
_participants.version = update.version;
rootScope.dispatchEvent('chat_full_update', update.chat_id);
return;
}
}
}
},
updateUserTyping: this.onUpdateUserTyping,
updateChatUserTyping: this.onUpdateUserTyping,
updateChannelUserTyping: this.onUpdateUserTyping,
updatePeerBlocked: this.onUpdatePeerBlocked
});
rootScope.addEventListener('chat_update', (chatId) => {
const fullChat = this.chatsFull[chatId];
const chat: Chat.chat | Chat.channel | Chat.chatForbidden | Chat.channelForbidden = appChatsManager.getChat(chatId);
if(!fullChat || !chat) {
return;
}
let updated = false;
if(!!fullChat.call !== !!(chat as Chat.chat).pFlags?.call_active) {
updated = true;
}
const {photo} = chat as Chat.chat;
if(photo) {
const hasChatPhoto = photo._ !== 'chatPhotoEmpty';
const hasFullChatPhoto = fullChat.chat_photo?._ !== 'photoEmpty';
if(hasChatPhoto !== hasFullChatPhoto || (photo as ChatPhoto.chatPhoto).photo_id !== fullChat.chat_photo?.id) {
updated = true;
}
}
if(updated) {
this.refreshFullPeer(chatId.toPeerId(true));
}
});
// * genius
rootScope.addEventListener('chat_full_update', (chatId) => {
rootScope.dispatchEvent('peer_full_update', chatId.toPeerId(true));
});
// * genius
rootScope.addEventListener('user_full_update', (userId) => {
rootScope.dispatchEvent('peer_full_update', userId.toPeerId(false));
});
rootScope.addEventListener('invalidate_participants', (chatId) => {
this.invalidateChannelParticipants(chatId);
});
this.typingsInPeer = {};
}
/* public saveBotInfo(botInfo: any) {
const botId = botInfo && botInfo.user_id;
if(!botId) {
return null;
}
const commands: any = {};
botInfo.commands.forEach((botCommand: any) => {
commands[botCommand.command] = botCommand.description;
});
return this.botInfos[botId] = {
id: botId,
version: botInfo.version,
shareText: botInfo.share_text,
description: botInfo.description,
commands: commands
};
} */
public getProfile(id: UserId, override?: true): Promise<UserFull> {
if(this.usersFull[id] && !override) {
return Promise.resolve(this.usersFull[id]);
}
return apiManager.invokeApiSingleProcess({
method: 'users.getFullUser',
params: {
id: appUsersManager.getUserInput(id)
},
processResult: (usersUserFull) => {
appChatsManager.saveApiChats(usersUserFull.chats, true);
appUsersManager.saveApiUsers(usersUserFull.users);
const userFull = usersUserFull.full_user;
const peerId = id.toPeerId(false);
if(userFull.profile_photo) {
userFull.profile_photo = appPhotosManager.savePhoto(userFull.profile_photo, {type: 'profilePhoto', peerId});
}
if(userFull.about !== undefined) {
userFull.rAbout = RichTextProcessor.wrapRichText(userFull.about, {noLinebreaks: true});
}
appNotificationsManager.savePeerSettings({
peerId,
settings: userFull.notify_settings
});
this.usersFull[id] = userFull;
/* if(userFull.bot_info) {
userFull.bot_info = this.saveBotInfo(userFull.bot_info) as any;
} */
//appMessagesManager.savePinnedMessage(id, userFull.pinned_msg_id);
rootScope.dispatchEvent('user_full_update', id);
return userFull;
}
});
}
public getProfileByPeerId(peerId: PeerId, override?: true): Promise<ChatFull.chatFull | ChatFull.channelFull | UserFull.userFull> {
if(appPeersManager.isAnyChat(peerId)) return this.getChatFull(peerId.toChatId(), override);
else return this.getProfile(peerId.toUserId(), override);
}
public getCachedFullChat(chatId: ChatId) {
return this.chatsFull[chatId];
}
public getCachedFullUser(userId: UserId) {
return this.usersFull[userId];
}
public getCachedProfileByPeerId(peerId: PeerId) {
return peerId.isUser() ? this.getCachedFullUser(peerId.toUserId()) : this.getCachedFullChat(peerId.toChatId());
}
public getFullPhoto(peerId: PeerId) {
return this.getProfileByPeerId(peerId).then(profile => {
switch(profile._) {
case 'userFull':
return profile.profile_photo;
case 'channelFull':
case 'chatFull':
return profile.chat_photo;
}
});
}
/* public getPeerBots(peerId: PeerId) {
var peerBots: any[] = [];
if(peerId >= 0 && !appUsersManager.isBot(peerId) ||
(appPeersManager.isChannel(peerId) && !appPeersManager.isMegagroup(peerId))) {
return Promise.resolve(peerBots);
}
if(peerId >= 0) {
return this.getProfile(peerId).then((userFull: any) => {
var botInfo = userFull.bot_info;
if(botInfo && botInfo._ !== 'botInfoEmpty') {
peerBots.push(botInfo);
}
return peerBots;
});
}
return this.getChatFull(peerId.toChatId()).then((chatFull: any) => {
chatFull.bot_info.forEach((botInfo: any) => {
peerBots.push(this.saveBotInfo(botInfo))
});
return peerBots;
});
} */
public getChatFull(id: ChatId, override?: true): Promise<ChatFull.chatFull | ChatFull.channelFull> {
if(appChatsManager.isChannel(id)) {
return this.getChannelFull(id, override);
}
const fullChat = this.chatsFull[id] as ChatFull.chatFull;
if(fullChat && !override) {
const chat = appChatsManager.getChat(id);
if(chat.version === (fullChat.participants as ChatParticipants.chatParticipants).version ||
chat.pFlags.left) {
return Promise.resolve(fullChat);
}
}
return apiManager.invokeApiSingleProcess({
method: 'messages.getFullChat',
params: {
chat_id: id
},
processResult: (result) => {
appChatsManager.saveApiChats(result.chats, true);
appUsersManager.saveApiUsers(result.users);
const fullChat = result.full_chat as ChatFull.chatFull;
const peerId = id.toPeerId(true);
if(fullChat && fullChat.chat_photo && fullChat.chat_photo.id) {
fullChat.chat_photo = appPhotosManager.savePhoto(fullChat.chat_photo, {type: 'profilePhoto', peerId});
}
//appMessagesManager.savePinnedMessage(peerId, fullChat.pinned_msg_id);
appNotificationsManager.savePeerSettings({
peerId,
settings: fullChat.notify_settings
});
this.chatsFull[id] = fullChat;
rootScope.dispatchEvent('chat_full_update', id);
return fullChat;
}
});
}
public getChatInviteLink(id: ChatId, force?: boolean) {
return this.getChatFull(id).then((chatFull) => {
if(!force &&
chatFull.exported_invite &&
chatFull.exported_invite._ == 'chatInviteExported') {
return chatFull.exported_invite.link;
}
return apiManager.invokeApi('messages.exportChatInvite', {
peer: appPeersManager.getInputPeerById(id.toPeerId(true))
}).then((exportedInvite) => {
if(this.chatsFull[id] !== undefined) {
this.chatsFull[id].exported_invite = exportedInvite;
}
return (exportedInvite as ExportedChatInvite.chatInviteExported).link;
});
});
}
public getChannelParticipants(id: ChatId, filter: ChannelParticipantsFilter = {_: 'channelParticipantsRecent'}, limit = 200, offset = 0) {
if(filter._ === 'channelParticipantsRecent') {
const chat = appChatsManager.getChat(id);
if(chat &&
chat.pFlags && (
chat.pFlags.kicked ||
chat.pFlags.broadcast && !chat.pFlags.creator && !chat.admin_rights
)) {
return Promise.reject();
}
}
return apiManager.invokeApiCacheable('channels.getParticipants', {
channel: appChatsManager.getChannelInput(id),
filter,
offset,
limit,
hash: '0'
}, {cacheSeconds: 60}).then(result => {
appUsersManager.saveApiUsers((result as ChannelsChannelParticipants.channelsChannelParticipants).users);
return result as ChannelsChannelParticipants.channelsChannelParticipants;
});
/* let maybeAddSelf = (participants: any[]) => {
let chat = appChatsManager.getChat(id);
let selfMustBeFirst = filter._ === 'channelParticipantsRecent' &&
!offset &&
!chat.pFlags.kicked &&
!chat.pFlags.left;
if(selfMustBeFirst) {
participants = copy(participants);
let myID = appUsersManager.getSelf().id;
let myIndex = participants.findIndex(p => p.user_id === myID);
let myParticipant;
if(myIndex !== -1) {
myParticipant = participants[myIndex];
participants.splice(myIndex, 1);
} else {
myParticipant = {_: 'channelParticipantSelf', user_id: myID};
}
participants.unshift(myParticipant);
}
return participants;
} */
}
public getChannelParticipant(id: ChatId, peerId: PeerId) {
return apiManager.invokeApiSingle('channels.getParticipant', {
channel: appChatsManager.getChannelInput(id),
participant: appPeersManager.getInputPeerById(peerId),
}).then(channelParticipant => {
appUsersManager.saveApiUsers(channelParticipant.users);
return channelParticipant.participant;
});
}
public getChannelFull(id: ChatId, override?: true): Promise<ChatFull.channelFull> {
if(this.chatsFull[id] !== undefined && !override) {
return Promise.resolve(this.chatsFull[id] as ChatFull.channelFull);
}
return apiManager.invokeApiSingleProcess({
method: 'channels.getFullChannel',
params: {
channel: appChatsManager.getChannelInput(id)
},
processResult: (result) => {
const peerId = id.toPeerId(true);
appChatsManager.saveApiChats(result.chats, true);
appUsersManager.saveApiUsers(result.users);
const fullChannel = result.full_chat as ChatFull.channelFull;
if(fullChannel && fullChannel.chat_photo.id) {
fullChannel.chat_photo = appPhotosManager.savePhoto(fullChannel.chat_photo, {type: 'profilePhoto', peerId});
//appPhotosManager.savePhoto(fullChannel.chat_photo);
}
appNotificationsManager.savePeerSettings({
peerId,
settings: fullChannel.notify_settings
});
this.chatsFull[id] = fullChannel;
rootScope.dispatchEvent('chat_full_update', id);
return fullChannel;
},
processError: (error) => {
switch(error.type) {
case 'CHANNEL_PRIVATE':
let channel = appChatsManager.getChat(id);
channel = {_: 'channelForbidden', access_hash: channel.access_hash, title: channel.title};
apiUpdatesManager.processUpdateMessage({
_: 'updates',
updates: [{
_: 'updateChannel',
channel_id: id
} as Update.updateChannel],
chats: [channel],
users: []
});
break;
}
throw error;
}
});
}
public getMentions(chatId: ChatId, query: string, threadId?: number): Promise<PeerId[]> {
const processUserIds = (topPeers: MyTopPeer[]) => {
const startsWithAt = query.charAt(0) === '@';
if(startsWithAt) query = query.slice(1);
/* const startsWithAt = query.charAt(0) === '@';
if(startsWithAt) query = query.slice(1);
const index = new SearchIndex<number>(!startsWithAt, !startsWithAt); */
const index = new SearchIndex<PeerId>({
ignoreCase: true
});
const ratingMap: Map<PeerId, number> = new Map();
topPeers.forEach(peer => {
index.indexObject(peer.id, appUsersManager.getUserSearchText(peer.id));
ratingMap.set(peer.id, peer.rating);
});
const peerIds = Array.from(index.search(query));
peerIds.sort((a, b) => ratingMap.get(b) - ratingMap.get(a));
return peerIds;
};
let promise: Promise<PeerId[]>;
if(appChatsManager.isChannel(chatId)) {
promise = this.getChannelParticipants(chatId, {
_: 'channelParticipantsMentions',
q: query,
top_msg_id: appMessagesIdsManager.getServerMessageId(threadId)
}, 50, 0).then(cP => {
return cP.participants.map(p => appChatsManager.getParticipantPeerId(p));
});
} else if(chatId) {
promise = (this.getChatFull(chatId) as Promise<ChatFull.chatFull>).then(chatFull => {
return (chatFull.participants as ChatParticipants.chatParticipants).participants.map(p => p.user_id.toPeerId());
});
} else {
promise = Promise.resolve([]);
}
return Promise.all([
// [],
appUsersManager.getTopPeers('bots_inline').catch(() => [] as MyTopPeer[]),
promise
]).then(results => {
const peers = results[0].concat(results[1].map(peerId => ({id: peerId, rating: 0})));
return processUserIds(peers);
});
}
public invalidateChannelParticipants(id: ChatId) {
apiManager.clearCache('channels.getParticipants', (params) => (params.channel as InputChannel.inputChannel).channel_id === id);
this.refreshFullPeer(id.toPeerId(true));
}
private refreshFullPeer(peerId: PeerId) {
if(peerId.isUser()) delete this.usersFull[peerId.toUserId()];
else delete this.chatsFull[peerId.toChatId()];
rootScope.dispatchEvent('peer_full_update', peerId);
// ! эта строчка будет создавать race condition:
// ! запрос вернёт chat с установленным флагом call_not_empty, хотя сам апдейт уже будет применён
// this.getProfileByPeerId(peerId, true);
}
public updateProfile(first_name: string, last_name: string, about: string) {
return apiManager.invokeApi('account.updateProfile', {
first_name,
last_name,
about
}).then(user => {
appUsersManager.saveApiUser(user);
return this.getProfile(rootScope.myId, true);
});
}
public uploadProfilePhoto(inputFile: InputFile) {
return apiManager.invokeApi('photos.uploadProfilePhoto', {
file: inputFile
}).then((updateResult) => {
appUsersManager.saveApiUsers(updateResult.users);
const myId = rootScope.myId;
appPhotosManager.savePhoto(updateResult.photo, {
type: 'profilePhoto',
peerId: myId
});
const userId = myId.toUserId();
apiUpdatesManager.processLocalUpdate({
_: 'updateUserPhoto',
user_id: userId,
date: tsNow(true),
photo: appUsersManager.getUser(userId).photo,
previous: true
});
});
}
public deletePhotos(photoIds: string[]) {
return apiManager.invokeApiSingle('photos.deletePhotos', {
id: photoIds.map(photoId => {
const photo = appPhotosManager.getPhoto(photoId);
return appPhotosManager.getInput(photo);
})
}).then((deletedList) => {
});
}
public getChatMembersString(chatId: ChatId) {
const chat: Chat = appChatsManager.getChat(chatId);
if(chat._ === 'chatForbidden') {
return i18n('YouWereKicked');
}
const chatFull = this.chatsFull[chatId];
let count: number;
if(chatFull) {
if(chatFull._ === 'channelFull') {
count = chatFull.participants_count;
} else {
count = (chatFull.participants as ChatParticipants.chatParticipants).participants?.length;
}
} else {
count = (chat as Chat.chat).participants_count || (chat as any).participants?.participants.length;
}
const isChannel = appChatsManager.isBroadcast(chatId);
count = count || 1;
let key: LangPackKey = isChannel ? 'Peer.Status.Subscribers' : 'Peer.Status.Member';
return i18n(key, [numberThousandSplitter(count)]);
}
private verifyParticipantForOnlineCount(participant: {user_id: UserId}) {
const user = appUsersManager.getUser(participant.user_id);
return !!(user && user.status && user.status._ === 'userStatusOnline');
}
private reduceParticipantsForOnlineCount(participants: {user_id: UserId}[]) {
return participants.reduce((acc, participant) => {
return acc + +this.verifyParticipantForOnlineCount(participant);
}, 0);
}
public async getOnlines(id: ChatId): Promise<number> {
const minOnline = 1;
if(appChatsManager.isBroadcast(id)) {
return minOnline;
}
const chatInfo = await this.getChatFull(id);
if(appChatsManager.isMegagroup(id)) {
if((chatInfo as ChatFull.channelFull).participants_count <= 100) {
const channelParticipants = await this.getChannelParticipants(id, {_: 'channelParticipantsRecent'}, 100);
return this.reduceParticipantsForOnlineCount(channelParticipants.participants as ChannelParticipant.channelParticipant[]);
}
const res = await apiManager.invokeApiCacheable('messages.getOnlines', {
peer: appChatsManager.getChannelInputPeer(id)
}, {cacheSeconds: 60});
const onlines = res.onlines ?? minOnline;
return onlines;
}
const _participants = (chatInfo as ChatFull.chatFull).participants as ChatParticipants.chatParticipants;
if(_participants?.participants) {
return this.reduceParticipantsForOnlineCount(_participants.participants);
} else {
return minOnline;
}
}
private onUpdateUserTyping = (update: Update.updateUserTyping | Update.updateChatUserTyping | Update.updateChannelUserTyping) => {
const fromId = (update as Update.updateUserTyping).user_id ?
(update as Update.updateUserTyping).user_id.toPeerId() :
appPeersManager.getPeerId((update as Update.updateChatUserTyping).from_id);
if(rootScope.myId === fromId || update.action._ === 'speakingInGroupCallAction') {
return;
}
const peerId = appPeersManager.getPeerId(update);
const typings = this.typingsInPeer[peerId] ?? (this.typingsInPeer[peerId] = []);
let typing = typings.find(t => t.userId === fromId);
const cancelAction = () => {
delete typing.timeout;
//typings.findAndSplice(t => t === typing);
const idx = typings.indexOf(typing);
if(idx !== -1) {
typings.splice(idx, 1);
}
rootScope.dispatchEvent('peer_typings', {peerId, typings});
if(!typings.length) {
delete this.typingsInPeer[peerId];
}
};
if(typing && typing.timeout !== undefined) {
clearTimeout(typing.timeout);
}
if(update.action._ === 'sendMessageCancelAction') {
if(!typing) {
return;
}
cancelAction();
return;
}
if(!typing) {
typing = {
userId: fromId
};
typings.push(typing);
}
//console.log('updateChatUserTyping', update, typings);
typing.action = update.action;
const hasUser = appUsersManager.hasUser(fromId);
if(!hasUser) {
// let's load user here
if(update._ === 'updateChatUserTyping') {
if(update.chat_id && appChatsManager.hasChat(update.chat_id) && !appChatsManager.isChannel(update.chat_id)) {
appProfileManager.getChatFull(update.chat_id).then(() => {
if(typing.timeout !== undefined && appUsersManager.hasUser(fromId)) {
rootScope.dispatchEvent('peer_typings', {peerId, typings});
}
});
}
}
//return;
} else {
appUsersManager.forceUserOnline(fromId);
}
typing.timeout = window.setTimeout(cancelAction, 6000);
if(hasUser) {
rootScope.dispatchEvent('peer_typings', {peerId, typings});
}
};
private onUpdatePeerBlocked = (update: Update.updatePeerBlocked) => {
const peerId = appPeersManager.getPeerId(update.peer_id);
if(appPeersManager.isUser(peerId)) {
const userId = peerId.toUserId();
const userFull = this.usersFull[userId];
if(userFull) {
if(update.blocked) userFull.pFlags.blocked = true;
else delete userFull.pFlags.blocked;
}
rootScope.dispatchEvent('user_full_update', userId);
}
rootScope.dispatchEvent('peer_block', {peerId, blocked: update.blocked});
};
public getPeerTypings(peerId: PeerId) {
return this.typingsInPeer[peerId];
}
}
const appProfileManager = new AppProfileManager();
MOUNT_CLASS_TO.appProfileManager = appProfileManager;
export default appProfileManager;