diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index d9e5d643..92d394a6 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -24,6 +24,7 @@ import { getHistory } from './methods/messages/get-history' import { getMessages } from './methods/messages/get-messages' import { iterHistory } from './methods/messages/iter-history' import { _parseEntities } from './methods/messages/parse-entities' +import { searchMessages } from './methods/messages/search-messages' import { sendPhoto } from './methods/messages/send-photo' import { sendText } from './methods/messages/send-text' import { @@ -590,6 +591,63 @@ export class TelegramClient extends BaseTelegramClient { ): Promise<[string, tl.TypeMessageEntity[] | undefined]> { return _parseEntities.apply(this, arguments) } + /** + * 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 + + /** + * Sequential number of the first message to be returned. + * + * Defaults to `0`. + */ + offset?: number + + /** + * Limits the number of messages to be retrieved. + * + * By default, no limit is applied and all messages are returned + */ + limit?: number + + /** + * Filter the results using some filter. + * Defaults to {@link SearchFilters.Empty} (i.e. will return all messages) + * + * @link SearchFilters + */ + filter?: tl.TypeMessagesFilter + + /** + * Search for messages sent by a specific user. + * + * 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 { + return searchMessages.apply(this, arguments) + } /** * Send a single photo * diff --git a/packages/client/src/methods/messages/search-messages.ts b/packages/client/src/methods/messages/search-messages.ts new file mode 100644 index 00000000..6ea16da3 --- /dev/null +++ b/packages/client/src/methods/messages/search-messages.ts @@ -0,0 +1,119 @@ +import { TelegramClient } from '../../client' +import { InputPeerLike, Message, MtCuteTypeAssertionError } from '../../types' +import { tl } from '@mtcute/tl' +import { + createUsersChatsIndex, + normalizeToInputPeer, +} from '../../utils/peer-utils' +import { SearchFilters } from '../../types' + +/** + * 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 + * @internal + */ +export async function* searchMessages( + this: TelegramClient, + chatId: InputPeerLike, + params?: { + /** + * Text query string. Required for text-only messages, + * optional for media. + * + * Defaults to `""` (empty string) + */ + query?: string + + /** + * Sequential number of the first message to be returned. + * + * Defaults to `0`. + */ + offset?: number + + /** + * Limits the number of messages to be retrieved. + * + * By default, no limit is applied and all messages are returned + */ + limit?: number + + /** + * Filter the results using some filter. + * Defaults to {@link SearchFilters.Empty} (i.e. will return all messages) + * + * @link SearchFilters + */ + filter?: tl.TypeMessagesFilter + + /** + * Search for messages sent by a specific user. + * + * 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 { + if (!params) params = {} + + let current = 0 + let offset = params.offset || 0 + + const total = params.limit || Infinity + const limit = Math.min(params.chunkSize || 100, total) + + const peer = normalizeToInputPeer(await this.resolvePeer(chatId)) + const fromUser = + (params.fromUser + ? normalizeToInputPeer(await this.resolvePeer(params.fromUser)) + : null) || undefined + + for (;;) { + const res = await this.call({ + _: 'messages.search', + peer, + q: params.query || '', + filter: params.filter || SearchFilters.Empty, + minDate: 0, + maxDate: 0, + offsetId: 0, + addOffset: offset, + limit, + minId: 0, + maxId: 0, + fromId: fromUser, + hash: 0, + }) + + if (res._ === 'messages.messagesNotModified') + throw new MtCuteTypeAssertionError( + 'searchMessages', + '!messages.messagesNotModified', + res._ + ) + + const { users, chats } = createUsersChatsIndex(res) + + const msgs = res.messages.map( + (msg) => new Message(this, msg, users, chats) + ) + + if (!msgs.length) break + + offset += msgs.length + yield* msgs + + current += msgs.length + if (current >= total) break + } +} diff --git a/packages/client/src/types/messages/index.ts b/packages/client/src/types/messages/index.ts index a08a754f..1d30582c 100644 --- a/packages/client/src/types/messages/index.ts +++ b/packages/client/src/types/messages/index.ts @@ -1,2 +1,3 @@ export * from './message-entity' export * from './message' +export * from './search-filters' diff --git a/packages/client/src/types/messages/search-filters.ts b/packages/client/src/types/messages/search-filters.ts new file mode 100644 index 00000000..84d46c1f --- /dev/null +++ b/packages/client/src/types/messages/search-filters.ts @@ -0,0 +1,50 @@ +import { tl } from '@mtcute/tl' + +/** + * Search filters to be used in {@link TelegramClient.searchMessages} + * and {@link TelegramClient.searchGlobal}. + * + * Note that due to Telegram API limitations, you can't combine filters, + * and can only use a limited pre-defined set. + * + * This object simply exports static TL objects for convenience, + * if it does not expose something, simply pass a TL object + * directly. + * + * - `Empty`: Search for all messages (used by default) + * - `Photo`: Search for photos + * - `Video`: Search for videos + * - `PhotoAndVideo`: Search for photos and videos + * - `Document`: Search for documents (generic files, not including audio, video, etc.) + * - `Url`: Search for messages containing URLs and text links + * - `Gif`: Search for messages containing GIFs + * - `Voice`: Search for messages containing voice notes + * - `Audio`: Search for messages containing audio files + * - `ChatPhotoChange`: Search for chat photo changes + * - `Call`: Search for calls + * - `Round`: Search for round messages (aka video notes) + * - `RoundAndVoice`: Search for round messages (aka video notes) and voice notes + * - `MyMention`: Search for mentions of yourself + * - `Location`: Search for geolocations + * - `Contact`: Search for contacts + * - `Pinned`: Search for pinned messages + */ +export const SearchFilters = { + Empty: { _: 'inputMessagesFilterEmpty' } as tl.TypeMessagesFilter, + Photo: { _: 'inputMessagesFilterPhotos' } as tl.TypeMessagesFilter, + Video: { _: 'inputMessagesFilterVideo' } as tl.TypeMessagesFilter, + PhotoAndVideo: { _: 'inputMessagesFilterPhotoVideo' } as tl.TypeMessagesFilter, + Document: { _: 'inputMessagesFilterDocument' } as tl.TypeMessagesFilter, + Url: { _: 'inputMessagesFilterUrl' } as tl.TypeMessagesFilter, + Gif: { _: 'inputMessagesFilterGif' } as tl.TypeMessagesFilter, + Voice: { _: 'inputMessagesFilterVoice' } as tl.TypeMessagesFilter, + Audio: { _: 'inputMessagesFilterMusic' } as tl.TypeMessagesFilter, + ChatPhotoChange: { _: 'inputMessagesFilterChatPhotos' } as tl.TypeMessagesFilter, + Call: { _: 'inputMessagesFilterPhoneCalls' } as tl.TypeMessagesFilter, + Round: { _: 'inputMessagesFilterRoundVideo' } as tl.TypeMessagesFilter, + RoundAndVoice: { _: 'inputMessagesFilterRoundVoice' } as tl.TypeMessagesFilter, + MyMention: { _: 'inputMessagesFilterMyMentions' } as tl.TypeMessagesFilter, + Location: { _: 'inputMessagesFilterGeo' } as tl.TypeMessagesFilter, + Contact: { _: 'inputMessagesFilterContacts' } as tl.TypeMessagesFilter, + Pinned: { _: 'inputMessagesFilterPinned' } as tl.TypeMessagesFilter, +} as const