From 82994408a2a0ddb8d4e8fa741627b3afdde506ff Mon Sep 17 00:00:00 2001 From: Alina Sireneva Date: Fri, 29 Sep 2023 17:02:36 +0300 Subject: [PATCH] feat: refactor some iterable methods to have non-iterable counterpart --- packages/client/src/client.ts | 227 +++++++++++++----- packages/client/src/methods/_imports.ts | 5 +- .../src/methods/chats/get-chat-event-log.ts | 187 ++++----------- .../src/methods/chats/iter-chat-event-log.ts | 82 +++++++ .../client/src/methods/dialogs/get-folders.ts | 28 ++- .../src/methods/dialogs/get-peer-dialogs.ts | 2 +- .../{get-dialogs.ts => iter-dialogs.ts} | 133 +++++----- .../src/methods/dialogs/parse-dialogs.ts | 26 -- .../invite-links/get-invite-link-members.ts | 82 +++---- .../methods/invite-links/get-invite-links.ts | 82 +++---- .../invite-links/iter-invite-link-members.ts | 63 +++++ .../methods/invite-links/iter-invite-links.ts | 67 ++++++ packages/client/src/types/messages/dialog.ts | 45 +++- .../{chat-event.ts => chat-event/actions.ts} | 120 ++++----- .../src/types/peers/chat-event/filters.ts | 123 ++++++++++ .../src/types/peers/chat-event/index.ts | 50 ++++ .../types/peers/chat-invite-link-member.ts | 59 +++++ .../src/types/peers/chat-invite-link.ts | 26 -- packages/client/src/types/peers/index.ts | 1 + .../src/types/updates/chat-join-request.ts | 13 +- packages/client/src/utils/misc-utils.ts | 10 +- packages/core/src/utils/type-assertions.ts | 2 +- 22 files changed, 925 insertions(+), 508 deletions(-) create mode 100644 packages/client/src/methods/chats/iter-chat-event-log.ts rename packages/client/src/methods/dialogs/{get-dialogs.ts => iter-dialogs.ts} (66%) delete mode 100644 packages/client/src/methods/dialogs/parse-dialogs.ts create mode 100644 packages/client/src/methods/invite-links/iter-invite-link-members.ts create mode 100644 packages/client/src/methods/invite-links/iter-invite-links.ts rename packages/client/src/types/peers/{chat-event.ts => chat-event/actions.ts} (80%) create mode 100644 packages/client/src/types/peers/chat-event/filters.ts create mode 100644 packages/client/src/types/peers/chat-event/index.ts create mode 100644 packages/client/src/types/peers/chat-invite-link-member.ts diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index f19e2ef0..ffa80053 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -60,6 +60,7 @@ import { getChatMembers } from './methods/chats/get-chat-members' import { getChatPreview } from './methods/chats/get-chat-preview' import { getFullChat } from './methods/chats/get-full-chat' import { getNearbyChats } from './methods/chats/get-nearby-chats' +import { iterChatEventLog } from './methods/chats/iter-chat-event-log' import { iterChatMembers } from './methods/chats/iter-chat-members' import { joinChat } from './methods/chats/join-chat' import { kickChatMember } from './methods/chats/kick-chat-member' @@ -84,10 +85,9 @@ import { createFolder } from './methods/dialogs/create-folder' import { deleteFolder } from './methods/dialogs/delete-folder' import { editFolder } from './methods/dialogs/edit-folder' import { findFolder } from './methods/dialogs/find-folder' -import { getDialogs } from './methods/dialogs/get-dialogs' -import { getFolders } from './methods/dialogs/get-folders' +import { _normalizeInputFolder, getFolders } from './methods/dialogs/get-folders' import { getPeerDialogs } from './methods/dialogs/get-peer-dialogs' -import { _parseDialogs } from './methods/dialogs/parse-dialogs' +import { iterDialogs } from './methods/dialogs/iter-dialogs' import { setFoldersOrder } from './methods/dialogs/set-folders' import { downloadAsBuffer } from './methods/files/download-buffer' import { downloadToFile } from './methods/files/download-file' @@ -107,6 +107,8 @@ import { getInviteLinks } from './methods/invite-links/get-invite-links' import { getPrimaryInviteLink } from './methods/invite-links/get-primary-invite-link' import { hideAllJoinRequests } from './methods/invite-links/hide-all-join-requests' import { hideJoinRequest } from './methods/invite-links/hide-join-request' +import { iterInviteLinkMembers } from './methods/invite-links/iter-invite-link-members' +import { iterInviteLinks } from './methods/invite-links/iter-invite-links' import { revokeInviteLink } from './methods/invite-links/revoke-invite-link' import { closePoll } from './methods/messages/close-poll' import { deleteMessages } from './methods/messages/delete-messages' @@ -198,10 +200,9 @@ import { BotStoppedUpdate, CallbackQuery, Chat, - ChatAction, ChatEvent, ChatInviteLink, - ChatInviteLinkJoinedMember, + ChatInviteLinkMember, ChatJoinRequestUpdate, ChatMember, ChatMemberUpdate, @@ -216,6 +217,8 @@ import { HistoryReadUpdate, IMessageEntityParser, InlineQuery, + InputChatEventFilters, + InputDialogFolder, InputFileLike, InputInlineResult, InputMediaLike, @@ -1123,8 +1126,7 @@ export interface TelegramClient extends BaseTelegramClient { rank?: string, ): Promise /** - * Get chat event log ("Recent actions" in official - * clients). + * Get chat event log ("Recent actions" in official clients). * * Only available for supergroups and channels, and * requires (any) administrator rights. @@ -1171,23 +1173,24 @@ export interface TelegramClient extends BaseTelegramClient { * and when passing one or more action types, * they will be filtered locally. */ - filters?: tl.TypeChannelAdminLogEventsFilter | MaybeArray['type']> + filters?: InputChatEventFilters /** * Limit the number of events returned. * - * Defaults to `Infinity`, i.e. all events are returned + * > Note: when using filters, there will likely be + * > less events returned than specified here. + * > This limit is only used to limit the number of + * > events to fetch from the server. + * > + * > If you need to limit the number of events + * > returned, use {@link iterChatEventLog} instead. + * + * @default 100 */ limit?: number - - /** - * Chunk size, usually not needed. - * - * Defaults to `100` - */ - chunkSize?: number }, - ): AsyncIterableIterator + ): Promise /** * Get information about a single chat member * @@ -1277,6 +1280,33 @@ export interface TelegramClient extends BaseTelegramClient { * @param longitude Longitude of the location */ getNearbyChats(latitude: number, longitude: number): Promise + /** + * Iterate over chat event log. + * + * Small wrapper over {@link getChatEventLog} + * + * @param chatId Chat ID + * @param params + */ + iterChatEventLog( + chatId: InputPeerLike, + params?: Parameters[1] & { + /** + * Total number of events to return. + * + * @default Infinity + */ + limit?: number + + /** + * Chunk size, passed as `limit` to {@link getChatEventLog}. + * Usually you don't need to touch this. + * + * @default 100 + */ + chunkSize?: number + }, + ): AsyncIterableIterator /** * Iterate through chat members * @@ -1570,17 +1600,35 @@ export interface TelegramClient extends BaseTelegramClient { * @param params Search parameters. At least one must be set. */ findFolder(params: { title?: string; emoji?: string; id?: number }): Promise + /** + * Get list of folders. + */ + getFolders(): Promise + + _normalizeInputFolder(folder: InputDialogFolder): Promise + /** + * Get dialogs with certain peers. + * + * @param peers Peers for which to fetch dialogs. + */ + getPeerDialogs(peers: InputPeerLike): Promise + /** + * Get dialogs with certain peers. + * + * @param peers Peers for which to fetch dialogs. + */ + getPeerDialogs(peers: InputPeerLike[]): Promise /** * Iterate over dialogs. * - * Note that due to Telegram limitations, + * Note that due to Telegram API 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 */ - getDialogs(params?: { + iterDialogs(params?: { /** * Offset message date used as an anchor for pagination. */ @@ -1599,7 +1647,7 @@ export interface TelegramClient extends BaseTelegramClient { /** * Limits the number of dialogs to be received. * - * Defaults to `Infinity`, i.e. all dialogs are fetched, ignored when `pinned=only` + * @default `Infinity`, i.e. all dialogs are fetched */ limit?: number @@ -1607,7 +1655,7 @@ export interface TelegramClient extends BaseTelegramClient { * Chunk size which will be passed to `messages.getDialogs`. * You shouldn't usually care about this. * - * Defaults to 100. + * @default 100. */ chunkSize?: number @@ -1621,11 +1669,11 @@ export interface TelegramClient extends BaseTelegramClient { * `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. + * + * @default `include`. */ pinned?: 'include' | 'exclude' | 'only' | 'keep' @@ -1636,11 +1684,13 @@ export interface TelegramClient extends BaseTelegramClient { * `exclude` them from the list, or `only` * return archived dialogs * - * Defaults to `exclude`, ignored for folders since folders + * Ignored for folders, since folders * themselves contain information about archived chats. * - * > **Note**: when fetching `only` pinned dialogs - * > passing `keep` will act as passing `only` + * > **Note**: when `pinned=only`, `archived=keep` will act as `only` + * > because of Telegram API limitations. + * + * @default `exclude` */ archived?: 'keep' | 'exclude' | 'only' @@ -1663,9 +1713,9 @@ export interface TelegramClient extends BaseTelegramClient { * When a folder with given ID or title is not found, * {@link MtArgumentError} is thrown * - * By default fetches from "All" folder + * @default (fetches from "All" folder) */ - folder?: string | number | tl.RawDialogFilter + folder?: InputDialogFolder /** * Additional filtering for the dialogs. @@ -1676,24 +1726,6 @@ export interface TelegramClient extends BaseTelegramClient { */ filter?: Partial> }): AsyncIterableIterator - /** - * Get list of folders. - */ - getFolders(): Promise - /** - * Get dialogs with certain peers. - * - * @param peers Peers for which to fetch dialogs. - */ - getPeerDialogs(peers: InputPeerLike): Promise - /** - * Get dialogs with certain peers. - * - * @param peers Peers for which to fetch dialogs. - */ - getPeerDialogs(peers: InputPeerLike[]): Promise - - _parseDialogs(res: tl.messages.TypeDialogs | tl.messages.TypePeerDialogs): Dialog[] /** * Reorder folders * @@ -1943,10 +1975,22 @@ export interface TelegramClient extends BaseTelegramClient { link?: string /** - * Maximum number of users to return (by default returns all) + * Maximum number of users to return + * + * @default 100 */ limit?: number + /** + * Offset request/join date used as an anchor for pagination. + */ + offsetDate?: Date | number + + /** + * Offset user used as an anchor for pagination + */ + offsetUser?: tl.TypeInputUser + /** * Whether to get users who have requested to join * the chat but weren't accepted yet @@ -1955,13 +1999,13 @@ export interface TelegramClient extends BaseTelegramClient { /** * Search for a user in the pending join requests list - * (only works if {@link requested} is true) + * (if passed, {@link requested} is assumed to be true) * * Doesn't work when {@link link} is set (Telegram limitation) */ requestedSearch?: string }, - ): AsyncIterableIterator + ): Promise> /** * Get detailed information about an invite link * @@ -1982,8 +2026,14 @@ export interface TelegramClient extends BaseTelegramClient { */ getInviteLinks( chatId: InputPeerLike, - adminId: InputPeerLike, params?: { + /** + * Only return this admin's links. + * + * @default `"self"` + */ + admin?: InputPeerLike + /** * Whether to fetch revoked invite links */ @@ -1991,18 +2041,22 @@ export interface TelegramClient extends BaseTelegramClient { /** * Limit the number of invite links to be fetched. - * By default, all links are fetched. + * + * @default 100 */ limit?: number /** - * Size of chunks which are fetched. Usually not needed. - * - * Defaults to `100` + * Offset date used as an anchor for pagination. */ - chunkSize?: number + offsetDate?: Date | number + + /** + * Offset link used as an anchor for pagination + */ + offsetLink?: string }, - ): AsyncIterableIterator + ): Promise> /** * Get primary invite link of a chat * @@ -2025,6 +2079,60 @@ export interface TelegramClient extends BaseTelegramClient { * @param action Whether to approve or deny the join request */ hideJoinRequest(peer: InputPeerLike, user: InputPeerLike, action: 'approve' | 'deny'): Promise + /** + * Iterate over users who have joined + * the chat with the given invite link. + * + * @param chatId Chat ID + * @param params Additional params + */ + iterInviteLinkMembers( + chatId: InputPeerLike, + params: Parameters[1] & { + /** + * Maximum number of users to return + * + * @default `Infinity`, i.e. all users are fetched + */ + limit?: number + + /** + * Chunk size which will be passed to `messages.getChatInviteImporters`. + * You shouldn't usually care about this. + * + * @default 100. + */ + chunkSize?: number + }, + ): AsyncIterableIterator + /** + * Iterate over invite links created by some administrator in the chat. + * + * As an administrator you can only get your own links + * (i.e. `adminId = "self"`), as a creator you can get + * any other admin's links. + * + * @param chatId Chat ID + * @param adminId Admin who created the links + * @param params + */ + iterInviteLinks( + chatId: InputPeerLike, + params?: Parameters[1] & { + /** + * Limit the number of invite links to be fetched. + * By default, all links are fetched. + */ + limit?: number + + /** + * Size of chunks which are fetched. Usually not needed. + * + * Defaults to `100` + */ + chunkSize?: number + }, + ): AsyncIterableIterator /** * Revoke an invite link. * @@ -3921,6 +4029,7 @@ export class TelegramClient extends BaseTelegramClient { getChat = getChat getFullChat = getFullChat getNearbyChats = getNearbyChats + iterChatEventLog = iterChatEventLog iterChatMembers = iterChatMembers joinChat = joinChat kickChatMember = kickChatMember @@ -3946,10 +4055,10 @@ export class TelegramClient extends BaseTelegramClient { deleteFolder = deleteFolder editFolder = editFolder findFolder = findFolder - getDialogs = getDialogs getFolders = getFolders + _normalizeInputFolder = _normalizeInputFolder getPeerDialogs = getPeerDialogs - _parseDialogs = _parseDialogs + iterDialogs = iterDialogs setFoldersOrder = setFoldersOrder downloadAsBuffer = downloadAsBuffer downloadToFile = downloadToFile @@ -3969,6 +4078,8 @@ export class TelegramClient extends BaseTelegramClient { getPrimaryInviteLink = getPrimaryInviteLink hideAllJoinRequests = hideAllJoinRequests hideJoinRequest = hideJoinRequest + iterInviteLinkMembers = iterInviteLinkMembers + iterInviteLinks = iterInviteLinks revokeInviteLink = revokeInviteLink closePoll = closePoll deleteMessages = deleteMessages diff --git a/packages/client/src/methods/_imports.ts b/packages/client/src/methods/_imports.ts index 2cf48488..5434b0a4 100644 --- a/packages/client/src/methods/_imports.ts +++ b/packages/client/src/methods/_imports.ts @@ -17,10 +17,9 @@ import { BotStoppedUpdate, CallbackQuery, Chat, - ChatAction, ChatEvent, ChatInviteLink, - ChatInviteLinkJoinedMember, + ChatInviteLinkMember, ChatJoinRequestUpdate, ChatMember, ChatMemberUpdate, @@ -35,6 +34,8 @@ import { HistoryReadUpdate, IMessageEntityParser, InlineQuery, + InputChatEventFilters, + InputDialogFolder, InputFileLike, InputInlineResult, InputMediaLike, diff --git a/packages/client/src/methods/chats/get-chat-event-log.ts b/packages/client/src/methods/chats/get-chat-event-log.ts index 51718c71..bd760aca 100644 --- a/packages/client/src/methods/chats/get-chat-event-log.ts +++ b/packages/client/src/methods/chats/get-chat-event-log.ts @@ -1,14 +1,14 @@ import Long from 'long' -import { assertNever, MaybeArray, tl } from '@mtcute/core' +import { tl } from '@mtcute/core' import { TelegramClient } from '../../client' -import { ChatAction, ChatEvent, InputPeerLike, PeersIndex } from '../../types' +import { ChatEvent, InputPeerLike, PeersIndex } from '../../types' +import { InputChatEventFilters, normalizeChatEventFilters } from '../../types/peers/chat-event/filters' import { normalizeToInputChannel, normalizeToInputUser } from '../../utils/peer-utils' /** - * Get chat event log ("Recent actions" in official - * clients). + * Get chat event log ("Recent actions" in official clients). * * Only available for supergroups and channels, and * requires (any) administrator rights. @@ -22,7 +22,7 @@ import { normalizeToInputChannel, normalizeToInputUser } from '../../utils/peer- * @param params * @internal */ -export async function* getChatEventLog( +export async function getChatEventLog( this: TelegramClient, chatId: InputPeerLike, params?: { @@ -57,165 +57,62 @@ export async function* getChatEventLog( * and when passing one or more action types, * they will be filtered locally. */ - filters?: tl.TypeChannelAdminLogEventsFilter | MaybeArray['type']> + filters?: InputChatEventFilters /** * Limit the number of events returned. * - * Defaults to `Infinity`, i.e. all events are returned + * > Note: when using filters, there will likely be + * > less events returned than specified here. + * > This limit is only used to limit the number of + * > events to fetch from the server. + * > + * > If you need to limit the number of events + * > returned, use {@link iterChatEventLog} instead. + * + * @default 100 */ limit?: number - - /** - * Chunk size, usually not needed. - * - * Defaults to `100` - */ - chunkSize?: number }, -): AsyncIterableIterator { +): Promise { if (!params) params = {} const channel = normalizeToInputChannel(await this.resolvePeer(chatId), chatId) - let current = 0 - let maxId = params.maxId ?? Long.ZERO - const minId = params.minId ?? Long.ZERO - const query = params.query ?? '' + const { maxId = Long.ZERO, minId = Long.ZERO, query = '', limit = 100, users, filters } = params - const total = params.limit || Infinity - const chunkSize = Math.min(params.chunkSize ?? 100, total) - - const admins: tl.TypeInputUser[] | undefined = params.users ? - await this.resolvePeerMany(params.users, normalizeToInputUser) : + const admins: tl.TypeInputUser[] | undefined = users ? + await this.resolvePeerMany(users, normalizeToInputUser) : undefined - let serverFilter: tl.Mutable | undefined = undefined - let localFilter: Record | undefined = undefined + const { serverFilter, localFilter } = normalizeChatEventFilters(filters) - if (params.filters) { - if (typeof params.filters === 'string' || Array.isArray(params.filters)) { - let input = params.filters - if (!Array.isArray(input)) input = [input] + const res = await this.call({ + _: 'channels.getAdminLog', + channel, + q: query, + eventsFilter: serverFilter, + admins, + maxId, + minId, + limit, + }) - serverFilter = { - _: 'channelAdminLogEventsFilter', - } - localFilter = {} + if (!res.events.length) return [] - input.forEach((type) => { - localFilter![type] = true + const peers = PeersIndex.from(res) - switch (type) { - case 'user_joined': - serverFilter!.join = true - break - case 'user_left': - serverFilter!.leave = true - break - case 'user_invited': - serverFilter!.invite = true - break - case 'title_changed': - case 'description_changed': - case 'linked_chat_changed': - case 'location_changed': - case 'photo_changed': - case 'username_changed': - case 'stickerset_changed': - serverFilter!.info = true - break - case 'invites_toggled': - case 'history_toggled': - case 'signatures_toggled': - case 'def_perms_changed': - serverFilter!.settings = true - break - case 'msg_pinned': - serverFilter!.pinned = true - break - case 'msg_edited': - case 'poll_stopped': - serverFilter!.edit = true - break - case 'msg_deleted': - serverFilter!.delete = true - break - case 'user_perms_changed': - serverFilter!.ban = true - serverFilter!.unban = true - serverFilter!.kick = true - serverFilter!.unkick = true - break - case 'user_admin_perms_changed': - serverFilter!.promote = true - serverFilter!.demote = true - break - case 'slow_mode_changed': - case 'ttl_changed': - // not documented so idk, enable both - serverFilter!.settings = true - serverFilter!.info = true - break - case 'call_started': - case 'call_ended': - serverFilter!.groupCall = true - break - case 'call_setting_changed': - // not documented so idk, enable all - serverFilter!.groupCall = true - serverFilter!.settings = true - serverFilter!.info = true - break - case 'user_joined_invite': - // not documented so idk, enable all - serverFilter!.join = true - serverFilter!.invite = true - serverFilter!.invites = true - break - case 'invite_deleted': - case 'invite_edited': - case 'invite_revoked': - serverFilter!.invites = true - break - default: - assertNever(type) - } - }) - } else { - serverFilter = params.filters + const results: ChatEvent[] = [] + + for (const evt of res.events) { + const parsed = new ChatEvent(this, evt, peers) + + if (localFilter && (!parsed.action || !localFilter[parsed.action.type])) { + continue } + + results.push(parsed) } - for (;;) { - const res = await this.call({ - _: 'channels.getAdminLog', - channel, - q: query, - eventsFilter: serverFilter, - admins, - maxId, - minId, - limit: Math.min(chunkSize, total - current), - }) - - if (!res.events.length) break - - const peers = PeersIndex.from(res) - const last = res.events[res.events.length - 1] - maxId = last.id - - for (const evt of res.events) { - const parsed = new ChatEvent(this, evt, peers) - - if (localFilter && (!parsed.action || !localFilter[parsed.action.type])) { - continue - } - - current += 1 - yield parsed - - if (current >= total) break - } - } + return results } diff --git a/packages/client/src/methods/chats/iter-chat-event-log.ts b/packages/client/src/methods/chats/iter-chat-event-log.ts new file mode 100644 index 00000000..251222ce --- /dev/null +++ b/packages/client/src/methods/chats/iter-chat-event-log.ts @@ -0,0 +1,82 @@ +import Long from 'long' + +import { tl } from '@mtcute/core' + +import { TelegramClient } from '../../client' +import { ChatEvent, InputPeerLike } from '../../types' +import { normalizeChatEventFilters } from '../../types/peers/chat-event/filters' +import { normalizeToInputChannel, normalizeToInputUser } from '../../utils/peer-utils' + +/** + * Iterate over chat event log. + * + * Small wrapper over {@link getChatEventLog} + * + * @param chatId Chat ID + * @param params + * @internal + */ +export async function* iterChatEventLog( + this: TelegramClient, + chatId: InputPeerLike, + params?: Parameters[1] & { + /** + * Total number of events to return. + * + * @default Infinity + */ + limit?: number + + /** + * Chunk size, passed as `limit` to {@link getChatEventLog}. + * Usually you don't need to touch this. + * + * @default 100 + */ + chunkSize?: number + }, +): AsyncIterableIterator { + if (!params) params = {} + + const channel = normalizeToInputChannel(await this.resolvePeer(chatId), chatId) + + const { minId = Long.ZERO, query = '', limit = Infinity, chunkSize = 100, users, filters } = params + + const admins: tl.TypeInputUser[] | undefined = users ? + await this.resolvePeerMany(users, normalizeToInputUser) : + undefined + + const { serverFilter, localFilter } = normalizeChatEventFilters(filters) + + let current = 0 + let maxId = params.maxId ?? Long.ZERO + + for (;;) { + const chunk = await this.getChatEventLog(channel, { + minId, + maxId, + query, + limit: localFilter ? chunkSize : Math.min(limit - current, chunkSize), + // provide already resolved users to avoid resolving them again + users: admins, + // local filters may mess with pagination + filters: { serverFilter }, + }) + + if (!chunk.length) break + + const last = chunk[chunk.length - 1] + maxId = last.id + + for (const item of chunk) { + if (localFilter && (!item.action || !localFilter[item.action.type])) { + continue + } + + current += 1 + yield item + + if (current >= limit) break + } + } +} diff --git a/packages/client/src/methods/dialogs/get-folders.ts b/packages/client/src/methods/dialogs/get-folders.ts index bf41ef59..9907dc38 100644 --- a/packages/client/src/methods/dialogs/get-folders.ts +++ b/packages/client/src/methods/dialogs/get-folders.ts @@ -1,6 +1,7 @@ -import { tl } from '@mtcute/core' +import { MtArgumentError, tl } from '@mtcute/core' import { TelegramClient } from '../../client' +import { InputDialogFolder } from '../../types' /** * Get list of folders. @@ -11,3 +12,28 @@ export async function getFolders(this: TelegramClient): Promise { + if (typeof folder === 'string' || typeof folder === 'number') { + const folders = await this.getFolders() + const found = folders.find((it) => { + if (it._ === 'dialogFilterDefault') { + return folder === 0 + } + + return it.id === folder || it.title === folder + }) + + if (!found) { + throw new MtArgumentError(`Could not find folder ${folder}`) + } + + return found + } + + return folder +} diff --git a/packages/client/src/methods/dialogs/get-peer-dialogs.ts b/packages/client/src/methods/dialogs/get-peer-dialogs.ts index 428999b7..048331f2 100644 --- a/packages/client/src/methods/dialogs/get-peer-dialogs.ts +++ b/packages/client/src/methods/dialogs/get-peer-dialogs.ts @@ -38,7 +38,7 @@ export async function getPeerDialogs( ), }) - const dialogs = this._parseDialogs(res) + const dialogs = Dialog.parseTlDialogs(this, res) return isSingle ? dialogs[0] : dialogs } diff --git a/packages/client/src/methods/dialogs/get-dialogs.ts b/packages/client/src/methods/dialogs/iter-dialogs.ts similarity index 66% rename from packages/client/src/methods/dialogs/get-dialogs.ts rename to packages/client/src/methods/dialogs/iter-dialogs.ts index 9f60ae9e..29452b86 100644 --- a/packages/client/src/methods/dialogs/get-dialogs.ts +++ b/packages/client/src/methods/dialogs/iter-dialogs.ts @@ -1,15 +1,15 @@ import Long from 'long' -import { MtArgumentError, tl } from '@mtcute/core' +import { MtUnsupportedError, tl } from '@mtcute/core' import { TelegramClient } from '../../client' -import { Dialog } from '../../types' +import { Dialog, InputDialogFolder } from '../../types' import { normalizeDate } from '../../utils/misc-utils' /** * Iterate over dialogs. * - * Note that due to Telegram limitations, + * Note that due to Telegram API limitations, * ordering here can only be anti-chronological * (i.e. newest - first), and draft update date * is not considered when sorting. @@ -17,7 +17,7 @@ import { normalizeDate } from '../../utils/misc-utils' * @param params Fetch parameters * @internal */ -export async function* getDialogs( +export async function* iterDialogs( this: TelegramClient, params?: { /** @@ -38,7 +38,7 @@ export async function* getDialogs( /** * Limits the number of dialogs to be received. * - * Defaults to `Infinity`, i.e. all dialogs are fetched, ignored when `pinned=only` + * @default `Infinity`, i.e. all dialogs are fetched */ limit?: number @@ -46,7 +46,7 @@ export async function* getDialogs( * Chunk size which will be passed to `messages.getDialogs`. * You shouldn't usually care about this. * - * Defaults to 100. + * @default 100. */ chunkSize?: number @@ -60,11 +60,11 @@ export async function* getDialogs( * `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. + * + * @default `include`. */ pinned?: 'include' | 'exclude' | 'only' | 'keep' @@ -75,11 +75,13 @@ export async function* getDialogs( * `exclude` them from the list, or `only` * return archived dialogs * - * Defaults to `exclude`, ignored for folders since folders + * Ignored for folders, since folders * themselves contain information about archived chats. * - * > **Note**: when fetching `only` pinned dialogs - * > passing `keep` will act as passing `only` + * > **Note**: when `pinned=only`, `archived=keep` will act as `only` + * > because of Telegram API limitations. + * + * @default `exclude` */ archived?: 'keep' | 'exclude' | 'only' @@ -102,9 +104,9 @@ export async function* getDialogs( * When a folder with given ID or title is not found, * {@link MtArgumentError} is thrown * - * By default fetches from "All" folder + * @default (fetches from "All" folder) */ - folder?: string | number | tl.RawDialogFilter + folder?: InputDialogFolder /** * Additional filtering for the dialogs. @@ -118,49 +120,59 @@ export async function* getDialogs( ): AsyncIterableIterator { if (!params) params = {} - // fetch folder if needed - let filters: tl.TypeDialogFilter | undefined + const { limit = Infinity, chunkSize = 100, folder, filter, pinned = 'include' } = params - if (typeof params.folder === 'string' || typeof params.folder === 'number') { - const folders = await this.getFolders() - const found = folders.find((it) => { - if (it._ === 'dialogFilterDefault') { - return params!.folder === 0 - } + let { offsetId = 0, offsetPeer = { _: 'inputPeerEmpty' }, archived = 'exclude' } = params - return it.id === params!.folder || it.title === params!.folder - }) + let offsetDate = normalizeDate(params.offsetDate) ?? 0 - if (!found) { - throw new MtArgumentError(`Could not find folder ${params.folder}`) - } + let localFilters_: tl.TypeDialogFilter | undefined - filters = found as tl.RawDialogFilter - } else { - filters = params.folder + if (folder) { + localFilters_ = await this._normalizeInputFolder(folder) } - if (params.filter) { - if (filters) { - filters = { - ...filters, - ...params.filter, + if (filter) { + if (localFilters_ && localFilters_._ !== 'dialogFilterDefault') { + localFilters_ = { + ...localFilters_, + ...filter, } } else { - filters = { - _: 'dialogFilterDefault', + localFilters_ = { + _: 'dialogFilter', + id: 0, + title: '', + pinnedPeers: [], + includePeers: [], + excludePeers: [], + ...params.filter, } } } + if (localFilters_?._ === 'dialogFilterDefault') { + localFilters_ = undefined + } + + if (localFilters_?._ === 'dialogFilterChatlist') { + throw new MtUnsupportedError('Shared chat folders are not supported yet') + } + + const localFilters = localFilters_ + + if (localFilters) { + archived = localFilters.excludeArchived ? 'exclude' : 'keep' + } + const fetchPinnedDialogsFromFolder = async (): Promise => { - if (!filters || filters._ === 'dialogFilterDefault' || !filters.pinnedPeers.length) { + if (!localFilters || !localFilters.pinnedPeers.length) { return null } const res = await this.call({ _: 'messages.getPeerDialogs', - peers: filters.pinnedPeers.map((peer) => ({ - _: 'inputDialogPeer', + peers: localFilters.pinnedPeers.map((peer) => ({ + _: 'inputDialogPeer' as const, peer, })), }) @@ -170,17 +182,10 @@ export async function* getDialogs( return res } - const pinned = params.pinned ?? 'include' - let archived = params.archived ?? 'exclude' - - if (filters) { - archived = filters._ !== 'dialogFilterDefault' && filters.excludeArchived ? 'exclude' : 'keep' - } - if (pinned === 'only') { let res - if (filters) { + if (localFilters) { res = await fetchPinnedDialogsFromFolder() } else { res = await this.call({ @@ -188,38 +193,36 @@ export async function* getDialogs( folderId: archived === 'exclude' ? 0 : 1, }) } - if (res) yield* this._parseDialogs(res) + if (res) yield* Dialog.parseTlDialogs(this, res, limit) 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._ !== 'dialogFilterDefault' && filters.pinnedPeers.length && pinned === 'include') { + if ( + localFilters?.pinnedPeers.length && + pinned === 'include' && + offsetId === 0 && + offsetDate === 0 && + offsetPeer._ === 'inputPeerEmpty' + ) { const res = await fetchPinnedDialogsFromFolder() if (res) { - const dialogs = this._parseDialogs(res) + const dialogs = Dialog.parseTlDialogs(this, res, limit) for (const d of dialogs) { yield d - - if (++current >= total) return + if (++current >= limit) return } } } - // 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 - const filterFolder = filters ? Dialog.filterFolder(filters, pinned !== 'keep') : undefined + const filterFolder = localFilters ? Dialog.filterFolder(localFilters, pinned !== 'keep') : undefined let folderId @@ -232,7 +235,8 @@ export async function* getDialogs( } for (;;) { - const dialogs = this._parseDialogs( + const dialogs = Dialog.parseTlDialogs( + this, await this.call({ _: 'messages.getDialogs', excludePinned: params.pinned === 'exclude', @@ -240,8 +244,7 @@ export async function* getDialogs( offsetDate, offsetId, offsetPeer, - - limit: chunkSize, + limit: filterFolder ? chunkSize : Math.min(limit - current, chunkSize), hash: Long.ZERO, }), ) @@ -250,13 +253,13 @@ export async function* getDialogs( const last = dialogs[dialogs.length - 1] offsetPeer = last.chat.inputPeer offsetId = last.raw.topMessage - offsetDate = normalizeDate(last.lastMessage?.date) ?? 0 + offsetDate = normalizeDate(last.lastMessage.date)! for (const d of dialogs) { if (filterFolder && !filterFolder(d)) continue yield d - if (++current >= total) return + if (++current >= limit) return } } } diff --git a/packages/client/src/methods/dialogs/parse-dialogs.ts b/packages/client/src/methods/dialogs/parse-dialogs.ts deleted file mode 100644 index 20f0c9db..00000000 --- a/packages/client/src/methods/dialogs/parse-dialogs.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { getMarkedPeerId, tl } from '@mtcute/core' -import { assertTypeIsNot } from '@mtcute/core/utils' - -import { TelegramClient } from '../../client' -import { Dialog, PeersIndex } from '../../types' - -/** @internal */ -export function _parseDialogs( - this: TelegramClient, - res: tl.messages.TypeDialogs | tl.messages.TypePeerDialogs, -): Dialog[] { - assertTypeIsNot('parseDialogs', res, 'messages.dialogsNotModified') - - const peers = PeersIndex.from(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, peers, messages)) -} diff --git a/packages/client/src/methods/invite-links/get-invite-link-members.ts b/packages/client/src/methods/invite-links/get-invite-link-members.ts index 28ce69c1..0e1c545f 100644 --- a/packages/client/src/methods/invite-links/get-invite-link-members.ts +++ b/packages/client/src/methods/invite-links/get-invite-link-members.ts @@ -1,7 +1,8 @@ import { tl } from '@mtcute/core' import { TelegramClient } from '../../client' -import { ChatInviteLinkJoinedMember, InputPeerLike, PeersIndex, User } from '../../types' +import { ArrayWithTotal, ChatInviteLinkMember, InputPeerLike, PeersIndex } from '../../types' +import { makeArrayWithTotal, normalizeDate } from '../../utils' /** * Iterate over users who have joined @@ -11,7 +12,7 @@ import { ChatInviteLinkJoinedMember, InputPeerLike, PeersIndex, User } from '../ * @param params Additional params * @internal */ -export async function* getInviteLinkMembers( +export async function getInviteLinkMembers( this: TelegramClient, chatId: InputPeerLike, params: { @@ -21,10 +22,22 @@ export async function* getInviteLinkMembers( link?: string /** - * Maximum number of users to return (by default returns all) + * Maximum number of users to return + * + * @default 100 */ limit?: number + /** + * Offset request/join date used as an anchor for pagination. + */ + offsetDate?: Date | number + + /** + * Offset user used as an anchor for pagination + */ + offsetUser?: tl.TypeInputUser + /** * Whether to get users who have requested to join * the chat but weren't accepted yet @@ -33,59 +46,36 @@ export async function* getInviteLinkMembers( /** * Search for a user in the pending join requests list - * (only works if {@link requested} is true) + * (if passed, {@link requested} is assumed to be true) * * Doesn't work when {@link link} is set (Telegram limitation) */ requestedSearch?: string }, -): AsyncIterableIterator { +): Promise> { const peer = await this.resolvePeer(chatId) - const limit = params.limit ?? Infinity - let current = 0 + const { limit = 100, link, requestedSearch, requested = Boolean(requestedSearch) } = params - let offsetDate = 0 - let offsetUser: tl.TypeInputUser = { _: 'inputUserEmpty' } + const { offsetUser = { _: 'inputUserEmpty' } } = params - for (;;) { - // for some reason ts needs annotation, idk - const res: tl.RpcCallReturn['messages.getChatInviteImporters'] = await this.call({ - _: 'messages.getChatInviteImporters', - limit: Math.min(100, limit - current), - peer, - link: params.link, - requested: params.requested, - q: params.requestedSearch, - offsetDate, - offsetUser, - }) + const offsetDate = normalizeDate(params.offsetDate) ?? 0 - if (!res.importers.length) break + const res = await this.call({ + _: 'messages.getChatInviteImporters', + limit, + peer, + link, + requested, + q: requestedSearch, + offsetDate, + offsetUser, + }) - const peers = PeersIndex.from(res) + const peers = PeersIndex.from(res) - const last = res.importers[res.importers.length - 1] - offsetDate = last.date - offsetUser = { - _: 'inputUser', - userId: last.userId, - accessHash: (peers.user(last.userId) as tl.RawUser).accessHash!, - } - - for (const it of res.importers) { - const user = new User(this, peers.user(it.userId)) - - yield { - user, - date: new Date(it.date * 1000), - isPendingRequest: it.requested!, - bio: it.about, - approvedBy: it.approvedBy, - } - } - - current += res.importers.length - if (current >= limit) break - } + return makeArrayWithTotal( + res.importers.map((it) => new ChatInviteLinkMember(this, it, peers)), + res.count, + ) } diff --git a/packages/client/src/methods/invite-links/get-invite-links.ts b/packages/client/src/methods/invite-links/get-invite-links.ts index a78d80f0..9db2b815 100644 --- a/packages/client/src/methods/invite-links/get-invite-links.ts +++ b/packages/client/src/methods/invite-links/get-invite-links.ts @@ -1,8 +1,6 @@ -import { tl } from '@mtcute/core' -import { assertTypeIsNot } from '@mtcute/core/utils' - import { TelegramClient } from '../../client' -import { ChatInviteLink, InputPeerLike, PeersIndex } from '../../types' +import { ArrayWithTotal, ChatInviteLink, InputPeerLike, PeersIndex } from '../../types' +import { makeArrayWithTotal, normalizeDate } from '../../utils' import { normalizeToInputUser } from '../../utils/peer-utils' /** @@ -17,11 +15,17 @@ import { normalizeToInputUser } from '../../utils/peer-utils' * @param params * @internal */ -export async function* getInviteLinks( +export async function getInviteLinks( this: TelegramClient, chatId: InputPeerLike, - adminId: InputPeerLike, params?: { + /** + * Only return this admin's links. + * + * @default `"self"` + */ + admin?: InputPeerLike + /** * Whether to fetch revoked invite links */ @@ -29,55 +33,43 @@ export async function* getInviteLinks( /** * Limit the number of invite links to be fetched. - * By default, all links are fetched. + * + * @default 100 */ limit?: number /** - * Size of chunks which are fetched. Usually not needed. - * - * Defaults to `100` + * Offset date used as an anchor for pagination. */ - chunkSize?: number + offsetDate?: Date | number + + /** + * Offset link used as an anchor for pagination + */ + offsetLink?: string }, -): AsyncIterableIterator { +): Promise> { if (!params) params = {} - let current = 0 - const total = params.limit || Infinity - const chunkSize = Math.min(params.chunkSize ?? 100, total) + const { revoked = false, limit = Infinity, admin } = params - const peer = await this.resolvePeer(chatId) - const admin = normalizeToInputUser(await this.resolvePeer(adminId), adminId) + const offsetDate = normalizeDate(params.offsetDate) + const offsetLink = params.offsetLink - let offsetDate: number | undefined = undefined - let offsetLink: string | undefined = undefined + const res = await this.call({ + _: 'messages.getExportedChatInvites', + peer: await this.resolvePeer(chatId), + revoked, + adminId: admin ? normalizeToInputUser(await this.resolvePeer(admin), admin) : { _: 'inputUserSelf' }, + limit, + offsetDate, + offsetLink, + }) - for (;;) { - const res: tl.RpcCallReturn['messages.getExportedChatInvites'] = await this.call({ - _: 'messages.getExportedChatInvites', - peer, - adminId: admin, - limit: Math.min(chunkSize, total - current), - offsetDate, - offsetLink, - }) + const peers = PeersIndex.from(res) - if (!res.invites.length) break - - const peers = PeersIndex.from(res) - - const last = res.invites[res.invites.length - 1] - - assertTypeIsNot('getInviteLinks', last, 'chatInvitePublicJoinRequests') - offsetDate = last.date - offsetLink = last.link - - for (const it of res.invites) { - yield new ChatInviteLink(this, it, peers) - } - - current += res.invites.length - if (current >= total) break - } + return makeArrayWithTotal( + res.invites.map((it) => new ChatInviteLink(this, it, peers)), + res.count, + ) } diff --git a/packages/client/src/methods/invite-links/iter-invite-link-members.ts b/packages/client/src/methods/invite-links/iter-invite-link-members.ts new file mode 100644 index 00000000..ef7a05ea --- /dev/null +++ b/packages/client/src/methods/invite-links/iter-invite-link-members.ts @@ -0,0 +1,63 @@ +import { TelegramClient } from '../../client' +import { ChatInviteLinkMember, InputPeerLike } from '../../types' +import { normalizeToInputUser } from '../../utils' + +/** + * Iterate over users who have joined + * the chat with the given invite link. + * + * @param chatId Chat ID + * @param params Additional params + * @internal + */ +export async function* iterInviteLinkMembers( + this: TelegramClient, + chatId: InputPeerLike, + params: Parameters[1] & { + /** + * Maximum number of users to return + * + * @default `Infinity`, i.e. all users are fetched + */ + limit?: number + + /** + * Chunk size which will be passed to `messages.getChatInviteImporters`. + * You shouldn't usually care about this. + * + * @default 100. + */ + chunkSize?: number + }, +): AsyncIterableIterator { + const peer = await this.resolvePeer(chatId) + + const { limit = Infinity, chunkSize = 100, link, requestedSearch, requested = Boolean(requestedSearch) } = params + + let { offsetDate, offsetUser = { _: 'inputUserEmpty' } } = params + + let current = 0 + + for (;;) { + const items = await this.getInviteLinkMembers(peer, { + limit: Math.min(chunkSize, limit - current), + link, + requested, + requestedSearch, + offsetDate, + offsetUser, + }) + + if (!items.length) break + + const last = items[items.length - 1] + offsetDate = last.date + offsetUser = normalizeToInputUser(last.user.inputPeer) + + for (const it of items) { + yield it + + if (++current >= limit) return + } + } +} diff --git a/packages/client/src/methods/invite-links/iter-invite-links.ts b/packages/client/src/methods/invite-links/iter-invite-links.ts new file mode 100644 index 00000000..eec9e9ce --- /dev/null +++ b/packages/client/src/methods/invite-links/iter-invite-links.ts @@ -0,0 +1,67 @@ +import { TelegramClient } from '../../client' +import { ChatInviteLink, InputPeerLike } from '../../types' + +/** + * Iterate over invite links created by some administrator in the chat. + * + * As an administrator you can only get your own links + * (i.e. `adminId = "self"`), as a creator you can get + * any other admin's links. + * + * @param chatId Chat ID + * @param adminId Admin who created the links + * @param params + * @internal + */ +export async function* iterInviteLinks( + this: TelegramClient, + chatId: InputPeerLike, + params?: Parameters[1] & { + /** + * Limit the number of invite links to be fetched. + * By default, all links are fetched. + */ + limit?: number + + /** + * Size of chunks which are fetched. Usually not needed. + * + * Defaults to `100` + */ + chunkSize?: number + }, +): AsyncIterableIterator { + if (!params) params = {} + + const { revoked = false, limit = Infinity, chunkSize = 100, admin } = params + + let { offsetDate, offsetLink } = params + + let current = 0 + + const peer = await this.resolvePeer(chatId) + const adminResolved = admin ? await this.resolvePeer(admin) : ({ _: 'inputUserSelf' } as const) + + for (;;) { + const links = await this.getInviteLinks(peer, { + admin: adminResolved, + revoked, + limit: Math.min(chunkSize, limit - current), + offsetDate, + offsetLink, + }) + + if (!links.length) return + + const last = links[links.length - 1] + + offsetDate = last.date + offsetLink = last.link + + for (const link of links) { + yield link + + if (++current >= limit) break + } + } +} diff --git a/packages/client/src/types/messages/dialog.ts b/packages/client/src/types/messages/dialog.ts index fd45e713..3946c5ea 100644 --- a/packages/client/src/types/messages/dialog.ts +++ b/packages/client/src/types/messages/dialog.ts @@ -1,12 +1,19 @@ import { getMarkedPeerId, tl } from '@mtcute/core' import { TelegramClient } from '../../client' -import { makeInspectable } from '../../utils' +import { assertTypeIsNot, hasValueAtKey, makeInspectable } from '../../utils' import { MtMessageNotFoundError } from '../errors' import { Chat, PeersIndex } from '../peers' import { DraftMessage } from './draft-message' import { Message } from './message' +/** + * Type used as an input for a folder in client methods + * + * You can pass folder object, id or title + */ +export type InputDialogFolder = string | number | tl.RawDialogFilter + /** * A dialog. * @@ -21,6 +28,40 @@ export class Dialog { readonly _messages: Record, ) {} + /** + * Parse a list of dialogs from a TL object + * + * @param client Client instance + * @param dialogs TL object + * @param limit Maximum number of dialogs to parse + */ + static parseTlDialogs( + client: TelegramClient, + dialogs: tl.messages.TypeDialogs | tl.messages.TypePeerDialogs, + limit?: number, + ): Dialog[] { + assertTypeIsNot('parseDialogs', dialogs, 'messages.dialogsNotModified') + + const peers = PeersIndex.from(dialogs) + + const messages: Record = {} + dialogs.messages.forEach((msg) => { + if (!msg.peerId) return + + messages[getMarkedPeerId(msg.peerId)] = msg + }) + + const arr = dialogs.dialogs + .filter(hasValueAtKey('_', 'dialog')) + .map((it) => new Dialog(client, it, peers, messages)) + + if (limit) { + return arr.slice(0, limit) + } + + return arr + } + /** * Find pinned dialogs from a list of dialogs * @@ -183,7 +224,7 @@ export class Dialog { /** * The latest message sent in this chat */ - get lastMessage(): Message | null { + get lastMessage(): Message { if (!this._lastMessage) { const cid = this.chat.id diff --git a/packages/client/src/types/peers/chat-event.ts b/packages/client/src/types/peers/chat-event/actions.ts similarity index 80% rename from packages/client/src/types/peers/chat-event.ts rename to packages/client/src/types/peers/chat-event/actions.ts index 2087d480..b2fa9006 100644 --- a/packages/client/src/types/peers/chat-event.ts +++ b/packages/client/src/types/peers/chat-event/actions.ts @@ -1,15 +1,12 @@ -import { tl, toggleChannelIdMark } from '@mtcute/core' +import { tl } from '@mtcute/core' -import { TelegramClient } from '../../client' -import { makeInspectable } from '../../utils' -import { Photo } from '../media' -import { Message } from '../messages' -import { ChatInviteLink } from './chat-invite-link' -import { ChatLocation } from './chat-location' -import { ChatMember } from './chat-member' -import { ChatPermissions } from './chat-permissions' -import { PeersIndex } from './index' -import { User } from './user' +import { PeersIndex, TelegramClient, toggleChannelIdMark } from '../../..' +import { Photo } from '../../media' +import { Message } from '../../messages' +import { ChatInviteLink } from '../chat-invite-link' +import { ChatLocation } from '../chat-location' +import { ChatMember } from '../chat-member' +import { ChatPermissions } from '../chat-permissions' /** A user has joined the group (in the case of big groups, info of the user that has joined isn't shown) */ export interface ChatActionUserJoined { @@ -321,7 +318,28 @@ export type ChatAction = | ChatActionTtlChanged | null -function _actionFromTl(this: ChatEvent, e: tl.TypeChannelAdminLogEventAction): ChatAction { +/** @internal */ +export function _actionFromTl( + e: tl.TypeChannelAdminLogEventAction, + client: TelegramClient, + peers: PeersIndex, +): ChatAction { + // todo - MTQ-78 + // channelAdminLogEventActionParticipantJoinByRequest#afb6144a invite:ExportedChatInvite approved_by:long + // channelAdminLogEventActionToggleNoForwards#cb2ac766 new_value:Bool = ChannelAdminLogEventAction; + // todo - MTQ-57 + // channelAdminLogEventActionChangeUsernames#f04fb3a9 prev_value:Vector new_value:Vector + // todo - MTQ-77 + // channelAdminLogEventActionToggleForum#2cc6383 new_value:Bool = ChannelAdminLogEventAction; + // channelAdminLogEventActionCreateTopic#58707d28 topic:ForumTopic = ChannelAdminLogEventAction; + // channelAdminLogEventActionEditTopic#f06fe208 prev_topic:ForumTopic new_topic:ForumTopic + // channelAdminLogEventActionDeleteTopic#ae168909 topic:ForumTopic = ChannelAdminLogEventAction; + // channelAdminLogEventActionPinTopic#5d8d353b flags:# prev_topic:flags.0?ForumTopic new_topic:flags.1?ForumTopic + // todo - MTQ-72 + // channelAdminLogEventActionSendMessage#278f2868 message:Message = ChannelAdminLogEventAction; + // channelAdminLogEventActionChangeAvailableReactions#be4e0ef8 prev_value:ChatReactions new_value:ChatReactions + // channelAdminLogEventActionToggleAntiSpam#64f36dfc new_value:Bool = ChannelAdminLogEventAction; + switch (e._) { case 'channelAdminLogEventActionParticipantJoin': return { type: 'user_joined' } @@ -346,8 +364,8 @@ function _actionFromTl(this: ChatEvent, e: tl.TypeChannelAdminLogEventAction): C case 'channelAdminLogEventActionChangePhoto': return { type: 'photo_changed', - old: new Photo(this.client, e.prevPhoto as tl.RawPhoto), - new: new Photo(this.client, e.newPhoto as tl.RawPhoto), + old: new Photo(client, e.prevPhoto as tl.RawPhoto), + new: new Photo(client, e.newPhoto as tl.RawPhoto), } case 'channelAdminLogEventActionToggleInvites': return { @@ -364,37 +382,37 @@ function _actionFromTl(this: ChatEvent, e: tl.TypeChannelAdminLogEventAction): C case 'channelAdminLogEventActionUpdatePinned': return { type: 'msg_pinned', - message: new Message(this.client, e.message, this._peers), + message: new Message(client, e.message, peers), } case 'channelAdminLogEventActionEditMessage': return { type: 'msg_edited', - old: new Message(this.client, e.prevMessage, this._peers), - new: new Message(this.client, e.newMessage, this._peers), + old: new Message(client, e.prevMessage, peers), + new: new Message(client, e.newMessage, peers), } case 'channelAdminLogEventActionDeleteMessage': return { type: 'msg_deleted', - message: new Message(this.client, e.message, this._peers), + message: new Message(client, e.message, peers), } case 'channelAdminLogEventActionParticipantLeave': return { type: 'user_left' } case 'channelAdminLogEventActionParticipantInvite': return { type: 'user_invited', - member: new ChatMember(this.client, e.participant, this._peers), + member: new ChatMember(client, e.participant, peers), } case 'channelAdminLogEventActionParticipantToggleBan': return { type: 'user_perms_changed', - old: new ChatMember(this.client, e.prevParticipant, this._peers), - new: new ChatMember(this.client, e.newParticipant, this._peers), + old: new ChatMember(client, e.prevParticipant, peers), + new: new ChatMember(client, e.newParticipant, peers), } case 'channelAdminLogEventActionParticipantToggleAdmin': return { type: 'user_admin_perms_changed', - old: new ChatMember(this.client, e.prevParticipant, this._peers), - new: new ChatMember(this.client, e.newParticipant, this._peers), + old: new ChatMember(client, e.prevParticipant, peers), + new: new ChatMember(client, e.newParticipant, peers), } case 'channelAdminLogEventActionChangeStickerSet': return { @@ -417,7 +435,7 @@ function _actionFromTl(this: ChatEvent, e: tl.TypeChannelAdminLogEventAction): C case 'channelAdminLogEventActionStopPoll': return { type: 'poll_stopped', - message: new Message(this.client, e.message, this._peers), + message: new Message(client, e.message, peers), } case 'channelAdminLogEventActionChangeLinkedChat': return { @@ -428,8 +446,8 @@ function _actionFromTl(this: ChatEvent, e: tl.TypeChannelAdminLogEventAction): C case 'channelAdminLogEventActionChangeLocation': return { type: 'location_changed', - old: e.prevValue._ === 'channelLocationEmpty' ? null : new ChatLocation(this.client, e.prevValue), - new: e.newValue._ === 'channelLocationEmpty' ? null : new ChatLocation(this.client, e.newValue), + old: e.prevValue._ === 'channelLocationEmpty' ? null : new ChatLocation(client, e.prevValue), + new: e.newValue._ === 'channelLocationEmpty' ? null : new ChatLocation(client, e.newValue), } case 'channelAdminLogEventActionToggleSlowMode': return { @@ -460,23 +478,23 @@ function _actionFromTl(this: ChatEvent, e: tl.TypeChannelAdminLogEventAction): C case 'channelAdminLogEventActionParticipantJoinByInvite': return { type: 'user_joined_invite', - link: new ChatInviteLink(this.client, e.invite, this._peers), + link: new ChatInviteLink(client, e.invite, peers), } case 'channelAdminLogEventActionExportedInviteDelete': return { type: 'invite_deleted', - link: new ChatInviteLink(this.client, e.invite, this._peers), + link: new ChatInviteLink(client, e.invite, peers), } case 'channelAdminLogEventActionExportedInviteRevoke': return { type: 'invite_revoked', - link: new ChatInviteLink(this.client, e.invite, this._peers), + link: new ChatInviteLink(client, e.invite, peers), } case 'channelAdminLogEventActionExportedInviteEdit': return { type: 'invite_edited', - old: new ChatInviteLink(this.client, e.prevInvite, this._peers), - new: new ChatInviteLink(this.client, e.newInvite, this._peers), + old: new ChatInviteLink(client, e.prevInvite, peers), + new: new ChatInviteLink(client, e.newInvite, peers), } case 'channelAdminLogEventActionChangeHistoryTTL': return { @@ -488,43 +506,3 @@ function _actionFromTl(this: ChatEvent, e: tl.TypeChannelAdminLogEventAction): C return null } } - -export class ChatEvent { - constructor( - readonly client: TelegramClient, - readonly raw: tl.TypeChannelAdminLogEvent, - readonly _peers: PeersIndex, - ) {} - - /** - * Event ID. - * - * Event IDs are generated in direct chronological order - * (i.e. newer events have bigger event ID) - */ - get id(): tl.Long { - return this.raw.id - } - - /** - * Date of the event - */ - get date(): Date { - return new Date(this.raw.date * 1000) - } - - private _actor?: User - /** - * Actor of the event - */ - get actor(): User { - return (this._actor ??= new User(this.client, this._peers.user(this.raw.userId))) - } - - private _action?: ChatAction - get action(): ChatAction { - return (this._action ??= _actionFromTl.call(this, this.raw.action)) - } -} - -makeInspectable(ChatEvent) diff --git a/packages/client/src/types/peers/chat-event/filters.ts b/packages/client/src/types/peers/chat-event/filters.ts new file mode 100644 index 00000000..936194b3 --- /dev/null +++ b/packages/client/src/types/peers/chat-event/filters.ts @@ -0,0 +1,123 @@ +import { assertNever, MaybeArray, tl } from '@mtcute/core' + +import { ChatAction } from './actions' + +export interface ChatEventFilters { + serverFilter?: tl.TypeChannelAdminLogEventsFilter + localFilter?: Record +} + +export type InputChatEventFilters = + | ChatEventFilters + | tl.TypeChannelAdminLogEventsFilter + | MaybeArray['type']> + | undefined + +/** @internal */ +export function normalizeChatEventFilters(input: InputChatEventFilters): ChatEventFilters { + if (!input) { + return {} + } + + if (typeof input === 'string' || Array.isArray(input)) { + if (!Array.isArray(input)) input = [input] + + const serverFilter: tl.Mutable = { + _: 'channelAdminLogEventsFilter', + } + const localFilter: Record = {} + + input.forEach((type) => { + localFilter[type] = true + + switch (type) { + case 'user_joined': + serverFilter.join = true + break + case 'user_left': + serverFilter.leave = true + break + case 'user_invited': + serverFilter.invite = true + break + case 'title_changed': + case 'description_changed': + case 'linked_chat_changed': + case 'location_changed': + case 'photo_changed': + case 'username_changed': + case 'stickerset_changed': + serverFilter.info = true + break + case 'invites_toggled': + case 'history_toggled': + case 'signatures_toggled': + case 'def_perms_changed': + serverFilter.settings = true + break + case 'msg_pinned': + serverFilter.pinned = true + break + case 'msg_edited': + case 'poll_stopped': + serverFilter.edit = true + break + case 'msg_deleted': + serverFilter.delete = true + break + case 'user_perms_changed': + serverFilter.ban = true + serverFilter.unban = true + serverFilter.kick = true + serverFilter.unkick = true + break + case 'user_admin_perms_changed': + serverFilter.promote = true + serverFilter.demote = true + break + case 'slow_mode_changed': + case 'ttl_changed': + // not documented so idk, enable both + serverFilter.settings = true + serverFilter.info = true + break + case 'call_started': + case 'call_ended': + serverFilter.groupCall = true + break + case 'call_setting_changed': + // not documented so idk, enable all + serverFilter.groupCall = true + serverFilter.settings = true + serverFilter.info = true + break + case 'user_joined_invite': + // not documented so idk, enable all + serverFilter.join = true + serverFilter.invite = true + serverFilter.invites = true + break + case 'invite_deleted': + case 'invite_edited': + case 'invite_revoked': + serverFilter.invites = true + break + default: + assertNever(type) + } + }) + + return { + serverFilter, + localFilter, + } + } + + if ('_' in input) { + return { + serverFilter: input, + } + } + + return input +} diff --git a/packages/client/src/types/peers/chat-event/index.ts b/packages/client/src/types/peers/chat-event/index.ts new file mode 100644 index 00000000..5150ea6f --- /dev/null +++ b/packages/client/src/types/peers/chat-event/index.ts @@ -0,0 +1,50 @@ +import { tl } from '@mtcute/core' + +import { TelegramClient } from '../../../client' +import { makeInspectable } from '../../../utils' +import { PeersIndex } from '../peers-index' +import { User } from '../user' +import { _actionFromTl, ChatAction } from './actions' + +export * from './actions' +export { InputChatEventFilters } from './filters' + +export class ChatEvent { + constructor( + readonly client: TelegramClient, + readonly raw: tl.TypeChannelAdminLogEvent, + readonly _peers: PeersIndex, + ) {} + + /** + * Event ID. + * + * Event IDs are generated in direct chronological order + * (i.e. newer events have bigger event ID) + */ + get id(): tl.Long { + return this.raw.id + } + + /** + * Date of the event + */ + get date(): Date { + return new Date(this.raw.date * 1000) + } + + private _actor?: User + /** + * Actor of the event + */ + get actor(): User { + return (this._actor ??= new User(this.client, this._peers.user(this.raw.userId))) + } + + private _action?: ChatAction + get action(): ChatAction { + return (this._action ??= _actionFromTl(this.raw.action, this.client, this._peers)) + } +} + +makeInspectable(ChatEvent) diff --git a/packages/client/src/types/peers/chat-invite-link-member.ts b/packages/client/src/types/peers/chat-invite-link-member.ts new file mode 100644 index 00000000..3e2e0aaf --- /dev/null +++ b/packages/client/src/types/peers/chat-invite-link-member.ts @@ -0,0 +1,59 @@ +import { tl } from '@mtcute/core' + +import { TelegramClient } from '../../client' +import { makeInspectable } from '../../utils' +import { PeersIndex } from './peers-index' +import { User } from './user' + +export class ChatInviteLinkMember { + constructor(readonly client: TelegramClient, readonly raw: tl.RawChatInviteImporter, readonly _peers: PeersIndex) {} + + private _user?: User + /** + * User who joined the chat + */ + get user(): User { + return (this._user ??= new User(this.client, this._peers.user(this.raw.userId))) + } + + /** + * Date when the user joined the chat + */ + get date(): Date { + return new Date(this.raw.date * 1000) + } + + /** + * Whether this user currently has a pending join request + * (and is actually not a member yet) + */ + get isPendingRequest(): boolean { + return this.raw.requested! + } + + /** + * Whether the participant joined by importing a chat folder deep link + */ + get isViaChatlist(): boolean { + return this.raw.requested! + } + + /** + * For users with pending requests, contains bio of the user that requested to join + */ + get bio(): string | null { + return this.raw.about ?? null + } + + private _approvedBy?: User + /** + * The administrator that approved the join request of the user + */ + get approvedBy(): User | null { + if (!this.raw.approvedBy) return null + + return (this._approvedBy ??= new User(this.client, this._peers.user(this.raw.approvedBy))) + } +} + +makeInspectable(ChatInviteLinkMember) diff --git a/packages/client/src/types/peers/chat-invite-link.ts b/packages/client/src/types/peers/chat-invite-link.ts index 9c7a5b5b..a4f5d1dc 100644 --- a/packages/client/src/types/peers/chat-invite-link.ts +++ b/packages/client/src/types/peers/chat-invite-link.ts @@ -6,32 +6,6 @@ import { makeInspectable } from '../../utils' import { PeersIndex } from './index' import { User } from './user' -export interface ChatInviteLinkJoinedMember { - /** - * User who joined the chat - */ - user: User - - /** - * Date when the user joined the chat - */ - date: Date - - /** - * Whether the user currently has a pending join request - */ - isPendingRequest: boolean - /** - * For users with pending requests, - * contains bio of the user that requested to join - */ - bio?: string - /** - * The administrator that approved the join request of the user - */ - approvedBy?: number -} - /** * An invite link */ diff --git a/packages/client/src/types/peers/index.ts b/packages/client/src/types/peers/index.ts index 1a3e1774..49a5cb58 100644 --- a/packages/client/src/types/peers/index.ts +++ b/packages/client/src/types/peers/index.ts @@ -3,6 +3,7 @@ import { tl } from '@mtcute/core' export * from './chat' export * from './chat-event' export * from './chat-invite-link' +export * from './chat-invite-link-member' export * from './chat-location' export * from './chat-member' export * from './chat-permissions' diff --git a/packages/client/src/types/updates/chat-join-request.ts b/packages/client/src/types/updates/chat-join-request.ts index c01db74b..6e2aa9c8 100644 --- a/packages/client/src/types/updates/chat-join-request.ts +++ b/packages/client/src/types/updates/chat-join-request.ts @@ -2,7 +2,7 @@ import { getBarePeerId, tl } from '@mtcute/core' import { TelegramClient } from '../../client' import { makeInspectable } from '../../utils' -import { ChatInviteLinkJoinedMember, PeersIndex, User } from '../peers' +import { PeersIndex, User } from '../peers' /** * This update is sent when a user requests to join a chat @@ -68,17 +68,6 @@ export class ChatJoinRequestUpdate { await this.client.hideJoinRequest(this.chatId, id, action) } } - - /** - * Fetch all pending join requests for this chat - */ - fetchAll(params?: { limit?: number; search?: string }): AsyncIterableIterator { - return this.client.getInviteLinkMembers(this.chatId, { - limit: params?.limit, - requested: true, - requestedSearch: params?.search, - }) - } } makeInspectable(ChatJoinRequestUpdate) diff --git a/packages/client/src/utils/misc-utils.ts b/packages/client/src/utils/misc-utils.ts index e3af8e0b..4db234c3 100644 --- a/packages/client/src/utils/misc-utils.ts +++ b/packages/client/src/utils/misc-utils.ts @@ -18,14 +18,10 @@ export async function resolveMaybeDynamic(val: MaybeDynamic): Promise { } export function makeArrayWithTotal(arr: T[], total: number): ArrayWithTotal { - Object.defineProperty(arr, 'total', { - value: total, - enumerable: false, - configurable: false, - writable: false, - }) + const a = arr as ArrayWithTotal + a.total = total - return arr as ArrayWithTotal + return a } export function extractChannelIdFromUpdate(upd: tl.TypeUpdate): number | undefined { diff --git a/packages/core/src/utils/type-assertions.ts b/packages/core/src/utils/type-assertions.ts index 46d9b85b..6c6c781b 100644 --- a/packages/core/src/utils/type-assertions.ts +++ b/packages/core/src/utils/type-assertions.ts @@ -44,7 +44,7 @@ export function hasPresentKey(k: K) { * files[0].imageUrl // TS will know this is present, because already it excluded the other union members. * ``` */ -export function hasValueAtKey(k: K, v: V) { +export function hasValueAtKey(k: K, v: V) { return function (a: T & { [k in K]: unknown }): a is T & { [k in K]: V } { return a[k] === v }