diff --git a/packages/core/src/highlevel/client.ts b/packages/core/src/highlevel/client.ts index 44cf9e9d..9e18afc1 100644 --- a/packages/core/src/highlevel/client.ts +++ b/packages/core/src/highlevel/client.ts @@ -151,6 +151,7 @@ import { pinMessage } from './methods/messages/pin-message.js' import { readHistory } from './methods/messages/read-history.js' import { readReactions } from './methods/messages/read-reactions.js' import { searchGlobal, SearchGlobalOffset } from './methods/messages/search-global.js' +import { iterSearchHashtag, searchHashtag, SearchHashtagOffset } from './methods/messages/search-hashtag.js' import { searchMessages, SearchMessagesOffset } from './methods/messages/search-messages.js' import { answerMedia, answerMediaGroup, answerText } from './methods/messages/send-answer.js' import { commentMedia, commentMediaGroup, commentText } from './methods/messages/send-comment.js' @@ -3687,6 +3688,53 @@ export interface TelegramClient extends ITelegramClient { */ onlyChannels?: boolean }): Promise> + + /** + * Perform a global hashtag search, across the entire Telegram + * + * **Available**: 👤 users only + * + * @param hashtag Hashtag to search for + * @param params Additional parameters + */ + searchHashtag( + hashtag: string, + params?: { + /** Offset for the search */ + offset?: SearchHashtagOffset + /** Limit the number of results */ + limit?: number + }, + ): Promise> + /** + * Perform a global hashtag search, across the entire Telegram + * + * Iterable version of {@link searchHashtag} + * + * **Available**: ✅ both users and bots + * + * @param hashtag Hashtag to search for + * @param params Additional parameters + */ + iterSearchHashtag( + hashtag: string, + params?: Parameters[2] & { + /** + * 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 * @@ -5916,6 +5964,12 @@ TelegramClient.prototype.readReactions = function (...args) { TelegramClient.prototype.searchGlobal = function (...args) { return searchGlobal(this._client, ...args) } +TelegramClient.prototype.searchHashtag = function (...args) { + return searchHashtag(this._client, ...args) +} +TelegramClient.prototype.iterSearchHashtag = function (...args) { + return iterSearchHashtag(this._client, ...args) +} TelegramClient.prototype.searchMessages = function (...args) { return searchMessages(this._client, ...args) } diff --git a/packages/core/src/highlevel/methods.ts b/packages/core/src/highlevel/methods.ts index 016ae892..e8d1f361 100644 --- a/packages/core/src/highlevel/methods.ts +++ b/packages/core/src/highlevel/methods.ts @@ -153,6 +153,9 @@ export { readHistory } from './methods/messages/read-history.js' export { readReactions } from './methods/messages/read-reactions.js' export type { SearchGlobalOffset } from './methods/messages/search-global.js' export { searchGlobal } from './methods/messages/search-global.js' +export type { SearchHashtagOffset } from './methods/messages/search-hashtag.js' +export { searchHashtag } from './methods/messages/search-hashtag.js' +export { iterSearchHashtag } from './methods/messages/search-hashtag.js' export type { SearchMessagesOffset } from './methods/messages/search-messages.js' export { searchMessages } from './methods/messages/search-messages.js' export { answerText } from './methods/messages/send-answer.js' diff --git a/packages/core/src/highlevel/methods/messages/search-hashtag.ts b/packages/core/src/highlevel/methods/messages/search-hashtag.ts new file mode 100644 index 00000000..e0d6c15e --- /dev/null +++ b/packages/core/src/highlevel/methods/messages/search-hashtag.ts @@ -0,0 +1,116 @@ +import { tl } from '@mtcute/tl' + +import { assertTypeIsNot } from '../../../utils/type-assertions.js' +import { ITelegramClient } from '../../client.types.js' +import { Message, PeersIndex } from '../../types/index.js' +import { ArrayPaginated } from '../../types/utils.js' +import { makeArrayPaginated } from '../../utils/misc-utils.js' + +// @exported +export interface SearchHashtagOffset { + rate: number + peer: tl.TypeInputPeer + id: number +} + +const defaultOffset: SearchHashtagOffset = { + rate: 0, + peer: { _: 'inputPeerEmpty' }, + id: 0, +} + +// @available=user +/** + * Perform a global hashtag search, across the entire Telegram + * + * @param hashtag Hashtag to search for + * @param params Additional parameters + */ +export async function searchHashtag( + client: ITelegramClient, + hashtag: string, + params?: { + /** Offset for the search */ + offset?: SearchHashtagOffset + /** Limit the number of results */ + limit?: number + }, +): Promise> { + const { offset: { rate: offsetRate, peer: offsetPeer, id: offsetId } = defaultOffset, limit = 100 } = params ?? {} + const res = await client.call({ + _: 'channels.searchPosts', + hashtag, + offsetId, + offsetRate, + offsetPeer, + limit, + }) + + assertTypeIsNot('searchHashtag', res, 'messages.messagesNotModified') + + const peers = PeersIndex.from(res) + const msgs = res.messages.filter((msg) => msg._ !== 'messageEmpty').map((msg) => new Message(msg, peers)) + const last = msgs[msgs.length - 1] + + const next = last ? + { + rate: (res as tl.messages.RawMessagesSlice).nextRate ?? last.raw.date, + peer: last.chat.inputPeer, + id: last.id, + } : + undefined + + return makeArrayPaginated(msgs, (res as tl.messages.RawMessagesSlice).count ?? msgs.length, next) +} + +/** + * Perform a global hashtag search, across the entire Telegram + * + * Iterable version of {@link searchHashtag} + * + * @param hashtag Hashtag to search for + * @param params Additional parameters + */ +export async function* iterSearchHashtag( + client: ITelegramClient, + hashtag: string, + params?: Parameters[2] & { + /** + * 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 { limit = Infinity, chunkSize = 100 } = params + let { offset } = params + let current = 0 + + for (;;) { + const res = await searchHashtag(client, hashtag, { + offset, + limit: Math.min(chunkSize, limit - current), + }) + + if (!res.length) return + + for (const msg of res) { + yield msg + + if (++current >= limit) return + } + + if (!res.next) return + offset = res.next + } +}