diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 2d369848..30b93dfa 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -26,7 +26,6 @@ import { getChatMember } from './methods/chats/get-chat-member' import { getChatMembers } from './methods/chats/get-chat-members' import { getChatPreview } from './methods/chats/get-chat-preview' import { getChat } from './methods/chats/get-chat' -import { getDialogs } from './methods/chats/get-dialogs' import { getFullChat } from './methods/chats/get-full-chat' import { iterChatMembers } from './methods/chats/iter-chat-members' import { joinChat } from './methods/chats/join-chat' @@ -39,6 +38,11 @@ import { setChatTitle } from './methods/chats/set-chat-title' import { setChatUsername } from './methods/chats/set-chat-username' import { setSlowMode } from './methods/chats/set-slow-mode' import { unarchiveChats } from './methods/chats/unarchive-chats' +import { createFolder } from './methods/dialogs/create-folder' +import { deleteFolder } from './methods/dialogs/delete-folder' +import { editFolder } from './methods/dialogs/edit-folder' +import { getDialogs } from './methods/dialogs/get-dialogs' +import { getFolders } from './methods/dialogs/get-folders' import { downloadAsBuffer } from './methods/files/download-buffer' import { downloadToFile } from './methods/files/download-file' import { downloadAsIterable } from './methods/files/download-iterable' @@ -93,6 +97,7 @@ import { InputPeerLike, MaybeDynamic, Message, + PartialExcept, PropagationSymbol, ReplyMarkup, SentCode, @@ -563,45 +568,6 @@ export class TelegramClient extends BaseTelegramClient { getChat(chatId: InputPeerLike): Promise { return getChat.apply(this, arguments) } - /** - * Get a chunk of dialogs - * - * You can get up to 100 dialogs at once - * - * @param params Fetch parameters - */ - getDialogs(params?: { - /** - * Offset date used as an anchor for pagination. - * - * Use {@link Dialog.date} for this value. - */ - offsetDate?: Date | number - - /** - * Limits the number of dialogs to be received. - * - * Defaults to 100. - */ - limit?: number - - /** - * How to handle pinned dialogs? - * Whether to `include` them, `exclude`, - * or `only` return pinned dialogs. - * - * Defaults to `include` - */ - pinned?: 'include' | 'exclude' | 'only' - - /** - * Whether to get dialogs from the - * archived dialogs list. - */ - archived?: boolean - }): Promise { - return getDialogs.apply(this, arguments) - } /** * Get full information about a chat. * @@ -775,6 +741,140 @@ export class TelegramClient extends BaseTelegramClient { unarchiveChats(chats: MaybeArray): Promise { return unarchiveChats.apply(this, arguments) } + /** + * Create a folder from given parameters + * + * ID for the folder is optional, if not + * provided it will be derived automatically. + * + * @param folder Parameters for the folder + * @returns Newly created folder + */ + createFolder( + folder: PartialExcept + ): Promise { + return createFolder.apply(this, arguments) + } + /** + * Delete a folder by its ID + * + * @param id Folder ID or folder itself + */ + deleteFolder(id: number | tl.RawDialogFilter): Promise { + return deleteFolder.apply(this, arguments) + } + /** + * Edit a folder with given modification + * + * @param folder Folder or folder ID. Note that passing an ID will require re-fetching all folders + * @param modification Modification that will be applied to this folder + * @returns Modified folder + */ + editFolder( + folder: tl.RawDialogFilter | number, + modification: Partial> + ): Promise { + return editFolder.apply(this, arguments) + } + /** + * Iterate over dialogs + * + * @param params Fetch parameters + */ + getDialogs(params?: { + /** + * Offset message date used as an anchor for pagination. + */ + offsetDate?: Date | number + + /** + * Offset message ID used as an anchor for pagination + */ + offsetId?: number + + /** + * Offset peer used as an anchor for pagination + */ + offsetPeer?: tl.TypeInputPeer + + /** + * Limits the number of dialogs to be received. + * + * Defaults to `Infinity`, i.e. all dialogs are fetched, ignored when `pinned=only` + */ + limit?: number + + /** + * Chunk size which will be passed to `messages.getDialogs`. + * You shouldn't usually care about this. + * + * Defaults to 100. + */ + chunkSize?: number + + /** + * How to handle pinned dialogs? + * + * Whether to `include` them at the start of the list, + * `exclude` them at all, or `only` return pinned dialogs. + * + * Additionally, for folders you can specify + * `keep`, which will return pinned dialogs + * ordered by date among other non-pinned dialogs. + * + * Defaults to `include`. + * + * > **Note**: fetching pinned dialogs from + * > folders is slow because of Telegram API limitations. + * > When possible, try to either `exclude` them, + * > or use `keep` and find them using {@link Dialog.findPinned}, + * > passing your folder there. + * > + * > Additionally, when using `include` mode with folders, + * > folders will only be fetched if all offset parameters are unset. + */ + pinned?: 'include' | 'exclude' | 'only' | 'keep' + + /** + * How to handle archived chats? + * + * Whether to `keep` them among other dialogs, + * `exclude` them from the list, or `only` + * return archived dialogs + * + * Defaults to `exclude`, ignored for folders since folders + * themselves contain information about archived chats. + * + * > **Note**: when fetching `only` pinned dialogs + * > passing `keep` will act as passing `only` + */ + archived?: 'keep' | 'exclude' | 'only' + + /** + * Folder from which the dialogs will be fetched. + * + * You can pass folder object, id or title + * + * Note that passing anything except object will + * cause the list of the folders to be fetched, + * and passing a title may fetch from + * a wrong folder if you have multiple with the same title. + * + * When a folder with given ID or title is not found, + * {@link MtCuteArgumentError} is thrown + * + * By default fetches from "All" folder + */ + folder?: string | number | tl.RawDialogFilter + }): AsyncIterableIterator { + return getDialogs.apply(this, arguments) + } + /** + * Get list of folders. + */ + getFolders(): Promise { + return getFolders.apply(this, arguments) + } /** * Download a file and return its contents as a Buffer. * diff --git a/packages/client/src/methods/_imports.ts b/packages/client/src/methods/_imports.ts index ebaf2ade..4880aa7e 100644 --- a/packages/client/src/methods/_imports.ts +++ b/packages/client/src/methods/_imports.ts @@ -21,6 +21,7 @@ import { UploadedFile, UploadFileLike, InputFileLike, + PartialExcept, FileDownloadParameters, UpdateHandler, handlers, diff --git a/packages/client/src/methods/chats/get-dialogs.ts b/packages/client/src/methods/chats/get-dialogs.ts deleted file mode 100644 index c8b8a15b..00000000 --- a/packages/client/src/methods/chats/get-dialogs.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { TelegramClient } from '../../client' -import { Dialog } from '../../types' -import { normalizeDate } from '../../utils/misc-utils' -import { createUsersChatsIndex } from '../../utils/peer-utils' -import { MtCuteTypeAssertionError } from '../../types' -import { tl } from '@mtcute/tl' -import { getMarkedPeerId } from '@mtcute/core' - -/** - * Get a chunk of dialogs - * - * You can get up to 100 dialogs at once - * - * @param params Fetch parameters - * @internal - */ -export async function getDialogs( - this: TelegramClient, - params?: { - /** - * Offset date used as an anchor for pagination. - * - * Use {@link Dialog.date} for this value. - */ - offsetDate?: Date | number - - /** - * Limits the number of dialogs to be received. - * - * Defaults to 100. - */ - limit?: number - - /** - * How to handle pinned dialogs? - * Whether to `include` them, `exclude`, - * or `only` return pinned dialogs. - * - * Defaults to `include` - */ - pinned?: 'include' | 'exclude' | 'only' - - /** - * Whether to get dialogs from the - * archived dialogs list. - */ - archived?: boolean - } -): Promise { - if (!params) params = {} - - let res - if (params.pinned === 'only') { - res = await this.call({ - _: 'messages.getPinnedDialogs', - folderId: params.archived ? 1 : 0, - }) - } else { - res = await this.call({ - _: 'messages.getDialogs', - excludePinned: params.pinned === 'exclude', - folderId: params.archived ? 1 : 0, - offsetDate: normalizeDate(params.offsetDate) ?? 0, - - // offseting by id and peer is useless because when some peer sends - // a message, their dialog goes to the top and we get a cycle - offsetId: 0, - offsetPeer: { _: 'inputPeerEmpty' }, - - limit: params.limit ?? 100, - hash: 0, - }) - } - - if (res._ === 'messages.dialogsNotModified') - throw new MtCuteTypeAssertionError( - 'getDialogs', - '!messages.dialogsNotModified', - 'messages.dialogsNotModified' - ) - - const { users, chats } = createUsersChatsIndex(res) - - const messages: Record = {} - res.messages.forEach((msg) => { - if (!msg.peerId) return - - messages[getMarkedPeerId(msg.peerId)] = msg - }) - - return res.dialogs - .filter(it => it._ === 'dialog') - .map(it => new Dialog(this, it as tl.RawDialog, users, chats, messages)) -} diff --git a/packages/client/src/methods/dialogs/create-folder.ts b/packages/client/src/methods/dialogs/create-folder.ts new file mode 100644 index 00000000..5edf48b7 --- /dev/null +++ b/packages/client/src/methods/dialogs/create-folder.ts @@ -0,0 +1,49 @@ +import { TelegramClient } from '../../client' +import { tl } from '@mtcute/tl' +import { PartialExcept } from '@mtcute/core' + +/** + * Create a folder from given parameters + * + * ID for the folder is optional, if not + * provided it will be derived automatically. + * + * @param folder Parameters for the folder + * @returns Newly created folder + * @internal + */ +export async function createFolder( + this: TelegramClient, + folder: PartialExcept +): Promise { + let id = folder.id + + if (!id) { + const old = await this.getFolders() + + // determine next id by finding max id + // thanks durov for awesome api + let max = 0 + old.forEach((it) => { + if (it.id > max) max = it.id + }) + id = max + 1 + } + + const filter: tl.RawDialogFilter = { + _: 'dialogFilter', + pinnedPeers: [], + includePeers: [], + excludePeers: [], + ...folder, + id + } + + await this.call({ + _: 'messages.updateDialogFilter', + id, + filter + }) + + return filter +} diff --git a/packages/client/src/methods/dialogs/delete-folder.ts b/packages/client/src/methods/dialogs/delete-folder.ts new file mode 100644 index 00000000..5850e0ae --- /dev/null +++ b/packages/client/src/methods/dialogs/delete-folder.ts @@ -0,0 +1,18 @@ +import { TelegramClient } from '../../client' +import { tl } from '@mtcute/tl' + +/** + * Delete a folder by its ID + * + * @param id Folder ID or folder itself + * @internal + */ +export async function deleteFolder( + this: TelegramClient, + id: number | tl.RawDialogFilter +): Promise { + await this.call({ + _: 'messages.updateDialogFilter', + id: typeof id === 'number' ? id : id.id, + }) +} diff --git a/packages/client/src/methods/dialogs/edit-folder.ts b/packages/client/src/methods/dialogs/edit-folder.ts new file mode 100644 index 00000000..a4a494b5 --- /dev/null +++ b/packages/client/src/methods/dialogs/edit-folder.ts @@ -0,0 +1,38 @@ +import { TelegramClient } from '../../client' +import { tl } from '@mtcute/tl' +import { MtCuteArgumentError } from '../../types' + +/** + * Edit a folder with given modification + * + * @param folder Folder or folder ID. Note that passing an ID will require re-fetching all folders + * @param modification Modification that will be applied to this folder + * @returns Modified folder + * @internal + */ +export async function editFolder( + this: TelegramClient, + folder: tl.RawDialogFilter | number, + modification: Partial> +): Promise { + if (typeof folder === 'number') { + const old = await this.getFolders() + const found = old.find(it => it.id === folder) + if (!found) throw new MtCuteArgumentError(`Could not find a folder with ID ${folder}`) + + folder = found + } + + const filter: tl.RawDialogFilter = { + ...folder, + ...modification + } + + await this.call({ + _: 'messages.updateDialogFilter', + id: folder.id, + filter + }) + + return filter +} diff --git a/packages/client/src/methods/dialogs/get-dialogs.ts b/packages/client/src/methods/dialogs/get-dialogs.ts new file mode 100644 index 00000000..fb0f6276 --- /dev/null +++ b/packages/client/src/methods/dialogs/get-dialogs.ts @@ -0,0 +1,255 @@ +import { TelegramClient } from '../../client' +import { + Dialog, + MtCuteArgumentError, + MtCuteTypeAssertionError, +} from '../../types' +import { normalizeDate } from '../../utils/misc-utils' +import { createUsersChatsIndex } from '../../utils/peer-utils' +import { tl } from '@mtcute/tl' +import { getMarkedPeerId } from '@mtcute/core' + +/** + * Iterate over dialogs. + * + * Note that due to Telegram limitations, + * ordering here can only be anti-chronological + * (i.e. newest - first), and draft update date + * is not considered when sorting. + * + * @param params Fetch parameters + * @internal + */ +export async function* getDialogs( + this: TelegramClient, + params?: { + /** + * Offset message date used as an anchor for pagination. + */ + offsetDate?: Date | number + + /** + * Offset message ID used as an anchor for pagination + */ + offsetId?: number + + /** + * Offset peer used as an anchor for pagination + */ + offsetPeer?: tl.TypeInputPeer + + /** + * Limits the number of dialogs to be received. + * + * Defaults to `Infinity`, i.e. all dialogs are fetched, ignored when `pinned=only` + */ + limit?: number + + /** + * Chunk size which will be passed to `messages.getDialogs`. + * You shouldn't usually care about this. + * + * Defaults to 100. + */ + chunkSize?: number + + /** + * How to handle pinned dialogs? + * + * Whether to `include` them at the start of the list, + * `exclude` them at all, or `only` return pinned dialogs. + * + * Additionally, for folders you can specify + * `keep`, which will return pinned dialogs + * ordered by date among other non-pinned dialogs. + * + * Defaults to `include`. + * + * > **Note**: When using `include` mode with folders, + * > pinned dialogs will only be fetched if all offset + * > parameters are unset. + */ + pinned?: 'include' | 'exclude' | 'only' | 'keep' + + /** + * How to handle archived chats? + * + * Whether to `keep` them among other dialogs, + * `exclude` them from the list, or `only` + * return archived dialogs + * + * Defaults to `exclude`, ignored for folders since folders + * themselves contain information about archived chats. + * + * > **Note**: when fetching `only` pinned dialogs + * > passing `keep` will act as passing `only` + */ + archived?: 'keep' | 'exclude' | 'only' + + /** + * Folder from which the dialogs will be fetched. + * + * You can pass folder object, id or title + * + * Note that passing anything except object will + * cause the list of the folders to be fetched, + * and passing a title may fetch from + * a wrong folder if you have multiple with the same title. + * + * Also note that fetching dialogs in a folder is + * *orders of magnitudes* slower than normal because + * of Telegram API limitations - we have to fetch all dialogs + * and filter the ones we need manually. If possible, + * use {@link Dialog.filterFolder} instead. + * + * When a folder with given ID or title is not found, + * {@link MtCuteArgumentError} is thrown + * + * By default fetches from "All" folder + */ + folder?: string | number | tl.RawDialogFilter + } +): AsyncIterableIterator { + if (!params) params = {} + + // fetch folder if needed + let filters: tl.RawDialogFilter | undefined + if ( + typeof params.folder === 'string' || + typeof params.folder === 'number' + ) { + const folders = await this.getFolders() + const found = folders.find( + (it) => it.id === params!.folder || it.title === params!.folder + ) + if (!found) + throw new MtCuteArgumentError(`Could not find folder ${params.folder}`) + + filters = found + } else { + filters = params.folder + } + + const fetchPinnedDialogsFromFolder = async (): Promise => { + if (!filters || !filters.pinnedPeers.length) return null + const res = await this.call({ + _: 'messages.getPeerDialogs', + peers: filters.pinnedPeers.map((peer) => ({ + _: 'inputDialogPeer', + peer + })) + }) + + res.dialogs.forEach((dialog: tl.Mutable) => dialog.pinned = true) + + return res + } + + const parseDialogs = ( + res: tl.messages.TypeDialogs | tl.messages.TypePeerDialogs + ): Dialog[] => { + if (res._ === 'messages.dialogsNotModified') + throw new MtCuteTypeAssertionError( + 'getDialogs', + '!messages.dialogsNotModified', + 'messages.dialogsNotModified' + ) + + const { users, chats } = createUsersChatsIndex(res) + + const messages: Record = {} + res.messages.forEach((msg) => { + if (!msg.peerId) return + + messages[getMarkedPeerId(msg.peerId)] = msg + }) + + return res.dialogs + .filter((it) => it._ === 'dialog') + .map( + (it) => + new Dialog(this, it as tl.RawDialog, users, chats, messages) + ) + } + + const pinned = params.pinned ?? 'include' + let archived = params.archived ?? 'exclude' + + if (filters) { + archived = filters.excludeArchived ? 'exclude' : 'keep' + } + + if (pinned === 'only') { + let res + if (filters) { + res = await fetchPinnedDialogsFromFolder() + } else { + res = await this.call({ + _: 'messages.getPinnedDialogs', + folderId: archived === 'exclude' ? 0 : 1, + }) + } + if (res) yield* parseDialogs(res) + return + } + + let current = 0 + const total = params.limit ?? Infinity + const chunkSize = Math.min(params.chunkSize ?? 100, total) + + let offsetId = params.offsetId ?? 0 + let offsetDate = normalizeDate(params.offsetDate) ?? 0 + let offsetPeer = params.offsetPeer ?? { _: 'inputPeerEmpty' } + + if (filters && filters.pinnedPeers.length && pinned === 'include') { + const res = await fetchPinnedDialogsFromFolder() + if (res) { + const dialogs = parseDialogs(res) + + for (const d of dialogs) { + yield d + + if (++current >= total) return + } + } + } + + const filterFolder = filters + // if pinned is `only`, this wouldn't be reached + // if pinned is `exclude`, we want to exclude them + // if pinned is `include`, we already yielded them, so we also want to exclude them + // if pinned is `keep`, we want to keep them + ? Dialog.filterFolder(filters, pinned !== 'keep') + : undefined + + const folderId = + archived === 'keep' ? undefined : archived === 'only' ? 1 : 0 + for (;;) { + const dialogs = parseDialogs( + await this.call({ + _: 'messages.getDialogs', + excludePinned: params.pinned === 'exclude', + folderId, + offsetDate, + offsetId, + offsetPeer, + + limit: chunkSize, + hash: 0, + }) + ) + if (!dialogs.length) return + + const last = dialogs[dialogs.length - 1] + offsetPeer = last.chat.inputPeer + offsetId = last.raw.topMessage + offsetDate = normalizeDate(last.lastMessage.date)! + + for (const d of dialogs) { + if (filterFolder && !filterFolder(d)) continue + + yield d + if (++current >= total) return + } + } +} diff --git a/packages/client/src/methods/dialogs/get-folders.ts b/packages/client/src/methods/dialogs/get-folders.ts new file mode 100644 index 00000000..9054ee83 --- /dev/null +++ b/packages/client/src/methods/dialogs/get-folders.ts @@ -0,0 +1,12 @@ +import { TelegramClient } from '../../client' +import { tl } from '@mtcute/tl' + +/** + * Get list of folders. + * @internal + */ +export async function getFolders(this: TelegramClient): Promise { + return this.call({ + _: 'messages.getDialogFilters' + }) +} diff --git a/packages/client/src/types/index.ts b/packages/client/src/types/index.ts index 1fed68e0..fe3f023d 100644 --- a/packages/client/src/types/index.ts +++ b/packages/client/src/types/index.ts @@ -8,4 +8,4 @@ export * from './updates' export * from './errors' export { MaybeDynamic } from './utils' -export { MaybeAsync } from '@mtcute/core' +export { MaybeAsync, PartialExcept } from '@mtcute/core' diff --git a/packages/client/src/types/messages/dialog.ts b/packages/client/src/types/messages/dialog.ts index 976c0304..e553b5df 100644 --- a/packages/client/src/types/messages/dialog.ts +++ b/packages/client/src/types/messages/dialog.ts @@ -4,6 +4,8 @@ import { Chat } from '../peers' import { Message } from './message' import { DraftMessage } from './draft-message' import { makeInspectable } from '../utils' +import { getMarkedPeerId } from '@mtcute/core' +import { MtCuteEmptyError } from '../errors' /** * A dialog. @@ -38,6 +40,90 @@ export class Dialog { this._messages = messages } + /** + * Find pinned dialogs from a list of dialogs + * + * @param dialogs Dialogs list + * @param folder If passed, status of pin will be checked against this folder, and not globally + */ + static findPinned( + dialogs: Dialog[], + folder?: tl.RawDialogFilter + ): Dialog[] { + if (folder) { + const index: Record = {} + folder.pinnedPeers.forEach((peer) => { + index[getMarkedPeerId(peer)] = true + }) + + return dialogs.filter((i) => index[i.chat.id]) + } + return dialogs.filter((i) => i.isPinned) + } + + /** + * Create a filter predicate for the given Folder. + * Returned predicate can be used in `Array.filter()` + * + * @param folder Folder to filter for + * @param excludePinned Whether to exclude pinned folders + */ + static filterFolder( + folder: tl.RawDialogFilter, + excludePinned = true + ): (val: Dialog) => boolean { + const pinned: Record = {} + const include: Record = {} + const exclude: Record = {} + + // populate indices + if (excludePinned) { + folder.pinnedPeers.forEach((peer) => { + pinned[getMarkedPeerId(peer)] = true + }) + } + folder.includePeers.forEach((peer) => { + include[getMarkedPeerId(peer)] = true + }) + folder.excludePeers.forEach((peer) => { + exclude[getMarkedPeerId(peer)] = true + }) + + return (dialog) => { + const chat = dialog.chat + + // manual exclusion/inclusion and pins + if (include[chat.id]) return true + if (exclude[chat.id] || pinned[chat.id]) return false + + // exclusions based on status + if (folder.excludeRead && !dialog.isUnread) return false + if (folder.excludeMuted && dialog.isMuted) return false + // even though this was handled in getDialogs, this method + // could be used outside of it, so check again + if (folder.excludeArchived && dialog.isArchived) return false + + // inclusions based on chat type + if (folder.contacts && chat.type === 'private' && chat.isContact) + return true + if ( + folder.nonContacts && + chat.type === 'private' && + !chat.isContact + ) + return true + if ( + folder.groups && + (chat.type === 'group' || chat.type === 'supergroup') + ) + return true + if (folder.broadcasts && chat.type === 'channel') return true + if (folder.bots && chat.type === 'bot') return true + + return false + } + } + /** * Whether this dialog is pinned */ @@ -61,6 +147,20 @@ export class Dialog { return this.raw.unreadMark || this.raw.unreadCount > 1 } + /** + * Whether this dialog is muted + */ + get isMuted(): boolean { + return !!this.raw.notifySettings.silent + } + + /** + * Whether this dialog is archived + */ + get isArchived(): boolean { + return this.raw.folderId === 1 + } + private _chat?: Chat /** * Chat that this dialog represents @@ -71,7 +171,9 @@ export class Dialog { let chat if (peer._ === 'peerChannel' || peer._ === 'peerChat') { - chat = this._chats[peer._ === 'peerChannel' ? peer.channelId : peer.chatId] + chat = this._chats[ + peer._ === 'peerChannel' ? peer.channelId : peer.chatId + ] } else { chat = this._users[peer.userId] } @@ -82,17 +184,22 @@ export class Dialog { return this._chat } - private _lastMessage?: Message | null + private _lastMessage?: Message /** * The latest message sent in this chat */ - get lastMessage(): Message | null { - if (this._lastMessage === undefined) { + get lastMessage(): Message { + if (!this._lastMessage) { const cid = this.chat.id if (cid in this._messages) { - this._lastMessage = new Message(this.client, this._messages[cid], this._users, this._chats) + this._lastMessage = new Message( + this.client, + this._messages[cid], + this._users, + this._chats + ) } else { - this._lastMessage = null + throw new MtCuteEmptyError() } } @@ -120,7 +227,11 @@ export class Dialog { get draftMessage(): DraftMessage | null { if (this._draftMessage === undefined) { if (this.raw.draft?._ === 'draftMessage') { - this._draftMessage = new DraftMessage(this.client, this.raw.draft, this.chat.inputPeer) + this._draftMessage = new DraftMessage( + this.client, + this.raw.draft, + this.chat.inputPeer + ) } else { this._draftMessage = null } diff --git a/packages/client/src/types/peers/chat.ts b/packages/client/src/types/peers/chat.ts index 77ed6336..5bf3b4ff 100644 --- a/packages/client/src/types/peers/chat.ts +++ b/packages/client/src/types/peers/chat.ts @@ -15,6 +15,7 @@ export namespace Chat { * - `group`: Legacy group * - `supergroup`: Supergroup * - `channel`: Broadcast channel + * - `gigagroup`: Gigagroup aka Broadcast group */ export type Type = | 'private' @@ -22,6 +23,7 @@ export namespace Chat { | 'group' | 'supergroup' | 'channel' + | 'gigagroup' } /** @@ -115,7 +117,9 @@ export class Chat { } else if (this.peer._ === 'chat') { this._type = 'group' } else if (this.peer._ === 'channel') { - this._type = this.peer.broadcast + this._type = this.peer.gigagroup + ? 'gigagroup' + : this.peer.broadcast ? 'channel' : 'supergroup' } @@ -168,6 +172,11 @@ export class Chat { return this.peer._ === 'user' && this.peer.self! } + /** Whether this peer is your contact */ + get isContact(): boolean { + return this.peer._ === 'user' && this.peer.contact! + } + /** * Title, for supergroups, channels and groups */ @@ -439,7 +448,10 @@ export class Chat { * Number of old messages to be forwarded (0-100). * Only applicable to legacy groups, ignored for supergroups and channels */ - async addMembers(users: MaybeArray, forwardCount?: number): Promise { + async addMembers( + users: MaybeArray, + forwardCount?: number + ): Promise { return this.client.addChatMembers(this.inputPeer, users, forwardCount) } diff --git a/packages/client/src/utils/misc-utils.ts b/packages/client/src/utils/misc-utils.ts index 60fe9474..ccc043f4 100644 --- a/packages/client/src/utils/misc-utils.ts +++ b/packages/client/src/utils/misc-utils.ts @@ -1,7 +1,7 @@ import { MaybeDynamic, MtCuteError } from '../types' import { BigInteger } from 'big-integer' -import { randomBytes } from '@mtcute/core/dist/utils/buffer-utils' -import { bufferToBigInt } from '@mtcute/core/dist/utils/bigint-utils' +import { randomBytes } from '@mtcute/core/src/utils/buffer-utils' +import { bufferToBigInt } from '@mtcute/core/src/utils/bigint-utils' import { tl } from '@mtcute/tl' export const EMPTY_BUFFER = Buffer.alloc(0) diff --git a/packages/client/src/utils/peer-utils.ts b/packages/client/src/utils/peer-utils.ts index 28dd6262..05490293 100644 --- a/packages/client/src/utils/peer-utils.ts +++ b/packages/client/src/utils/peer-utils.ts @@ -2,7 +2,6 @@ import { tl } from '@mtcute/tl' export const INVITE_LINK_REGEX = /^(?:https?:\/\/)?(?:www\.)?(?:t(?:elegram)?\.(?:org|me|dog)\/joinchat\/)([\w-]+)$/i - // helpers to normalize result of `resolvePeer` function export function normalizeToInputPeer( @@ -81,7 +80,16 @@ export function inputPeerToPeer(inp: tl.TypeInputPeer): tl.TypePeer { return inp as never } -export function createUsersChatsIndex(obj: { users: tl.TypeUser[], chats: tl.TypeChat[] }): { +export function createUsersChatsIndex( + obj: { + users: tl.TypeUser[] + chats: tl.TypeChat[] + }, + second?: { + users: tl.TypeUser[] + chats: tl.TypeChat[] + } +): { users: Record chats: Record } { @@ -90,5 +98,10 @@ export function createUsersChatsIndex(obj: { users: tl.TypeUser[], chats: tl.Typ obj.users.forEach((e) => (users[e.id] = e)) obj.chats.forEach((e) => (chats[e.id] = e)) + if (second) { + second.users.forEach((e) => (users[e.id] = e)) + second.chats.forEach((e) => (chats[e.id] = e)) + } + return { users, chats } }