From ea7eabf0be4152a9332f44eddd3d591eead58ba6 Mon Sep 17 00:00:00 2001 From: Alina Sireneva Date: Mon, 2 Oct 2023 18:00:00 +0300 Subject: [PATCH] feat: more non-iterable versions of methods --- packages/client/scripts/generate-client.js | 54 ++- packages/client/src/client.ts | 427 +++++++++++------- packages/client/src/methods/README.md | 8 +- .../methods/invite-links/get-invite-links.ts | 28 +- .../methods/invite-links/iter-invite-links.ts | 13 +- .../src/methods/messages/get-history.ts | 144 +++--- .../methods/messages/get-reaction-users.ts | 75 ++- .../src/methods/messages/iter-history.ts | 60 +++ .../methods/messages/iter-reaction-users.ts | 60 +++ .../methods/messages/iter-search-global.ts | 65 +++ .../methods/messages/iter-search-messages.ts | 82 ++++ .../src/methods/messages/search-global.ts | 105 +++-- .../src/methods/messages/search-messages.ts | 136 +++--- .../src/methods/users/get-profile-photo.ts | 29 ++ .../src/methods/users/get-profile-photos.ts | 26 +- .../src/methods/users/iter-profile-photos.ts | 52 +-- packages/client/src/types/media/photo.ts | 7 + .../client/src/types/messages/reactions.ts | 15 +- 18 files changed, 902 insertions(+), 484 deletions(-) create mode 100644 packages/client/src/methods/messages/iter-history.ts create mode 100644 packages/client/src/methods/messages/iter-reaction-users.ts create mode 100644 packages/client/src/methods/messages/iter-search-global.ts create mode 100644 packages/client/src/methods/messages/iter-search-messages.ts create mode 100644 packages/client/src/methods/users/get-profile-photo.ts diff --git a/packages/client/scripts/generate-client.js b/packages/client/scripts/generate-client.js index 5199e004..322c42ad 100644 --- a/packages/client/scripts/generate-client.js +++ b/packages/client/scripts/generate-client.js @@ -33,6 +33,7 @@ async function addSingleMethod(state, fileName) { const fileFullText = await fs.promises.readFile(fileName, 'utf-8') const program = ts.createSourceFile(path.basename(fileName), fileFullText, ts.ScriptTarget.ES2018, true) const relPath = path.relative(targetDir, fileName).replace(/\\/g, '/') // replace path delim to unix + const module = `./${relPath.replace(/\.ts$/, '')}` state.files[relPath] = fileFullText @@ -53,6 +54,7 @@ async function addSingleMethod(state, fileName) { for (const stmt of program.statements) { const isCopy = checkForFlag(stmt, '@copy') + const isTypeExported = checkForFlag(stmt, '@exported') if (stmt.kind === ts.SyntaxKind.ImportDeclaration) { if (!isCopy) continue @@ -159,19 +161,6 @@ async function addSingleMethod(state, fileName) { ) } - const returnsExported = ( - stmt.body ? - ts.getLeadingCommentRanges(fileFullText, stmt.body.pos + 2) || - (stmt.statements && - stmt.statements.length && - ts.getLeadingCommentRanges(fileFullText, stmt.statements[0].pos)) || - [] : - [] - ) - .map((range) => fileFullText.substring(range.pos, range.end)) - .join('\n') - .includes('@returns-exported') - // overloads const isOverload = !stmt.body @@ -193,20 +182,11 @@ async function addSingleMethod(state, fileName) { hasOverloads: hasOverloads[name] && !isOverload, }) - const module = `./${relPath.replace(/\.ts$/, '')}` - if (!(module in state.imports)) { state.imports[module] = new Set() } state.imports[module].add(name) - - if (returnsExported) { - let returnType = stmt.type.getText() - let m = returnType.match(/^Promise<(.+)>$/) - if (m) returnType = m[1] - state.imports[module].add(returnType) - } } } else if (stmt.kind === ts.SyntaxKind.InterfaceDeclaration) { if (isCopy) { @@ -217,8 +197,22 @@ async function addSingleMethod(state, fileName) { continue } + const isExported = (stmt.modifiers || []).find((mod) => mod.kind === ts.SyntaxKind.ExportKeyword) + + if (isTypeExported) { + if (!isExported) { + throwError(stmt, fileName, 'Exported interfaces must be exported') + } + + if (!(module in state.imports)) { + state.imports[module] = new Set() + } + + state.imports[module].add(stmt.name.escapedText) + continue + } + if (!checkForFlag(stmt, '@extension')) continue - const isExported = (stmt.modifiers || []).find((mod) => mod.kind === 92 /* ExportKeyword */) if (isExported) { throwError(isExported, fileName, 'Extension interfaces must not be imported') @@ -233,8 +227,22 @@ async function addSingleMethod(state, fileName) { code: member.getText(), }) } + } else if (stmt.kind === ts.SyntaxKind.TypeAliasDeclaration && isTypeExported) { + const isExported = (stmt.modifiers || []).find((mod) => mod.kind === ts.SyntaxKind.ExportKeyword) + + if (!isExported) { + throwError(stmt, fileName, 'Exported type aliases must be exported') + } + + if (!(module in state.imports)) { + state.imports[module] = new Set() + } + + state.imports[module].add(stmt.name.escapedText) } else if (isCopy) { state.copy.push({ from: relPath, code: stmt.getFullText().trim() }) + } else if (isTypeExported) { + throwError(stmt, fileName, 'Only functions and interfaces can be exported') } } } diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 7fac26c4..c66152bc 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -103,7 +103,7 @@ import { editInviteLink } from './methods/invite-links/edit-invite-link' import { exportInviteLink } from './methods/invite-links/export-invite-link' import { getInviteLink } from './methods/invite-links/get-invite-link' import { getInviteLinkMembers } from './methods/invite-links/get-invite-link-members' -import { getInviteLinks } from './methods/invite-links/get-invite-links' +import { getInviteLinks, GetInviteLinksOffset } 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' @@ -118,19 +118,23 @@ import { editMessage } from './methods/messages/edit-message' import { _findMessageInUpdate } from './methods/messages/find-in-update' import { forwardMessages } from './methods/messages/forward-messages' import { _getDiscussionMessage, getDiscussionMessage } from './methods/messages/get-discussion-message' -import { getHistory } from './methods/messages/get-history' +import { getHistory, GetHistoryOffset } from './methods/messages/get-history' import { getMessageGroup } from './methods/messages/get-message-group' import { getMessageReactions } from './methods/messages/get-message-reactions' import { getMessages } from './methods/messages/get-messages' import { getMessagesUnsafe } from './methods/messages/get-messages-unsafe' -import { getReactionUsers } from './methods/messages/get-reaction-users' +import { getReactionUsers, GetReactionUsersOffset } from './methods/messages/get-reaction-users' import { getScheduledMessages } from './methods/messages/get-scheduled-messages' +import { iterHistory } from './methods/messages/iter-history' +import { iterReactionUsers } from './methods/messages/iter-reaction-users' +import { iterSearchGlobal } from './methods/messages/iter-search-global' +import { iterSearchMessages } from './methods/messages/iter-search-messages' import { _parseEntities } from './methods/messages/parse-entities' import { pinMessage } from './methods/messages/pin-message' import { readHistory } from './methods/messages/read-history' import { readReactions } from './methods/messages/read-reactions' -import { searchGlobal } from './methods/messages/search-global' -import { searchMessages } from './methods/messages/search-messages' +import { searchGlobal, SearchGlobalOffset } from './methods/messages/search-global' +import { searchMessages, SearchMessagesOffset } from './methods/messages/search-messages' import { sendCopy } from './methods/messages/send-copy' import { sendMedia } from './methods/messages/send-media' import { sendMediaGroup } from './methods/messages/send-media-group' @@ -183,6 +187,7 @@ import { deleteProfilePhotos } from './methods/users/delete-profile-photos' import { getCommonChats } from './methods/users/get-common-chats' import { getMe } from './methods/users/get-me' import { getMyUsername } from './methods/users/get-my-username' +import { getProfilePhoto } from './methods/users/get-profile-photo' import { getProfilePhotos } from './methods/users/get-profile-photos' import { getUsers } from './methods/users/get-users' import { iterProfilePhotos } from './methods/users/iter-profile-photos' @@ -2055,16 +2060,11 @@ export interface TelegramClient extends BaseTelegramClient { limit?: number /** - * Offset date used as an anchor for pagination. + * Offset for pagination. */ - offsetDate?: Date | number - - /** - * Offset link used as an anchor for pagination - */ - offsetLink?: string + offset?: GetInviteLinksOffset }, - ): Promise> + ): Promise> /** * Get primary invite link of a chat * @@ -2512,7 +2512,7 @@ export interface TelegramClient extends BaseTelegramClient { */ getDiscussionMessage(peer: InputPeerLike, message: number): Promise /** - * Iterate through a chat history sequentially. + * Get chat history. * * @param chatId Chat's marked ID, its username, phone or `"me"` or `"self"`. * @param params Additional fetch parameters @@ -2523,61 +2523,58 @@ export interface TelegramClient extends BaseTelegramClient { /** * Limits the number of messages to be retrieved. * - * By default, no limit is applied and all messages - * are returned. + * @default 100 */ limit?: number /** - * Sequential number of the first message to be returned. - * Defaults to 0 (most recent message). - * - * Negative values are also accepted and are useful - * in case you set `offsetId` or `offsetDate`. + * Offset for pagination */ - offset?: number + offset?: GetHistoryOffset /** - * Pass a message identifier as an offset to retrieve - * only older messages starting from that message + * Additional offset from {@link offset}, in resulting messages. + * + * This can be used for advanced use cases, like: + * - Loading 20 messages newer than message with ID `MSGID`: + * `offset = MSGID, addOffset = -20, limit = 20` + * - Loading 20 messages around message with ID `MSGID`: + * `offset = MSGID, addOffset = -10, limit = 20` + * + * @default `0` (disabled) */ - offsetId?: number + + addOffset?: number /** * Minimum message ID to return * - * Defaults to `0` (disabled). + * @default `0` (disabled). */ minId?: number /** * Maximum message ID to return. * - * > *Seems* to work the same as {@link offsetId} + * Unless {@link addOffset} is used, this will work the same as {@link offset}. * - * Defaults to `0` (disabled). + * @default `0` (disabled). */ maxId?: number /** - * Pass a date (`Date` or Unix time in ms) as an offset to retrieve - * only older messages starting from that date. - */ - offsetDate?: number | Date - - /** - * Pass `true` to retrieve messages in reversed order (from older to recent) + * Whether to retrieve messages in reversed order (from older to recent), + * starting from {@link offset} (inclusive). + * + * > **Note**: Using `reverse=true` requires you to pass offset from which to start + * > fetching the messages "downwards". If you call `getHistory` with `reverse=true` + * > and without any offset, it will return an empty array. + * + * @default false */ reverse?: boolean - - /** - * Chunk size. Usually you shouldn't care about this. - * - * Defaults to `100` - */ - chunkSize?: number }, - ): AsyncIterableIterator + ): Promise> /** * Get all messages inside of a message group * @@ -2678,20 +2675,18 @@ export interface TelegramClient extends BaseTelegramClient { emoji?: InputReaction /** - * Limit the number of events returned. + * Limit the number of users returned. * - * Defaults to `Infinity`, i.e. all events are returned + * @default 100 */ limit?: number /** - * Chunk size, usually not needed. - * - * Defaults to `100` + * Offset for pagination */ - chunkSize?: number + offset?: GetReactionUsersOffset }, - ): AsyncIterableIterator + ): Promise> /** * Get a single scheduled message in chat by its ID * @@ -2709,6 +2704,111 @@ export interface TelegramClient extends BaseTelegramClient { * @param messageIds Scheduled messages IDs */ getScheduledMessages(chatId: InputPeerLike, messageIds: number[]): Promise<(Message | null)[]> + /** + * Iterate over chat history. Wrapper over {@link getHistory} + * + * @param chatId Chat's marked ID, its username, phone or `"me"` or `"self"`. + * @param params Additional fetch parameters + */ + iterHistory( + chatId: InputPeerLike, + params?: Parameters[1] & { + /** + * Limits the number of messages to be retrieved. + * + * @default Infinity, i.e. all messages + */ + limit?: number + + /** + * Chunk size. Usually you shouldn't care about this. + * + * @default 100 + */ + chunkSize?: number + }, + ): AsyncIterableIterator + /** + * Iterate over users who have reacted to the message. + * + * Wrapper over {@link getReactionUsers}. + * + * @param chatId Chat ID + * @param messageId Message ID + * @param params + */ + iterReactionUsers( + chatId: InputPeerLike, + messageId: number, + params?: Parameters[2] & { + /** + * Limit the number of events returned. + * + * @default `Infinity`, i.e. all events are returned + */ + limit?: number + + /** + * Chunk size, usually not needed. + * + * @default 100 + */ + chunkSize?: number + }, + ): AsyncIterableIterator + /** + * Search for messages globally from all of your chats. + * + * Iterable version of {@link searchGlobal} + * + * **Note**: Due to Telegram limitations, you can only get up to ~10000 messages + * + * @param params Search parameters + */ + iterSearchGlobal( + params?: Parameters[0] & { + /** + * Limits the number of messages to be retrieved. + * + * @default `Infinity`, i.e. all messages are returned + */ + limit?: number + + /** + * Chunk size, which will be passed as `limit` parameter + * for `messages.search`. Usually you shouldn't care about this. + * + * @default 100 + */ + chunkSize?: number + }, + ): AsyncIterableIterator + /** + * Search for messages inside a specific chat + * + * Iterable version of {@link searchMessages} + * + * @param chatId Chat's marked ID, its username, phone or `"me"` or `"self"`. + * @param params Additional search parameters + */ + iterSearchMessages( + params?: Parameters[0] & { + /** + * Limits the number of messages to be retrieved. + * + * @default `Infinity`, i.e. all messages are returned + */ + limit?: number + + /** + * Chunk size, which will be passed as `limit` parameter + * for `messages.search`. Usually you shouldn't care about this. + * + * @default `100` + */ + chunkSize?: number + }, + ): AsyncIterableIterator _parseEntities( text?: string | FormattedString, @@ -2752,14 +2852,14 @@ export interface TelegramClient extends BaseTelegramClient { /** * Text query string. Use `"@"` to search for mentions. * - * Defaults to `""` (empty string) + * @default `""` (empty string) */ query?: string /** * Limits the number of messages to be retrieved. * - * By default, no limit is applied and all messages are returned + * @default 100 */ limit?: number @@ -2772,112 +2872,121 @@ export interface TelegramClient extends BaseTelegramClient { filter?: tl.TypeMessagesFilter /** - * Chunk size, which will be passed as `limit` parameter - * for `messages.search`. Usually you shouldn't care about this. - * - * Defaults to `100` + * Offset data used for pagination */ - chunkSize?: number - }): AsyncIterableIterator + offset?: SearchGlobalOffset + + /** + * Only return messages newer than this date + */ + minDate?: Date | number + + /** + * Only return messages older than this date + */ + maxDate?: Date | number + }): Promise> /** * Search for messages inside a specific chat * * @param chatId Chat's marked ID, its username, phone or `"me"` or `"self"`. * @param params Additional search parameters */ - searchMessages( - chatId: InputPeerLike, - params?: { - /** - * Text query string. Required for text-only messages, - * optional for media. - * - * Defaults to `""` (empty string) - */ - query?: string + searchMessages(params?: { + /** + * Text query string. Required for text-only messages, + * optional for media. + * + * @default `""` (empty string) + */ + query?: string - /** - * Offset ID for the search. Only messages earlier than this - * ID will be returned. - * - * Defaults to `0` (for the latest message). - */ - offsetId?: number + /** + * Chat where to search for messages. + * + * When empty, will search across common message box (i.e. private messages and legacy chats) + */ + chatId?: InputPeerLike - /** - * Offset from the {@link offsetId}. Only used for the - * first chunk - * - * Defaults to `0` (for the same message as {@link offsetId}). - */ - offset?: number + /** + * Offset ID for the search. Only messages earlier than this ID will be returned. + * + * @default `0` (starting from the latest message). + */ + offset?: SearchMessagesOffset - /** - * Minimum message ID to return - * - * Defaults to `0` (disabled). - */ - minId?: number + /** + * Additional offset from {@link offset}, in resulting messages. + * + * This can be used for advanced use cases, like: + * - Loading 20 results newer than message with ID `MSGID`: + * `offset = MSGID, addOffset = -20, limit = 20` + * - Loading 20 results around message with ID `MSGID`: + * `offset = MSGID, addOffset = -10, limit = 20` + * + * When {@link offset} is not set, this will be relative to the last message + * + * @default `0` (disabled) + */ + addOffset?: number - /** - * Maximum message ID to return. - * - * > *Seems* to work the same as {@link offsetId} - * - * Defaults to `0` (disabled). - */ - maxId?: number + /** + * Minimum message ID to return + * + * @default `0` (disabled). + */ + minId?: number - /** - * Minimum message date to return - * - * Defaults to `0` (disabled). - */ - minDate?: number | Date + /** + * Maximum message ID to return. + * + * Unless {@link addOffset} is used, this will work the same as {@link offset}. + * + * @default `0` (disabled). + */ + maxId?: number - /** - * Maximum message date to return - * - * Defaults to `0` (disabled). - */ - maxDate?: number | Date + /** + * Minimum message date to return + * + * Defaults to `0` (disabled). + */ + minDate?: number | Date - /** - * Thread ID to return only messages from this thread. - */ - threadId?: number + /** + * Maximum message date to return + * + * Defaults to `0` (disabled). + */ + maxDate?: number | Date - /** - * Limits the number of messages to be retrieved. - * - * By default, no limit is applied and all messages are returned - */ - limit?: number + /** + * Thread ID to return only messages from this thread. + */ + threadId?: number - /** - * Filter the results using some filter. - * Defaults to {@link SearchFilters.Empty} (i.e. will return all messages) - * - * @link SearchFilters - */ - filter?: tl.TypeMessagesFilter + /** + * Limits the number of messages to be retrieved. + * + * @default 100 + */ + limit?: number - /** - * Search for messages sent by a specific user. - * - * Pass their marked ID, username, phone or `"me"` or `"self"` - */ - fromUser?: InputPeerLike + /** + * Filter the results using some filter. + * Defaults to {@link SearchFilters.Empty} (i.e. will return all messages) + * + * @link SearchFilters + */ + filter?: tl.TypeMessagesFilter - /** - * Chunk size, which will be passed as `limit` parameter - * for `messages.search`. Usually you shouldn't care about this. - * - * Defaults to `100` - */ - chunkSize?: number - }, - ): AsyncIterableIterator + /** + * Search only for messages sent by a specific user. + * + * You can pass their marked ID, username, phone or `"me"` or `"self"` + */ + fromUser?: InputPeerLike + }): Promise> /** * Copy a message (i.e. send the same message, * but do not forward it). @@ -3743,6 +3852,14 @@ export interface TelegramClient extends BaseTelegramClient { * */ getMyUsername(): string | null + /** + * Get a single profile picture of a user by its ID + * + * @param userId User ID, username, phone number, `"me"` or `"self"` + * @param photoId ID of the photo to fetch + * @param params + */ + getProfilePhoto(userId: InputPeerLike, photoId: tl.Long): Promise /** * Get a list of profile pictures of a user * @@ -3755,18 +3872,18 @@ export interface TelegramClient extends BaseTelegramClient { /** * Offset from which to fetch. * - * Defaults to `0` + * @default `0` */ offset?: number /** * Maximum number of items to fetch (up to 100) * - * Defaults to `100` + * @default `100` */ limit?: number }, - ): Promise + ): Promise> /** * Get information about a single user. * @@ -3790,33 +3907,20 @@ export interface TelegramClient extends BaseTelegramClient { */ iterProfilePhotos( userId: InputPeerLike, - params?: { - /** - * Offset from which to fetch. - * - * Defaults to `0` - */ - offset?: number - + params?: Parameters[1] & { /** * Maximum number of items to fetch * - * Defaults to `Infinity`, i.e. all items are fetched + * @default `Infinity`, i.e. all items are fetched */ limit?: number /** * Size of chunks which are fetched. Usually not needed. * - * Defaults to `100` + * @default 100 */ chunkSize?: number - - /** - * If set, the method will return only photos - * with IDs less than the set one - */ - maxId?: tl.Long }, ): AsyncIterableIterator /** @@ -4095,6 +4199,10 @@ export class TelegramClient extends BaseTelegramClient { getMessages = getMessages getReactionUsers = getReactionUsers getScheduledMessages = getScheduledMessages + iterHistory = iterHistory + iterReactionUsers = iterReactionUsers + iterSearchGlobal = iterSearchGlobal + iterSearchMessages = iterSearchMessages _parseEntities = _parseEntities pinMessage = pinMessage readHistory = readHistory @@ -4151,6 +4259,7 @@ export class TelegramClient extends BaseTelegramClient { getCommonChats = getCommonChats getMe = getMe getMyUsername = getMyUsername + getProfilePhoto = getProfilePhoto getProfilePhotos = getProfilePhotos getUsers = getUsers iterProfilePhotos = iterProfilePhotos diff --git a/packages/client/src/methods/README.md b/packages/client/src/methods/README.md index 411e4202..6e1a01a2 100644 --- a/packages/client/src/methods/README.md +++ b/packages/client/src/methods/README.md @@ -63,18 +63,18 @@ function _initializeAwesomeExtension(this: TelegramClient) { } ``` -## `@returns-exported` +## `@exported` -Used as a first statement inside an exported function's body to indicate that this method returns an object of type -which is exported from the same file. +Used as a first statement inside an exported function's body to indicate that +this exported type should be imported from the client Example: ```typescript +// @exported export type FooOrBar = Foo | Bar export function getFooOrBar(this: TelegramClient): FooOrBar { - // @returns-exported return new Foo() } ``` 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 cf6f5054..1b163007 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,14 @@ import { TelegramClient } from '../../client' import { ArrayPaginated, ChatInviteLink, InputPeerLike, PeersIndex } from '../../types' -import { makeArrayPaginated, normalizeDate } from '../../utils' +import { makeArrayPaginated } from '../../utils' import { normalizeToInputUser } from '../../utils/peer-utils' +// @exported +export interface GetInviteLinksOffset { + date: number + link: string +} + /** * Get invite links created by some administrator in the chat. * @@ -39,22 +45,14 @@ export async function getInviteLinks( limit?: number /** - * Offset date used as an anchor for pagination. + * Offset for pagination. */ - offsetDate?: Date | number - - /** - * Offset link used as an anchor for pagination - */ - offsetLink?: string + offset?: GetInviteLinksOffset }, -): Promise> { +): Promise> { if (!params) params = {} - const { revoked = false, limit = Infinity, admin } = params - - const offsetDate = normalizeDate(params.offsetDate) - const offsetLink = params.offsetLink + const { revoked = false, limit = Infinity, admin, offset } = params const res = await this.call({ _: 'messages.getExportedChatInvites', @@ -62,8 +60,8 @@ export async function getInviteLinks( revoked, adminId: admin ? normalizeToInputUser(await this.resolvePeer(admin), admin) : { _: 'inputUserSelf' }, limit, - offsetDate, - offsetLink, + offsetDate: offset?.date, + offsetLink: offset?.link, }) const peers = PeersIndex.from(res) diff --git a/packages/client/src/methods/invite-links/iter-invite-links.ts b/packages/client/src/methods/invite-links/iter-invite-links.ts index eec9e9ce..122bc8de 100644 --- a/packages/client/src/methods/invite-links/iter-invite-links.ts +++ b/packages/client/src/methods/invite-links/iter-invite-links.ts @@ -35,7 +35,7 @@ export async function* iterInviteLinks( const { revoked = false, limit = Infinity, chunkSize = 100, admin } = params - let { offsetDate, offsetLink } = params + let { offset } = params let current = 0 @@ -47,21 +47,18 @@ export async function* iterInviteLinks( admin: adminResolved, revoked, limit: Math.min(chunkSize, limit - current), - offsetDate, - offsetLink, + offset, }) 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 } + + if (!links.next) return + offset = links.next } } diff --git a/packages/client/src/methods/messages/get-history.ts b/packages/client/src/methods/messages/get-history.ts index 1a93654a..5e351c46 100644 --- a/packages/client/src/methods/messages/get-history.ts +++ b/packages/client/src/methods/messages/get-history.ts @@ -1,126 +1,130 @@ import Long from 'long' +import { tl } from '@mtcute/core' import { assertTypeIsNot } from '@mtcute/core/utils' import { TelegramClient } from '../../client' -import { InputPeerLike, Message, PeersIndex } from '../../types' -import { normalizeDate } from '../../utils/misc-utils' +import { ArrayPaginated, InputPeerLike, Message, PeersIndex } from '../../types' +import { makeArrayPaginated } from '../../utils' + +// @exported +export interface GetHistoryOffset { + id: number + date: number +} + +const defaultOffset: GetHistoryOffset = { + id: 0, + date: 0, +} /** - * Iterate through a chat history sequentially. + * Get chat history. * * @param chatId Chat's marked ID, its username, phone or `"me"` or `"self"`. * @param params Additional fetch parameters * @internal */ -export async function* getHistory( +export async function getHistory( this: TelegramClient, chatId: InputPeerLike, params?: { /** * Limits the number of messages to be retrieved. * - * By default, no limit is applied and all messages - * are returned. + * @default 100 */ limit?: number /** - * Sequential number of the first message to be returned. - * Defaults to 0 (most recent message). - * - * Negative values are also accepted and are useful - * in case you set `offsetId` or `offsetDate`. + * Offset for pagination */ - offset?: number + offset?: GetHistoryOffset /** - * Pass a message identifier as an offset to retrieve - * only older messages starting from that message + * Additional offset from {@link offset}, in resulting messages. + * + * This can be used for advanced use cases, like: + * - Loading 20 messages newer than message with ID `MSGID`: + * `offset = MSGID, addOffset = -20, limit = 20` + * - Loading 20 messages around message with ID `MSGID`: + * `offset = MSGID, addOffset = -10, limit = 20` + * + * @default `0` (disabled) */ - offsetId?: number + + addOffset?: number /** * Minimum message ID to return * - * Defaults to `0` (disabled). + * @default `0` (disabled). */ minId?: number /** * Maximum message ID to return. * - * > *Seems* to work the same as {@link offsetId} + * Unless {@link addOffset} is used, this will work the same as {@link offset}. * - * Defaults to `0` (disabled). + * @default `0` (disabled). */ maxId?: number /** - * Pass a date (`Date` or Unix time in ms) as an offset to retrieve - * only older messages starting from that date. - */ - offsetDate?: number | Date - - /** - * Pass `true` to retrieve messages in reversed order (from older to recent) + * Whether to retrieve messages in reversed order (from older to recent), + * starting from {@link offset} (inclusive). + * + * > **Note**: Using `reverse=true` requires you to pass offset from which to start + * > fetching the messages "downwards". If you call `getHistory` with `reverse=true` + * > and without any offset, it will return an empty array. + * + * @default false */ reverse?: boolean - - /** - * Chunk size. Usually you shouldn't care about this. - * - * Defaults to `100` - */ - chunkSize?: number }, -): AsyncIterableIterator { +): Promise> { if (!params) params = {} - let current = 0 - const total = params.limit || Infinity - const limit = Math.min(params.chunkSize || 100, total) + const { + limit = 100, + offset: { id: offsetId = 0, date: offsetDate = 0 } = defaultOffset, + addOffset = 0, + minId = 0, + maxId = 0, + reverse = false, + } = params - const minId = params.minId || 0 - const maxId = params.maxId || 0 + const addOffsetAdjusted = addOffset + (reverse ? -limit : 0) - let offsetId = params.offsetId ?? (params.reverse && !params.offsetDate ? 1 : 0) - const offsetDate = normalizeDate(params.offsetDate) || 0 - const baseOffset = -(params.reverse ? limit : 0) - let addOffset = (params.offset ? params.offset * (params.reverse ? -1 : 1) : 0) + baseOffset - - // resolve peer once and pass an InputPeer afterwards const peer = await this.resolvePeer(chatId) - for (;;) { - const res = await this.call({ - _: 'messages.getHistory', - peer, - offsetId, - offsetDate, - addOffset, - limit: Math.min(limit, total - current), - maxId, - minId, - hash: Long.ZERO, - }) + const res = await this.call({ + _: 'messages.getHistory', + peer, + offsetId, + offsetDate, + addOffset: addOffsetAdjusted, + limit, + maxId, + minId, + hash: Long.ZERO, + }) - assertTypeIsNot('getHistory', res, 'messages.messagesNotModified') + assertTypeIsNot('getHistory', res, 'messages.messagesNotModified') - const peers = PeersIndex.from(res) + const peers = PeersIndex.from(res) + const msgs = res.messages.filter((msg) => msg._ !== 'messageEmpty').map((msg) => new Message(this, msg, peers)) - const msgs = res.messages.filter((msg) => msg._ !== 'messageEmpty').map((msg) => new Message(this, msg, peers)) + if (reverse) msgs.reverse() - if (!msgs.length) break + const last = msgs[msgs.length - 1] + const next = last ? + { + id: last.id + (reverse ? 1 : 0), + date: last.raw.date, + } : + undefined - if (params.reverse) msgs.reverse() - - offsetId = msgs[msgs.length - 1].id + (params.reverse ? 1 : 0) - addOffset = baseOffset - - yield* msgs - current += msgs.length - - if (current >= total) break - } + return makeArrayPaginated(msgs, (res as tl.messages.RawMessagesSlice).count ?? msgs.length, next) } diff --git a/packages/client/src/methods/messages/get-reaction-users.ts b/packages/client/src/methods/messages/get-reaction-users.ts index 2d7250d0..d59d3d07 100644 --- a/packages/client/src/methods/messages/get-reaction-users.ts +++ b/packages/client/src/methods/messages/get-reaction-users.ts @@ -1,7 +1,16 @@ -import { tl } from '@mtcute/core' - import { TelegramClient } from '../../client' -import { InputPeerLike, InputReaction, normalizeInputReaction, PeerReaction, PeersIndex } from '../../types' +import { + ArrayPaginated, + InputPeerLike, + InputReaction, + normalizeInputReaction, + PeerReaction, + PeersIndex, +} from '../../types' +import { makeArrayPaginated } from '../../utils' + +// @exported +export type GetReactionUsersOffset = string /** * Get users who have reacted to the message. @@ -11,7 +20,7 @@ import { InputPeerLike, InputReaction, normalizeInputReaction, PeerReaction, Pee * @param params * @internal */ -export async function* getReactionUsers( +export async function getReactionUsers( this: TelegramClient, chatId: InputPeerLike, messageId: number, @@ -22,54 +31,40 @@ export async function* getReactionUsers( emoji?: InputReaction /** - * Limit the number of events returned. + * Limit the number of users returned. * - * Defaults to `Infinity`, i.e. all events are returned + * @default 100 */ limit?: number /** - * Chunk size, usually not needed. - * - * Defaults to `100` + * Offset for pagination */ - chunkSize?: number + offset?: GetReactionUsersOffset }, -): AsyncIterableIterator { +): Promise> { if (!params) params = {} + const { limit = 100, offset, emoji } = params + const peer = await this.resolvePeer(chatId) - let current = 0 - let offset: string | undefined = undefined - const total = params.limit || Infinity - const chunkSize = Math.min(params.chunkSize ?? 100, total) + const reaction = normalizeInputReaction(emoji) - const reaction = normalizeInputReaction(params.emoji) + const res = await this.call({ + _: 'messages.getMessageReactionsList', + peer, + id: messageId, + reaction, + limit, + offset, + }) - for (;;) { - const res: tl.RpcCallReturn['messages.getMessageReactionsList'] = await this.call({ - _: 'messages.getMessageReactionsList', - peer, - id: messageId, - reaction, - limit: Math.min(chunkSize, total - current), - offset, - }) + const peers = PeersIndex.from(res) - if (!res.reactions.length) break - - offset = res.nextOffset - - const peers = PeersIndex.from(res) - - for (const reaction of res.reactions) { - const parsed = new PeerReaction(this, reaction, peers) - - current += 1 - yield parsed - - if (current >= total) break - } - } + return makeArrayPaginated( + res.reactions.map((it) => new PeerReaction(this, it, peers)), + res.count, + res.nextOffset, + ) } diff --git a/packages/client/src/methods/messages/iter-history.ts b/packages/client/src/methods/messages/iter-history.ts new file mode 100644 index 00000000..b5b62f14 --- /dev/null +++ b/packages/client/src/methods/messages/iter-history.ts @@ -0,0 +1,60 @@ +import { TelegramClient } from '../../client' +import { InputPeerLike, Message } from '../../types' + +/** + * Iterate over chat history. Wrapper over {@link getHistory} + * + * @param chatId Chat's marked ID, its username, phone or `"me"` or `"self"`. + * @param params Additional fetch parameters + * @internal + */ +export async function* iterHistory( + this: TelegramClient, + chatId: InputPeerLike, + params?: Parameters[1] & { + /** + * Limits the number of messages to be retrieved. + * + * @default Infinity, i.e. all messages + */ + limit?: number + + /** + * Chunk size. Usually you shouldn't care about this. + * + * @default 100 + */ + chunkSize?: number + }, +): AsyncIterableIterator { + if (!params) params = {} + + const { limit = Infinity, chunkSize = 100, minId = 0, maxId = 0, reverse = false } = params + + let { offset, addOffset = 0 } = params + let current = 0 + + // resolve peer once and pass an InputPeer afterwards + const peer = await this.resolvePeer(chatId) + + for (;;) { + const res = await this.getHistory(peer, { + offset, + addOffset, + limit: Math.min(chunkSize, limit - current), + maxId, + minId, + reverse, + }) + + for (const msg of res) { + yield msg + + if (++current >= limit) return + } + + if (!res.next) return + offset = res.next + addOffset = 0 + } +} diff --git a/packages/client/src/methods/messages/iter-reaction-users.ts b/packages/client/src/methods/messages/iter-reaction-users.ts new file mode 100644 index 00000000..9fba21e0 --- /dev/null +++ b/packages/client/src/methods/messages/iter-reaction-users.ts @@ -0,0 +1,60 @@ +import { TelegramClient } from '../../client' +import { InputPeerLike, normalizeInputReaction, PeerReaction } from '../../types' + +/** + * Iterate over users who have reacted to the message. + * + * Wrapper over {@link getReactionUsers}. + * + * @param chatId Chat ID + * @param messageId Message ID + * @param params + * @internal + */ +export async function* iterReactionUsers( + this: TelegramClient, + chatId: InputPeerLike, + messageId: number, + params?: Parameters[2] & { + /** + * Limit the number of events returned. + * + * @default `Infinity`, i.e. all events are returned + */ + limit?: number + + /** + * Chunk size, usually not needed. + * + * @default 100 + */ + chunkSize?: number + }, +): AsyncIterableIterator { + if (!params) params = {} + + const peer = await this.resolvePeer(chatId) + + const { limit = Infinity, chunkSize = 100 } = params + + let current = 0 + let { offset } = params + + const reaction = normalizeInputReaction(params.emoji) + + for (;;) { + const res = await this.getReactionUsers(peer, messageId, { + emoji: reaction, + limit: Math.min(chunkSize, limit - current), + offset, + }) + + offset = res.next + + for (const reaction of res) { + yield reaction + + if (++current >= limit) break + } + } +} diff --git a/packages/client/src/methods/messages/iter-search-global.ts b/packages/client/src/methods/messages/iter-search-global.ts new file mode 100644 index 00000000..a2ed8681 --- /dev/null +++ b/packages/client/src/methods/messages/iter-search-global.ts @@ -0,0 +1,65 @@ +import { TelegramClient } from '../../client' +import { Message, SearchFilters } from '../../types' +import { normalizeDate } from '../../utils' + +/** + * Search for messages globally from all of your chats. + * + * Iterable version of {@link searchGlobal} + * + * **Note**: Due to Telegram limitations, you can only get up to ~10000 messages + * + * @param params Search parameters + * @internal + */ +export async function* iterSearchGlobal( + this: TelegramClient, + params?: Parameters[0] & { + /** + * Limits the number of messages to be retrieved. + * + * @default `Infinity`, i.e. all messages are returned + */ + limit?: number + + /** + * Chunk size, which will be passed as `limit` parameter + * for `messages.search`. Usually you shouldn't care about this. + * + * @default 100 + */ + chunkSize?: number + }, +): AsyncIterableIterator { + if (!params) params = {} + + const { query = '', filter = SearchFilters.Empty, limit = Infinity, chunkSize = 100 } = params + + const minDate = normalizeDate(params.minDate) ?? 0 + const maxDate = normalizeDate(params.maxDate) ?? 0 + + let { offset } = params + let current = 0 + + for (;;) { + const res = await this.searchGlobal({ + query, + filter, + limit: Math.min(chunkSize, limit - current), + minDate, + maxDate, + offset, + }) + + if (!res.length) return + + for (const msg of res) { + yield msg + + if (++current >= limit) return + } + + if (!res.next) return + offset = res.next + } +} diff --git a/packages/client/src/methods/messages/iter-search-messages.ts b/packages/client/src/methods/messages/iter-search-messages.ts new file mode 100644 index 00000000..9460330f --- /dev/null +++ b/packages/client/src/methods/messages/iter-search-messages.ts @@ -0,0 +1,82 @@ +import { TelegramClient } from '../../client' +import { Message, SearchFilters } from '../../types' +import { normalizeDate } from '../../utils/misc-utils' + +/** + * Search for messages inside a specific chat + * + * Iterable version of {@link searchMessages} + * + * @param chatId Chat's marked ID, its username, phone or `"me"` or `"self"`. + * @param params Additional search parameters + * @internal + */ +export async function* iterSearchMessages( + this: TelegramClient, + params?: Parameters[0] & { + /** + * Limits the number of messages to be retrieved. + * + * @default `Infinity`, i.e. all messages are returned + */ + limit?: number + + /** + * Chunk size, which will be passed as `limit` parameter + * for `messages.search`. Usually you shouldn't care about this. + * + * @default `100` + */ + chunkSize?: number + }, +): AsyncIterableIterator { + if (!params) params = {} + + const { + query = '', + chatId = { _: 'inputPeerEmpty' }, + minId = 0, + maxId = 0, + threadId, + limit = Infinity, + chunkSize = 100, + filter = SearchFilters.Empty, + } = params + + const minDate = normalizeDate(params.minDate) ?? 0 + const maxDate = normalizeDate(params.maxDate) ?? 0 + const peer = await this.resolvePeer(chatId) + const fromUser = params.fromUser ? await this.resolvePeer(params.fromUser) : undefined + + let { offset, addOffset } = params + let current = 0 + + for (;;) { + const res = await this.searchMessages({ + query, + chatId: peer, + offset, + addOffset, + minId, + maxId, + threadId, + filter, + fromUser, + minDate, + maxDate, + limit: Math.min(chunkSize, limit - current), + }) + + if (!res.length) break + + for (const msg of res) { + yield msg + + if (++current >= limit) return + } + + if (!res.next) break + offset = res.next + addOffset = undefined + } +} diff --git a/packages/client/src/methods/messages/search-global.ts b/packages/client/src/methods/messages/search-global.ts index 92dfe777..84a6e760 100644 --- a/packages/client/src/methods/messages/search-global.ts +++ b/packages/client/src/methods/messages/search-global.ts @@ -2,7 +2,21 @@ import { tl } from '@mtcute/core' import { assertTypeIsNot } from '@mtcute/core/utils' import { TelegramClient } from '../../client' -import { Message, PeersIndex, SearchFilters } from '../../types' +import { ArrayPaginated, Message, PeersIndex, SearchFilters } from '../../types' +import { makeArrayPaginated, normalizeDate } from '../../utils' + +// @exported +export interface SearchGlobalOffset { + rate: number + peer: tl.TypeInputPeer + id: number +} + +const defaultOffset: SearchGlobalOffset = { + rate: 0, + peer: { _: 'inputPeerEmpty' }, + id: 0, +} /** * Search for messages globally from all of your chats @@ -12,20 +26,20 @@ import { Message, PeersIndex, SearchFilters } from '../../types' * @param params Search parameters * @internal */ -export async function* searchGlobal( +export async function searchGlobal( this: TelegramClient, params?: { /** * Text query string. Use `"@"` to search for mentions. * - * Defaults to `""` (empty string) + * @default `""` (empty string) */ query?: string /** * Limits the number of messages to be retrieved. * - * By default, no limit is applied and all messages are returned + * @default 100 */ limit?: number @@ -38,54 +52,59 @@ export async function* searchGlobal( filter?: tl.TypeMessagesFilter /** - * Chunk size, which will be passed as `limit` parameter - * for `messages.search`. Usually you shouldn't care about this. - * - * Defaults to `100` + * Offset data used for pagination */ - chunkSize?: number + offset?: SearchGlobalOffset + + /** + * Only return messages newer than this date + */ + minDate?: Date | number + + /** + * Only return messages older than this date + */ + maxDate?: Date | number }, -): AsyncIterableIterator { +): Promise> { if (!params) params = {} - let current = 0 + const { + query = '', + filter = SearchFilters.Empty, + limit = 100, + offset: { rate: offsetRate, peer: offsetPeer, id: offsetId } = defaultOffset, + } = params - const total = params.limit || Infinity - const limit = Math.min(params.chunkSize || 100, total) + const minDate = normalizeDate(params.minDate) ?? 0 + const maxDate = normalizeDate(params.maxDate) ?? 0 - let offsetRate = 0 - let offsetPeer = { _: 'inputPeerEmpty' } as tl.TypeInputPeer - let offsetId = 0 + const res = await this.call({ + _: 'messages.searchGlobal', + q: query, + filter, + minDate, + maxDate, + offsetId, + offsetRate, + offsetPeer, + limit, + }) - for (;;) { - const res: tl.RpcCallReturn['messages.searchGlobal'] = await this.call({ - _: 'messages.searchGlobal', - q: params.query || '', - filter: params.filter || SearchFilters.Empty, - minDate: 0, - maxDate: 0, - offsetId, - offsetRate, - offsetPeer: offsetPeer, - limit: Math.min(limit, total - current), - }) + assertTypeIsNot('searchGlobal', res, 'messages.messagesNotModified') + const peers = PeersIndex.from(res) - assertTypeIsNot('searchGlobal', res, 'messages.messagesNotModified') + const msgs = res.messages.filter((msg) => msg._ !== 'messageEmpty').map((msg) => new Message(this, msg, peers)) - const peers = PeersIndex.from(res) + const last = msgs[msgs.length - 1] - const msgs = res.messages.filter((msg) => msg._ !== 'messageEmpty').map((msg) => new Message(this, msg, peers)) + const next = last ? + { + rate: (res as tl.messages.RawMessagesSlice).nextRate ?? last.raw.date, + peer: last.chat.inputPeer, + id: last.id, + } : + undefined - if (!msgs.length) break - - const last = msgs[msgs.length - 1] - offsetRate = (res as tl.messages.RawMessagesSlice).nextRate ?? last.raw.date - offsetPeer = last.chat.inputPeer - offsetId = last.id - - yield* msgs - - current += msgs.length - if (current >= total) break - } + return makeArrayPaginated(msgs, (res as tl.messages.RawMessagesSlice).count ?? msgs.length, next) } diff --git a/packages/client/src/methods/messages/search-messages.ts b/packages/client/src/methods/messages/search-messages.ts index f20bdc63..88b59d90 100644 --- a/packages/client/src/methods/messages/search-messages.ts +++ b/packages/client/src/methods/messages/search-messages.ts @@ -4,8 +4,11 @@ import { tl } from '@mtcute/core' import { assertTypeIsNot } from '@mtcute/core/utils' import { TelegramClient } from '../../client' -import { InputPeerLike, Message, PeersIndex, SearchFilters } from '../../types' -import { normalizeDate } from '../../utils/misc-utils' +import { ArrayPaginated, InputPeerLike, Message, PeersIndex, SearchFilters } from '../../types' +import { makeArrayPaginated, normalizeDate } from '../../utils/misc-utils' + +// @exported +export type SearchMessagesOffset = number /** * Search for messages inside a specific chat @@ -14,47 +17,59 @@ import { normalizeDate } from '../../utils/misc-utils' * @param params Additional search parameters * @internal */ -export async function* searchMessages( +export async function searchMessages( this: TelegramClient, - chatId: InputPeerLike, params?: { /** * Text query string. Required for text-only messages, * optional for media. * - * Defaults to `""` (empty string) + * @default `""` (empty string) */ query?: string /** - * Offset ID for the search. Only messages earlier than this - * ID will be returned. + * Chat where to search for messages. * - * Defaults to `0` (for the latest message). + * When empty, will search across common message box (i.e. private messages and legacy chats) */ - offsetId?: number + chatId?: InputPeerLike /** - * Offset from the {@link offsetId}. Only used for the - * first chunk + * Offset ID for the search. Only messages earlier than this ID will be returned. * - * Defaults to `0` (for the same message as {@link offsetId}). + * @default `0` (starting from the latest message). */ - offset?: number + offset?: SearchMessagesOffset + + /** + * Additional offset from {@link offset}, in resulting messages. + * + * This can be used for advanced use cases, like: + * - Loading 20 results newer than message with ID `MSGID`: + * `offset = MSGID, addOffset = -20, limit = 20` + * - Loading 20 results around message with ID `MSGID`: + * `offset = MSGID, addOffset = -10, limit = 20` + * + * When {@link offset} is not set, this will be relative to the last message + * + * @default `0` (disabled) + */ + addOffset?: number /** * Minimum message ID to return * - * Defaults to `0` (disabled). + * @default `0` (disabled). */ minId?: number /** * Maximum message ID to return. * - * > *Seems* to work the same as {@link offsetId} + * Unless {@link addOffset} is used, this will work the same as {@link offset}. * - * Defaults to `0` (disabled). + * @default `0` (disabled). */ maxId?: number @@ -80,7 +95,7 @@ export async function* searchMessages( /** * Limits the number of messages to be retrieved. * - * By default, no limit is applied and all messages are returned + * @default 100 */ limit?: number @@ -93,70 +108,57 @@ export async function* searchMessages( filter?: tl.TypeMessagesFilter /** - * Search for messages sent by a specific user. + * Search only for messages sent by a specific user. * - * Pass their marked ID, username, phone or `"me"` or `"self"` + * You can pass their marked ID, username, phone or `"me"` or `"self"` */ fromUser?: InputPeerLike - - /** - * Chunk size, which will be passed as `limit` parameter - * for `messages.search`. Usually you shouldn't care about this. - * - * Defaults to `100` - */ - chunkSize?: number }, -): AsyncIterableIterator { +): Promise> { if (!params) params = {} - let current = 0 - let offsetId = params.offsetId || 0 - let offset = params.offset || 0 + const { + query = '', + chatId = { _: 'inputPeerEmpty' }, + offset = 0, + addOffset = 0, + minId = 0, + maxId = 0, + threadId, + limit = 100, + filter = SearchFilters.Empty, + } = params const minDate = normalizeDate(params.minDate) ?? 0 const maxDate = normalizeDate(params.maxDate) ?? 0 - const minId = params.minId ?? 0 - const maxId = params.maxId ?? 0 - - const total = params.limit || Infinity - const limit = Math.min(params.chunkSize || 100, total) - const peer = await this.resolvePeer(chatId) - const fromUser = (params.fromUser ? await this.resolvePeer(params.fromUser) : null) || undefined + const fromUser = params.fromUser ? await this.resolvePeer(params.fromUser) : undefined - for (;;) { - const res: tl.RpcCallReturn['messages.search'] = await this.call({ - _: 'messages.search', - peer, - q: params.query || '', - filter: params.filter || SearchFilters.Empty, - minDate, - maxDate, - offsetId, - addOffset: offset, - limit: Math.min(limit, total - current), - minId, - maxId, - fromId: fromUser, - hash: Long.ZERO, - }) + const res = await this.call({ + _: 'messages.search', + peer, + q: query, + filter, + minDate, + maxDate, + offsetId: offset, + addOffset, + limit, + minId, + maxId, + fromId: fromUser, + topMsgId: threadId, + hash: Long.ZERO, + }) - assertTypeIsNot('searchMessages', res, 'messages.messagesNotModified') + assertTypeIsNot('searchMessages', res, 'messages.messagesNotModified') - // for successive chunks, we need to reset the offset - offset = 0 + const peers = PeersIndex.from(res) - const peers = PeersIndex.from(res) + const msgs = res.messages.filter((msg) => msg._ !== 'messageEmpty').map((msg) => new Message(this, msg, peers)) - const msgs = res.messages.filter((msg) => msg._ !== 'messageEmpty').map((msg) => new Message(this, msg, peers)) + const last = msgs[msgs.length - 1] + const next = last ? last.id : undefined - if (!msgs.length) break - - offsetId = res.messages[res.messages.length - 1].id - yield* msgs - - current += msgs.length - if (current >= total) break - } + return makeArrayPaginated(msgs, (res as tl.messages.RawMessagesSlice).count ?? msgs.length, next) } diff --git a/packages/client/src/methods/users/get-profile-photo.ts b/packages/client/src/methods/users/get-profile-photo.ts new file mode 100644 index 00000000..f0073e6e --- /dev/null +++ b/packages/client/src/methods/users/get-profile-photo.ts @@ -0,0 +1,29 @@ +import { tl } from '@mtcute/core' +import { assertTypeIs } from '@mtcute/core/utils' + +import { TelegramClient } from '../../client' +import { InputPeerLike, Photo } from '../../types' +import { normalizeToInputUser } from '../../utils/peer-utils' + +/** + * Get a single profile picture of a user by its ID + * + * @param userId User ID, username, phone number, `"me"` or `"self"` + * @param photoId ID of the photo to fetch + * @param params + * @internal + */ +export async function getProfilePhoto(this: TelegramClient, userId: InputPeerLike, photoId: tl.Long): Promise { + const res = await this.call({ + _: 'photos.getUserPhotos', + userId: normalizeToInputUser(await this.resolvePeer(userId), userId), + offset: -1, + limit: 1, + maxId: photoId, + }) + + const photo = res.photos[0] + assertTypeIs('getProfilePhotos', photo, 'photo') + + return new Photo(this, photo) +} diff --git a/packages/client/src/methods/users/get-profile-photos.ts b/packages/client/src/methods/users/get-profile-photos.ts index 4aa8b0b5..ce063fa4 100644 --- a/packages/client/src/methods/users/get-profile-photos.ts +++ b/packages/client/src/methods/users/get-profile-photos.ts @@ -1,9 +1,11 @@ import Long from 'long' import { tl } from '@mtcute/core' +import { assertTypeIs } from '@mtcute/core/utils' import { TelegramClient } from '../../client' -import { InputPeerLike, Photo } from '../../types' +import { ArrayPaginated, InputPeerLike, Photo } from '../../types' +import { makeArrayPaginated } from '../../utils' import { normalizeToInputUser } from '../../utils/peer-utils' /** @@ -20,27 +22,37 @@ export async function getProfilePhotos( /** * Offset from which to fetch. * - * Defaults to `0` + * @default `0` */ offset?: number /** * Maximum number of items to fetch (up to 100) * - * Defaults to `100` + * @default `100` */ limit?: number }, -): Promise { +): Promise> { if (!params) params = {} + const { offset = 0, limit = 100 } = params + const res = await this.call({ _: 'photos.getUserPhotos', userId: normalizeToInputUser(await this.resolvePeer(userId), userId), - offset: params.offset ?? 0, - limit: params.limit ?? 100, + offset, + limit, maxId: Long.ZERO, }) - return res.photos.map((it) => new Photo(this, it as tl.RawPhoto)) + return makeArrayPaginated( + res.photos.map((it) => { + assertTypeIs('getProfilePhotos', it, 'photo') + + return new Photo(this, it) + }), + (res as tl.photos.RawPhotosSlice).count ?? res.photos.length, + offset + res.photos.length, + ) } diff --git a/packages/client/src/methods/users/iter-profile-photos.ts b/packages/client/src/methods/users/iter-profile-photos.ts index c5c37847..0885c6f7 100644 --- a/packages/client/src/methods/users/iter-profile-photos.ts +++ b/packages/client/src/methods/users/iter-profile-photos.ts @@ -1,7 +1,3 @@ -import Long from 'long' - -import { tl } from '@mtcute/core' - import { TelegramClient } from '../../client' import { InputPeerLike, Photo } from '../../types' import { normalizeToInputUser } from '../../utils/peer-utils' @@ -16,66 +12,44 @@ import { normalizeToInputUser } from '../../utils/peer-utils' export async function* iterProfilePhotos( this: TelegramClient, userId: InputPeerLike, - params?: { - /** - * Offset from which to fetch. - * - * Defaults to `0` - */ - offset?: number - + params?: Parameters[1] & { /** * Maximum number of items to fetch * - * Defaults to `Infinity`, i.e. all items are fetched + * @default `Infinity`, i.e. all items are fetched */ limit?: number /** * Size of chunks which are fetched. Usually not needed. * - * Defaults to `100` + * @default 100 */ chunkSize?: number - - /** - * If set, the method will return only photos - * with IDs less than the set one - */ - maxId?: tl.Long }, ): AsyncIterableIterator { if (!params) params = {} const peer = normalizeToInputUser(await this.resolvePeer(userId), userId) - let offset = params.offset || 0 + const { limit = Infinity, chunkSize = 100 } = params + + let { offset } = params let current = 0 - const total = params.limit || Infinity - - const limit = Math.min(params.chunkSize || 100, total) - - const maxId = params.maxId || Long.ZERO for (;;) { - const res = await this.call({ - _: 'photos.getUserPhotos', - userId: peer, - limit: Math.min(limit, total - current), + const res = await this.getProfilePhotos(peer, { offset, - maxId, + limit: Math.min(chunkSize, limit - current), }) - if (!res.photos.length) break + for (const it of res) { + yield it - offset += res.photos.length - - for (const it of res.photos) { - yield new Photo(this, it as tl.RawPhoto) + if (++current >= limit) return } - current += res.photos.length - - if (current >= total) break + if (!res.next) return + offset = res.next } } diff --git a/packages/client/src/types/media/photo.ts b/packages/client/src/types/media/photo.ts index 76e0fbac..35bf50ec 100644 --- a/packages/client/src/types/media/photo.ts +++ b/packages/client/src/types/media/photo.ts @@ -77,6 +77,13 @@ export class Photo extends FileLocation { this.type = 'photo' } + /** + * Photo ID + */ + get id(): tl.Long { + return this.raw.id + } + /** Date this photo was sent */ get date(): Date { return new Date(this.raw.date * 1000) diff --git a/packages/client/src/types/messages/reactions.ts b/packages/client/src/types/messages/reactions.ts index 19199ed0..f13a54ab 100644 --- a/packages/client/src/types/messages/reactions.ts +++ b/packages/client/src/types/messages/reactions.ts @@ -1,3 +1,5 @@ +import Long from 'long' + import { getMarkedPeerId, tl } from '@mtcute/core' import { assertTypeIs } from '@mtcute/core/utils' @@ -10,7 +12,7 @@ import { PeersIndex, User } from '../peers' * * Either a `string` with a unicode emoji, or a `tl.Long` for a custom emoji */ -export type InputReaction = string | tl.Long +export type InputReaction = string | tl.Long | tl.TypeReaction export function normalizeInputReaction(reaction?: InputReaction | null): tl.TypeReaction { if (typeof reaction === 'string') { @@ -18,11 +20,13 @@ export function normalizeInputReaction(reaction?: InputReaction | null): tl.Type _: 'reactionEmoji', emoticon: reaction, } - } else if (reaction) { + } else if (Long.isLong(reaction)) { return { _: 'reactionCustomEmoji', documentId: reaction, } + } else if (reaction) { + return reaction } return { @@ -139,13 +143,6 @@ export class MessageReactions { (reaction) => new PeerReaction(this.client, reaction, this._peers), )) } - - /** - * Get the users who reacted to this message - */ - getUsers(params?: Parameters[2]): AsyncIterableIterator { - return this.client.getReactionUsers(this.messageId, this.chatId, params) - } } makeInspectable(MessageReactions)