import type BigInt from 'big-integer'; import { Api as GramJs } from '../../../lib/gramjs'; import type { OnApiUpdate, ApiChat, ApiMessage, ApiUser, ApiMessageEntity, ApiFormattedText, ApiChatFullInfo, ApiChatFolder, ApiChatBannedRights, ApiChatAdminRights, ApiGroupCall, ApiUserStatus, } from '../../types'; import { DEBUG, ARCHIVED_FOLDER_ID, MEMBERS_LOAD_SLICE, SERVICE_NOTIFICATIONS_USER_ID, } from '../../../config'; import { invokeRequest, uploadFile } from './client'; import { buildApiChatFromDialog, getPeerKey, buildChatMembers, buildApiChatFromPreview, buildApiChatFolder, buildApiChatFolderFromSuggested, buildApiChatBotCommands, buildApiChatSettings, } from '../apiBuilders/chats'; import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages'; import { buildApiUser, buildApiUsersAndStatuses } from '../apiBuilders/users'; import { buildCollectionByKey } from '../../../util/iteratees'; import { buildInputEntity, buildInputPeer, buildMtpMessageEntity, buildFilterFromApiFolder, isMessageWithMedia, buildChatBannedRights, buildChatAdminRights, } from '../gramjsBuilders'; import { addEntitiesWithPhotosToLocalDb, addMessageToLocalDb, addPhotoToLocalDb } from '../helpers'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; import { buildApiPhoto } from '../apiBuilders/common'; import { buildStickerSet } from '../apiBuilders/symbols'; type FullChatData = { fullInfo: ApiChatFullInfo; users?: ApiUser[]; userStatusesById: { [userId: string]: ApiUserStatus }; groupCall?: Partial; membersCount?: number; }; const MAX_INT_32 = 2 ** 31 - 1; let onUpdate: OnApiUpdate; export function init(_onUpdate: OnApiUpdate) { onUpdate = _onUpdate; } export async function fetchChats({ limit, offsetDate, archived, withPinned, serverTimeOffset, lastLocalServiceMessage, }: { limit: number; offsetDate?: number; archived?: boolean; withPinned?: boolean; serverTimeOffset: number; lastLocalServiceMessage?: ApiMessage; }) { const result = await invokeRequest(new GramJs.messages.GetDialogs({ offsetPeer: new GramJs.InputPeerEmpty(), limit, offsetDate, folderId: archived ? ARCHIVED_FOLDER_ID : undefined, ...(withPinned && { excludePinned: true }), })); const resultPinned = withPinned ? await invokeRequest(new GramJs.messages.GetPinnedDialogs({ folderId: archived ? ARCHIVED_FOLDER_ID : undefined, })) : undefined; if (!result || result instanceof GramJs.messages.DialogsNotModified) { return undefined; } if (resultPinned) { updateLocalDb(resultPinned); } updateLocalDb(result); const lastMessagesByChatId = buildCollectionByKey( (resultPinned ? resultPinned.messages : []).concat(result.messages) .map(buildApiMessage) .filter(Boolean as any), 'chatId', ); const peersByKey: Record = { ...(resultPinned && preparePeers(resultPinned)), ...preparePeers(result), }; const chats: ApiChat[] = []; const draftsById: Record = {}; const replyingToById: Record = {}; const dialogs = (resultPinned ? resultPinned.dialogs : []).concat(result.dialogs); const orderedPinnedIds: string[] = []; dialogs.forEach((dialog) => { if ( !(dialog instanceof GramJs.Dialog) // This request can return dialogs not belonging to specified folder || (!archived && dialog.folderId === ARCHIVED_FOLDER_ID) || (archived && dialog.folderId !== ARCHIVED_FOLDER_ID) ) { return; } const peerEntity = peersByKey[getPeerKey(dialog.peer)]; const chat = buildApiChatFromDialog(dialog, peerEntity, serverTimeOffset); if ( chat.id === SERVICE_NOTIFICATIONS_USER_ID && lastLocalServiceMessage && (!lastMessagesByChatId[chat.id] || lastLocalServiceMessage.date > lastMessagesByChatId[chat.id].date) ) { chat.lastMessage = lastLocalServiceMessage; } else { chat.lastMessage = lastMessagesByChatId[chat.id]; } chat.isListed = true; chats.push(chat); if (withPinned && dialog.pinned) { orderedPinnedIds.push(chat.id); } if (dialog.draft) { const { formattedText, replyingToId } = buildMessageDraft(dialog.draft) || {}; if (formattedText) { draftsById[chat.id] = formattedText; } if (replyingToId) { replyingToById[chat.id] = replyingToId; } } }); const chatIds = chats.map((chat) => chat.id); const { users, userStatusesById } = buildApiUsersAndStatuses((resultPinned?.users || []).concat(result.users)); let totalChatCount: number; if (result instanceof GramJs.messages.DialogsSlice) { totalChatCount = result.count; } else { totalChatCount = chatIds.length; } return { chatIds, chats, users, userStatusesById, draftsById, replyingToById, orderedPinnedIds: withPinned ? orderedPinnedIds : undefined, totalChatCount, }; } export function fetchFullChat(chat: ApiChat) { const { id, accessHash, adminRights } = chat; const input = buildInputEntity(id, accessHash); return input instanceof GramJs.InputChannel ? getFullChannelInfo(id, accessHash!, adminRights) : getFullChatInfo(id); } export async function fetchChatSettings(chat: ApiChat) { const { id, accessHash } = chat; const result = await invokeRequest(new GramJs.messages.GetPeerSettings({ peer: buildInputPeer(id, accessHash), })); if (!result) { return undefined; } return buildApiChatSettings(result.settings); } export async function searchChats({ query }: { query: string }) { const result = await invokeRequest(new GramJs.contacts.Search({ q: query })); if (!result) { return undefined; } updateLocalDb(result); const localPeerIds = result.myResults.map(getApiChatIdFromMtpPeer); const allChats = result.chats.concat(result.users) .map((user) => buildApiChatFromPreview(user)) .filter(Boolean as any); const allUsers = result.users.map(buildApiUser).filter((user) => Boolean(user) && !user.isSelf) as ApiUser[]; return { localChats: allChats.filter((r) => localPeerIds.includes(r.id)), localUsers: allUsers.filter((u) => localPeerIds.includes(u.id)), globalChats: allChats.filter((r) => !localPeerIds.includes(r.id)), globalUsers: allUsers.filter((u) => !localPeerIds.includes(u.id)), }; } export async function fetchChat({ type, user, }: { type: 'user' | 'self' | 'support'; user?: ApiUser; }) { let mtpUser: GramJs.User | undefined; if (type === 'self' || type === 'user') { const result = await invokeRequest(new GramJs.users.GetUsers({ id: [ type === 'user' && user ? buildInputEntity(user.id, user.accessHash) as GramJs.InputUser : new GramJs.InputUserSelf(), ], })); if (!result || !result.length) { return undefined; } [mtpUser] = result; } else if (type === 'support') { const result = await invokeRequest(new GramJs.help.GetSupport()); if (!result || !result.user) { return undefined; } mtpUser = result.user; } const chat = buildApiChatFromPreview(mtpUser!, type === 'support'); if (!chat) { return undefined; } onUpdate({ '@type': 'updateChat', id: chat.id, chat, }); return { chatId: chat.id }; } export async function requestChatUpdate({ chat, serverTimeOffset, lastLocalMessage, noLastMessage, }: { chat: ApiChat; serverTimeOffset: number; lastLocalMessage?: ApiMessage; noLastMessage?: boolean; }) { const { id, accessHash } = chat; const result = await invokeRequest(new GramJs.messages.GetPeerDialogs({ peers: [new GramJs.InputDialogPeer({ peer: buildInputPeer(id, accessHash), })], })); if (!result) { return; } const dialog = result.dialogs[0]; if (!dialog || !(dialog instanceof GramJs.Dialog)) { return; } const peersByKey = preparePeers(result); const peerEntity = peersByKey[getPeerKey(dialog.peer)]; if (!peerEntity) { return; } updateLocalDb(result); const lastRemoteMessage = buildApiMessage(result.messages[0]); const lastMessage = lastLocalMessage && (!lastRemoteMessage || (lastLocalMessage.date > lastRemoteMessage.date)) ? lastLocalMessage : lastRemoteMessage; onUpdate({ '@type': 'updateChat', id, chat: { ...buildApiChatFromDialog(dialog, peerEntity, serverTimeOffset), ...(!noLastMessage && { lastMessage }), }, }); } export function saveDraft({ chat, text, entities, replyToMsgId, }: { chat: ApiChat; text: string; entities?: ApiMessageEntity[]; replyToMsgId?: number; }) { return invokeRequest(new GramJs.messages.SaveDraft({ peer: buildInputPeer(chat.id, chat.accessHash), message: text, ...(entities && { entities: entities.map(buildMtpMessageEntity), }), replyToMsgId, })); } export function clearDraft(chat: ApiChat) { return invokeRequest(new GramJs.messages.SaveDraft({ peer: buildInputPeer(chat.id, chat.accessHash), message: '', })); } async function getFullChatInfo(chatId: string): Promise { const result = await invokeRequest(new GramJs.messages.GetFullChat({ chatId: buildInputEntity(chatId) as BigInt.BigInteger, })); if (!result || !(result.fullChat instanceof GramJs.ChatFull)) { return undefined; } updateLocalDb(result); const { about, participants, exportedInvite, botInfo, call, availableReactions, recentRequesters, requestsPending, } = result.fullChat; const members = buildChatMembers(participants); const adminMembers = members ? members.filter(({ isAdmin, isOwner }) => isAdmin || isOwner) : undefined; const botCommands = botInfo ? buildApiChatBotCommands(botInfo) : undefined; const { users, userStatusesById } = buildApiUsersAndStatuses(result.users); return { fullInfo: { about, members, adminMembers, canViewMembers: true, botCommands, ...(exportedInvite && { inviteLink: exportedInvite.link, }), groupCallId: call?.id.toString(), enabledReactions: availableReactions, requestsPending, recentRequesterIds: recentRequesters?.map((userId) => buildApiPeerId(userId, 'user')), }, users, userStatusesById, groupCall: call ? { chatId, isLoaded: false, id: call.id.toString(), accessHash: call.accessHash.toString(), connectionState: 'disconnected', participantsCount: 0, version: 0, participants: {}, } : undefined, membersCount: members?.length, }; } async function getFullChannelInfo( id: string, accessHash: string, adminRights?: ApiChatAdminRights, ): Promise { const result = await invokeRequest(new GramJs.channels.GetFullChannel({ channel: buildInputEntity(id, accessHash) as GramJs.InputChannel, })); if (!result || !(result.fullChat instanceof GramJs.ChannelFull)) { return undefined; } const { about, onlineCount, exportedInvite, slowmodeSeconds, slowmodeNextSendDate, migratedFromChatId, migratedFromMaxId, canViewParticipants, canViewStats, linkedChatId, hiddenPrehistory, call, botInfo, availableReactions, defaultSendAs, requestsPending, recentRequesters, statsDc, participantsCount, stickerset, } = result.fullChat; const inviteLink = exportedInvite instanceof GramJs.ChatInviteExported ? exportedInvite.link : undefined; const { members, users, userStatusesById } = (canViewParticipants && await fetchMembers(id, accessHash)) || {}; const { members: kickedMembers, users: bannedUsers, userStatusesById: bannedStatusesById } = ( canViewParticipants && adminRights && await fetchMembers(id, accessHash, 'kicked') ) || {}; const { members: adminMembers, users: adminUsers, userStatusesById: adminStatusesById } = ( canViewParticipants && adminRights && await fetchMembers(id, accessHash, 'admin') ) || {}; const botCommands = botInfo ? buildApiChatBotCommands(botInfo) : undefined; if (result?.chats?.length > 1) { updateLocalDb(result); const [, mtpLinkedChat] = result.chats; const chat = buildApiChatFromPreview(mtpLinkedChat, undefined, true); if (chat) { onUpdate({ '@type': 'updateChat', id: chat.id, chat, }); } } const statusesById = { ...userStatusesById, ...bannedStatusesById, ...adminStatusesById, }; return { fullInfo: { about, onlineCount, inviteLink, slowMode: slowmodeSeconds ? { seconds: slowmodeSeconds, nextSendDate: slowmodeNextSendDate, } : undefined, migratedFrom: migratedFromChatId ? { chatId: buildApiPeerId(migratedFromChatId, 'chat'), maxMessageId: migratedFromMaxId, } : undefined, canViewMembers: canViewParticipants, canViewStatistics: canViewStats, isPreHistoryHidden: hiddenPrehistory, members, kickedMembers, adminMembers, groupCallId: call ? String(call.id) : undefined, linkedChatId: linkedChatId ? buildApiPeerId(linkedChatId, 'chat') : undefined, botCommands, enabledReactions: availableReactions, sendAsId: defaultSendAs ? getApiChatIdFromMtpPeer(defaultSendAs) : undefined, requestsPending, recentRequesterIds: recentRequesters?.map((userId) => buildApiPeerId(userId, 'user')), statisticsDcId: statsDc, stickerSet: stickerset ? buildStickerSet(stickerset) : undefined, }, users: [...(users || []), ...(bannedUsers || []), ...(adminUsers || [])], userStatusesById: statusesById, groupCall: call ? { chatId: id, isLoaded: false, id: call.id.toString(), accessHash: call?.accessHash.toString(), participants: {}, version: 0, participantsCount: 0, connectionState: 'disconnected', } : undefined, membersCount: participantsCount, }; } export async function updateChatMutedState({ chat, isMuted, serverTimeOffset, }: { chat: ApiChat; isMuted: boolean; serverTimeOffset: number; }) { await invokeRequest(new GramJs.account.UpdateNotifySettings({ peer: new GramJs.InputNotifyPeer({ peer: buildInputPeer(chat.id, chat.accessHash), }), settings: new GramJs.InputPeerNotifySettings({ muteUntil: isMuted ? MAX_INT_32 : 0 }), })); onUpdate({ '@type': 'updateNotifyExceptions', chatId: chat.id, isMuted, }); void requestChatUpdate({ chat, serverTimeOffset, noLastMessage: true, }); } export async function createChannel({ title, about = '', users, }: { title: string; about?: string; users?: ApiUser[]; }, noErrorUpdate = false): Promise { const result = await invokeRequest(new GramJs.channels.CreateChannel({ broadcast: true, title, about, })); // `createChannel` can return a lot of different update types according to docs, // but currently channel creation returns only `Updates` type. // Errors are added to catch unexpected cases in future testing if (!(result instanceof GramJs.Updates)) { if (DEBUG) { // eslint-disable-next-line no-console console.error('Unexpected channel creation update', result); } return undefined; } const newChannel = result.chats[0]; if (!newChannel || !(newChannel instanceof GramJs.Channel)) { if (DEBUG) { // eslint-disable-next-line no-console console.error('Created channel not found', result); } return undefined; } const channel = buildApiChatFromPreview(newChannel)!; if (users?.length) { try { await invokeRequest(new GramJs.channels.InviteToChannel({ channel: buildInputEntity(channel.id, channel.accessHash) as GramJs.InputChannel, users: users.map(({ id, accessHash }) => buildInputEntity(id, accessHash)) as GramJs.InputUser[], }), undefined, noErrorUpdate); } catch (err) { // `noErrorUpdate` will cause an exception which we don't want either } } return channel; } export function joinChannel({ channelId, accessHash, }: { channelId: string; accessHash: string; }) { return invokeRequest(new GramJs.channels.JoinChannel({ channel: buildInputEntity(channelId, accessHash) as GramJs.InputChannel, }), true); } export function deleteChatUser({ chat, user, }: { chat: ApiChat; user: ApiUser; }) { if (chat.type !== 'chatTypeBasicGroup') return undefined; return invokeRequest(new GramJs.messages.DeleteChatUser({ chatId: buildInputEntity(chat.id, chat.accessHash) as BigInt.BigInteger, userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser, }), true); } export function deleteChat({ chatId, }: { chatId: string; }) { return invokeRequest(new GramJs.messages.DeleteChat({ chatId: buildInputEntity(chatId) as BigInt.BigInteger, }), true); } export function leaveChannel({ channelId, accessHash, }: { channelId: string; accessHash: string; }) { return invokeRequest(new GramJs.channels.LeaveChannel({ channel: buildInputEntity(channelId, accessHash) as GramJs.InputChannel, }), true); } export function deleteChannel({ channelId, accessHash, }: { channelId: string; accessHash: string; }) { return invokeRequest(new GramJs.channels.DeleteChannel({ channel: buildInputEntity(channelId, accessHash) as GramJs.InputChannel, }), true); } export async function createGroupChat({ title, users, }: { title: string; users: ApiUser[]; }): Promise { const result = await invokeRequest(new GramJs.messages.CreateChat({ title, users: users.map(({ id, accessHash }) => buildInputEntity(id, accessHash)) as GramJs.InputUser[], }), undefined, true); // `createChat` can return a lot of different update types according to docs, // but currently chat creation returns only `Updates` type. // Errors are added to catch unexpected cases in future testing if (!(result instanceof GramJs.Updates)) { if (DEBUG) { // eslint-disable-next-line no-console console.error('Unexpected chat creation update', result); } return undefined; } const newChat = result.chats[0]; if (!newChat || !(newChat instanceof GramJs.Chat)) { if (DEBUG) { // eslint-disable-next-line no-console console.error('Created chat not found', result); } return undefined; } return buildApiChatFromPreview(newChat); } export async function editChatPhoto({ chatId, accessHash, photo, }: { chatId: string; accessHash?: string; photo: File; }) { const uploadedPhoto = await uploadFile(photo); const inputEntity = buildInputEntity(chatId, accessHash); return invokeRequest( inputEntity instanceof GramJs.InputChannel ? new GramJs.channels.EditPhoto({ channel: inputEntity as GramJs.InputChannel, photo: new GramJs.InputChatUploadedPhoto({ file: uploadedPhoto, }), }) : new GramJs.messages.EditChatPhoto({ chatId: inputEntity as BigInt.BigInteger, photo: new GramJs.InputChatUploadedPhoto({ file: uploadedPhoto, }), }), true, ); } export async function toggleChatPinned({ chat, shouldBePinned, }: { chat: ApiChat; shouldBePinned: boolean; }) { const { id, accessHash } = chat; const isActionSuccessful = await invokeRequest(new GramJs.messages.ToggleDialogPin({ peer: new GramJs.InputDialogPeer({ peer: buildInputPeer(id, accessHash), }), pinned: shouldBePinned || undefined, })); if (isActionSuccessful) { onUpdate({ '@type': 'updateChatPinned', id: chat.id, isPinned: shouldBePinned, }); } } export function toggleChatArchived({ chat, folderId, }: { chat: ApiChat; folderId: number; }) { const { id, accessHash } = chat; return invokeRequest(new GramJs.folders.EditPeerFolders({ folderPeers: [new GramJs.InputFolderPeer({ peer: buildInputPeer(id, accessHash), folderId, })], }), true); } export async function fetchChatFolders() { const result = await invokeRequest(new GramJs.messages.GetDialogFilters()); if (!result) { return undefined; } return { byId: buildCollectionByKey(result.map(buildApiChatFolder), 'id') as Record, orderedIds: result.map(({ id }) => id), }; } export async function fetchRecommendedChatFolders() { const results = await invokeRequest(new GramJs.messages.GetSuggestedDialogFilters()); if (!results) { return undefined; } return results.map(buildApiChatFolderFromSuggested); } export async function editChatFolder({ id, folderUpdate, }: { id: number; folderUpdate: ApiChatFolder; }) { const filter = buildFilterFromApiFolder(folderUpdate); const isActionSuccessful = await invokeRequest(new GramJs.messages.UpdateDialogFilter({ id, filter, })); if (isActionSuccessful) { onUpdate({ '@type': 'updateChatFolder', id, folder: folderUpdate, }); } } export async function deleteChatFolder(id: number) { const isActionSuccessful = await invokeRequest(new GramJs.messages.UpdateDialogFilter({ id, filter: undefined, })); const recommendedChatFolders = await fetchRecommendedChatFolders(); if (isActionSuccessful) { onUpdate({ '@type': 'updateChatFolder', id, folder: undefined, }); } if (recommendedChatFolders) { onUpdate({ '@type': 'updateRecommendedChatFolders', folders: recommendedChatFolders, }); } } export async function toggleDialogUnread({ chat, hasUnreadMark, }: { chat: ApiChat; hasUnreadMark: boolean | undefined; }) { const { id, accessHash } = chat; const isActionSuccessful = await invokeRequest(new GramJs.messages.MarkDialogUnread({ peer: new GramJs.InputDialogPeer({ peer: buildInputPeer(id, accessHash), }), unread: hasUnreadMark || undefined, })); if (isActionSuccessful) { onUpdate({ '@type': 'updateChat', id: chat.id, chat: { hasUnreadMark }, }); } } export async function getChatByPhoneNumber(phoneNumber: string) { const result = await invokeRequest(new GramJs.contacts.ResolvePhone({ phone: phoneNumber, })); return processResolvedPeer(result); } export async function getChatByUsername(username: string) { const result = await invokeRequest(new GramJs.contacts.ResolveUsername({ username, })); return processResolvedPeer(result); } function processResolvedPeer(result?: GramJs.contacts.TypeResolvedPeer) { if (!result) { return undefined; } const { users, chats } = result; const chat = chats.length ? buildApiChatFromPreview(chats[0]) : buildApiChatFromPreview(users[0]); if (!chat) { return undefined; } updateLocalDb(result); return chat; } export function togglePreHistoryHidden({ chat, isEnabled, }: { chat: ApiChat; isEnabled: boolean }) { const { id, accessHash } = chat; const channel = buildInputEntity(id, accessHash); return invokeRequest(new GramJs.channels.TogglePreHistoryHidden({ channel: channel as GramJs.InputChannel, enabled: isEnabled, }), true); } export function updateChatDefaultBannedRights({ chat, bannedRights, }: { chat: ApiChat; bannedRights: ApiChatBannedRights }) { const { id, accessHash } = chat; const peer = buildInputPeer(id, accessHash); return invokeRequest(new GramJs.messages.EditChatDefaultBannedRights({ peer, bannedRights: buildChatBannedRights(bannedRights), }), true); } export function updateChatMemberBannedRights({ chat, user, bannedRights, untilDate, }: { chat: ApiChat; user: ApiUser; bannedRights: ApiChatBannedRights; untilDate?: number }) { const channel = buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel; const participant = buildInputPeer(user.id, user.accessHash) as GramJs.InputUser; return invokeRequest(new GramJs.channels.EditBanned({ channel, participant, bannedRights: buildChatBannedRights(bannedRights, untilDate), }), true); } export function updateChatAdmin({ chat, user, adminRights, customTitle = '', }: { chat: ApiChat; user: ApiUser; adminRights: ApiChatAdminRights; customTitle: string }) { const channel = buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel; const userId = buildInputEntity(user.id, user.accessHash) as GramJs.InputUser; return invokeRequest(new GramJs.channels.EditAdmin({ channel, userId, adminRights: buildChatAdminRights(adminRights), rank: customTitle, }), true); } export async function updateChatTitle(chat: ApiChat, title: string) { const inputEntity = buildInputEntity(chat.id, chat.accessHash); await invokeRequest( inputEntity instanceof GramJs.InputChannel ? new GramJs.channels.EditTitle({ channel: inputEntity as GramJs.InputChannel, title, }) : new GramJs.messages.EditChatTitle({ chatId: inputEntity as BigInt.BigInteger, title, }), true, ); } export async function updateChatAbout(chat: ApiChat, about: string) { const result = await invokeRequest(new GramJs.messages.EditChatAbout({ peer: buildInputPeer(chat.id, chat.accessHash), about, })); if (!result) { return; } onUpdate({ '@type': 'updateChatFullInfo', id: chat.id, fullInfo: { about, }, }); } export function toggleSignatures({ chat, isEnabled, }: { chat: ApiChat; isEnabled: boolean }) { const { id, accessHash } = chat; const channel = buildInputEntity(id, accessHash); return invokeRequest(new GramJs.channels.ToggleSignatures({ channel: channel as GramJs.InputChannel, enabled: isEnabled, }), true); } type ChannelMembersFilter = 'kicked' | 'admin' | 'recent'; export async function fetchMembers( chatId: string, accessHash: string, memberFilter: ChannelMembersFilter = 'recent', offset?: number, ) { let filter: GramJs.TypeChannelParticipantsFilter; switch (memberFilter) { case 'kicked': filter = new GramJs.ChannelParticipantsKicked({ q: '' }); break; case 'admin': filter = new GramJs.ChannelParticipantsAdmins(); break; default: filter = new GramJs.ChannelParticipantsRecent(); break; } const result = await invokeRequest(new GramJs.channels.GetParticipants({ channel: buildInputEntity(chatId, accessHash) as GramJs.InputChannel, filter, offset, limit: MEMBERS_LOAD_SLICE, })); if (!result || result instanceof GramJs.channels.ChannelParticipantsNotModified) { return undefined; } updateLocalDb(result); const { users, userStatusesById } = buildApiUsersAndStatuses(result.users); return { members: buildChatMembers(result), users, userStatusesById, }; } export async function fetchGroupsForDiscussion() { const result = await invokeRequest(new GramJs.channels.GetGroupsForDiscussion()); if (!result) { return undefined; } updateLocalDb(result); return result.chats.map((chat) => buildApiChatFromPreview(chat)); } export function setDiscussionGroup({ channel, chat, }: { channel: ApiChat; chat?: ApiChat; }) { return invokeRequest(new GramJs.channels.SetDiscussionGroup({ broadcast: buildInputPeer(channel.id, channel.accessHash), group: chat ? buildInputPeer(chat.id, chat.accessHash) : new GramJs.InputChannelEmpty(), }), true); } export async function migrateChat(chat: ApiChat) { const result = await invokeRequest( new GramJs.messages.MigrateChat({ chatId: buildInputEntity(chat.id) as BigInt.BigInteger }), ); // `migrateChat` can return a lot of different update types according to docs, // but currently chat migrations returns only `Updates` type. // Errors are added to catch unexpected cases in future testing if (!result || !(result instanceof GramJs.Updates)) { if (DEBUG) { // eslint-disable-next-line no-console console.error('Unexpected channel creation update', result); } return undefined; } updateLocalDb(result); return buildApiChatFromPreview(result.chats[1]); } export async function openChatByInvite(hash: string) { const result = await invokeRequest(new GramJs.messages.CheckChatInvite({ hash })); if (!result) { return undefined; } let chat: ApiChat | undefined; if (result instanceof GramJs.ChatInvite) { const { photo, participantsCount, title, channel, requestNeeded, about, megagroup, } = result; if (photo instanceof GramJs.Photo) { addPhotoToLocalDb(result.photo); } onUpdate({ '@type': 'showInvite', data: { title, about, hash, participantsCount, isChannel: channel && !megagroup, isRequestNeeded: requestNeeded, ...(photo instanceof GramJs.Photo && { photo: buildApiPhoto(photo) }), }, }); } else { chat = buildApiChatFromPreview(result.chat); if (chat) { onUpdate({ '@type': 'updateChat', id: chat.id, chat, }); } } if (!chat) { return undefined; } return { chatId: chat.id }; } export async function addChatMembers(chat: ApiChat, users: ApiUser[], noErrorUpdate = false) { try { if (chat.type === 'chatTypeChannel' || chat.type === 'chatTypeSuperGroup') { return await invokeRequest(new GramJs.channels.InviteToChannel({ channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel, users: users.map((user) => buildInputEntity(user.id, user.accessHash)) as GramJs.InputUser[], }), true, noErrorUpdate); } return await Promise.all(users.map((user) => { return invokeRequest(new GramJs.messages.AddChatUser({ chatId: buildInputEntity(chat.id) as BigInt.BigInteger, userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser, }), true, noErrorUpdate); })); } catch (err) { // `noErrorUpdate` will cause an exception which we don't want either return undefined; } } export function deleteChatMember(chat: ApiChat, user: ApiUser) { if (chat.type === 'chatTypeChannel' || chat.type === 'chatTypeSuperGroup') { return updateChatMemberBannedRights({ chat, user, bannedRights: { viewMessages: true, sendMessages: true, sendMedia: true, sendStickers: true, sendGifs: true, sendGames: true, sendInline: true, embedLinks: true, sendPolls: true, changeInfo: true, inviteUsers: true, pinMessages: true, }, untilDate: MAX_INT_32, }); } else { return invokeRequest(new GramJs.messages.DeleteChatUser({ chatId: buildInputEntity(chat.id) as BigInt.BigInteger, userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser, }), true); } } function preparePeers( result: GramJs.messages.Dialogs | GramJs.messages.DialogsSlice | GramJs.messages.PeerDialogs, ) { const store: Record = {}; result.chats.forEach((chat) => { store[`chat${chat.id}`] = chat; }); result.users.forEach((user) => { store[`user${user.id}`] = user; }); return store; } function updateLocalDb(result: ( GramJs.messages.Dialogs | GramJs.messages.DialogsSlice | GramJs.messages.PeerDialogs | GramJs.messages.ChatFull | GramJs.contacts.Found | GramJs.contacts.ResolvedPeer | GramJs.channels.ChannelParticipants | GramJs.messages.Chats | GramJs.messages.ChatsSlice | GramJs.TypeUpdates )) { if ('users' in result) { addEntitiesWithPhotosToLocalDb(result.users); } if ('chats' in result) { addEntitiesWithPhotosToLocalDb(result.chats); } if ('messages' in result) { result.messages.forEach((message) => { if (message instanceof GramJs.Message && isMessageWithMedia(message)) { addMessageToLocalDb(message); } }); } } export async function importChatInvite({ hash }: { hash: string }) { const updates = await invokeRequest(new GramJs.messages.ImportChatInvite({ hash })); if (!(updates instanceof GramJs.Updates) || !updates.chats.length) { return undefined; } return buildApiChatFromPreview(updates.chats[0]); } export function setChatEnabledReactions({ chat, enabledReactions, }: { chat: ApiChat; enabledReactions: string[]; }) { return invokeRequest(new GramJs.messages.SetChatAvailableReactions({ peer: buildInputPeer(chat.id, chat.accessHash), availableReactions: enabledReactions, }), true); } export function toggleIsProtected({ chat, isProtected, }: { chat: ApiChat; isProtected: boolean }) { const { id, accessHash } = chat; return invokeRequest(new GramJs.messages.ToggleNoForwards({ peer: buildInputPeer(id, accessHash), enabled: isProtected, }), true); }