From 14dc62e9120e71bc0549d02d8739269d6662aa20 Mon Sep 17 00:00:00 2001 From: teidesu Date: Wed, 14 Apr 2021 19:43:05 +0300 Subject: [PATCH] feat(client): getChatMembers and iterChatMembers methods --- packages/client/src/client.ts | 82 ++++++++++ .../src/methods/chats/get-chat-members.ts | 147 ++++++++++++++++++ .../src/methods/chats/iter-chat-members.ts | 67 ++++++++ 3 files changed, 296 insertions(+) create mode 100644 packages/client/src/methods/chats/get-chat-members.ts create mode 100644 packages/client/src/methods/chats/iter-chat-members.ts diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 91b14abf..2f7487f7 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -23,9 +23,11 @@ import { deleteChatPhoto } from './methods/chats/delete-chat-photo' import { deleteGroup } from './methods/chats/delete-group' import { deleteHistory } from './methods/chats/delete-history' import { getChatMember } from './methods/chats/get-chat-member' +import { getChatMembers } from './methods/chats/get-chat-members' import { getChatPreview } from './methods/chats/get-chat-preview' import { getChat } from './methods/chats/get-chat' import { getFullChat } from './methods/chats/get-full-chat' +import { iterChatMembers } from './methods/chats/iter-chat-members' import { joinChat } from './methods/chats/join-chat' import { leaveChat } from './methods/chats/leave-chat' import { setChatDefaultPermissions } from './methods/chats/set-chat-default-permissions' @@ -479,6 +481,62 @@ export class TelegramClient extends BaseTelegramClient { ): Promise { return getChatMember.apply(this, arguments) } + /** + * Get a chunk of members of some chat. + * + * You can retrieve up to 200 members at once + * + * @param chatId Chat ID or username + * @param params Additional parameters + */ + getChatMembers( + chatId: InputPeerLike, + params?: { + /** + * Search query to filter members by their display names and usernames + * Defaults to `''` (empty string) + * + * > **Note**: Only used for these values of `filter`: + * > `all`, `banned`, `restricted`, `contacts` + */ + query?: string + + /** + * Sequential number of the first member to be returned. + */ + offset?: number + + /** + * Maximum number of members to be retrieved. Defaults to `200` + */ + limit?: number + + /** + * Type of the query. Can be: + * - `all`: get all members + * - `banned`: get only banned members + * - `restricted`: get only restricted members + * - `bots`: get only bots + * - `recent`: get recent members + * - `admins`: get only administrators (and creator) + * - `contacts`: get only contacts + * - `mention`: get users that can be mentioned ([learn more](https://mt.tei.su/tl/class/channelParticipantsMentions)) + * + * Only used for channels and supergroups. Defaults to `recent` + */ + type?: + | 'all' + | 'banned' + | 'restricted' + | 'bots' + | 'recent' + | 'admins' + | 'contacts' + | 'mention' + } + ): Promise { + return getChatMembers.apply(this, arguments) + } /** * Get preview information about a private chat. * @@ -513,6 +571,30 @@ export class TelegramClient extends BaseTelegramClient { getFullChat(chatId: InputPeerLike): Promise { return getFullChat.apply(this, arguments) } + /** + * Iterate through chat members + * + * This method is a small wrapper over {@link getChatMembers}, + * which also handles duplicate entries (i.e. does not yield + * the same member twice) + * + * @param chatId Chat ID or username + * @param params Additional parameters + */ + iterChatMembers( + chatId: InputPeerLike, + params?: Parameters[1] & { + /** + * Chunk size, which will be passed as `limit` parameter + * to {@link getChatMembers}. Usually you shouldn't care about this. + * + * Defaults to `200` + */ + chunkSize?: number + } + ): AsyncIterableIterator { + return iterChatMembers.apply(this, arguments) + } /** * Join a channel or supergroup * diff --git a/packages/client/src/methods/chats/get-chat-members.ts b/packages/client/src/methods/chats/get-chat-members.ts new file mode 100644 index 00000000..175fc620 --- /dev/null +++ b/packages/client/src/methods/chats/get-chat-members.ts @@ -0,0 +1,147 @@ +import { + ChatMember, + InputPeerLike, + MtCuteInvalidPeerTypeError, +} from '../../types' +import { TelegramClient } from '../../client' +import { + createUsersChatsIndex, + normalizeToInputChannel, + normalizeToInputPeer, +} from '../../utils/peer-utils' +import { assertTypeIs } from '../../utils/type-assertion' +import { tl } from '@mtcute/tl' + +/** + * Get a chunk of members of some chat. + * + * You can retrieve up to 200 members at once + * + * @param chatId Chat ID or username + * @param params Additional parameters + * @internal + */ +export async function getChatMembers( + this: TelegramClient, + chatId: InputPeerLike, + params?: { + /** + * Search query to filter members by their display names and usernames + * Defaults to `''` (empty string) + * + * > **Note**: Only used for these values of `filter`: + * > `all`, `banned`, `restricted`, `contacts` + */ + query?: string + + /** + * Sequential number of the first member to be returned. + */ + offset?: number + + /** + * Maximum number of members to be retrieved. Defaults to `200` + */ + limit?: number + + /** + * Type of the query. Can be: + * - `all`: get all members + * - `banned`: get only banned members + * - `restricted`: get only restricted members + * - `bots`: get only bots + * - `recent`: get recent members + * - `admins`: get only administrators (and creator) + * - `contacts`: get only contacts + * - `mention`: get users that can be mentioned ([learn more](https://mt.tei.su/tl/class/channelParticipantsMentions)) + * + * Only used for channels and supergroups. Defaults to `recent` + */ + type?: + | 'all' + | 'banned' + | 'restricted' + | 'bots' + | 'recent' + | 'admins' + | 'contacts' + | 'mention' + } +): Promise { + if (!params) params = {} + + const chat = normalizeToInputPeer(await this.resolvePeer(chatId)) + if (chat._ === 'inputPeerUser') + throw new MtCuteInvalidPeerTypeError(chatId, 'chat or channel') + + if (chat._ === 'inputPeerChat') { + const res = await this.call({ + _: 'messages.getFullChat', + chatId: chat.chatId, + }) + + assertTypeIs( + 'getChatMember (@ messages.getFullChat)', + res.fullChat, + 'chatFull' + ) + + let members = + res.fullChat.participants._ === 'chatParticipantsForbidden' + ? [] + : res.fullChat.participants.participants + + if (params.offset) members = members.slice(params.offset) + if (params.limit) members = members.slice(0, params.limit) + + const { users } = createUsersChatsIndex(res) + + return members.map((m) => new ChatMember(this, m, users)) + } + + if (chat._ === 'inputPeerChannel') { + const q = params.query?.toLowerCase() ?? '' + const type = params.type ?? 'recent' + + let filter: tl.TypeChannelParticipantsFilter + if (type === 'all') { + filter = { _: 'channelParticipantsSearch', q } + } else if (type === 'banned') { + filter = { _: 'channelParticipantsKicked', q } + } else if (type === 'restricted') { + filter = { _: 'channelParticipantsBanned', q } + } else if (type === 'mention') { + filter = { _: 'channelParticipantsMentions', q } + } else if (type === 'bots') { + filter = { _: 'channelParticipantsBots' } + } else if (type === 'recent') { + filter = { _: 'channelParticipantsRecent' } + } else if (type === 'admins') { + filter = { _: 'channelParticipantsAdmins' } + } else if (type === 'contacts') { + filter = { _: 'channelParticipantsContacts', q } + } else { + return type as never + } + + const res = await this.call({ + _: 'channels.getParticipants', + channel: normalizeToInputChannel(chat)!, + filter, + offset: params.offset ?? 0, + limit: params.limit ?? 200, + hash: 0, + }) + + assertTypeIs( + 'getChatMembers (@ channels.getParticipants)', + res, + 'channels.channelParticipants' + ) + + const { users } = createUsersChatsIndex(res) + return res.participants.map(i => new ChatMember(this, i, users)) + } + + throw new Error('should not happen') +} diff --git a/packages/client/src/methods/chats/iter-chat-members.ts b/packages/client/src/methods/chats/iter-chat-members.ts new file mode 100644 index 00000000..0d08d22b --- /dev/null +++ b/packages/client/src/methods/chats/iter-chat-members.ts @@ -0,0 +1,67 @@ +import { TelegramClient } from '../../client' +import { ChatMember, InputPeerLike } from '../../types' + +/** + * Iterate through chat members + * + * This method is a small wrapper over {@link getChatMembers}, + * which also handles duplicate entries (i.e. does not yield + * the same member twice) + * + * @param chatId Chat ID or username + * @param params Additional parameters + * @internal + */ +export async function* iterChatMembers( + this: TelegramClient, + chatId: InputPeerLike, + params?: Parameters[1] & { + /** + * Chunk size, which will be passed as `limit` parameter + * to {@link getChatMembers}. Usually you shouldn't care about this. + * + * Defaults to `200` + */ + chunkSize?: number + } +): AsyncIterableIterator { + if (!params) params = {} + + let current = 0 + let total = params.limit || Infinity + const limit = Math.min(params.chunkSize ?? 200, total) + let offset = params.offset ?? 0 + + const yielded = new Set() + const chat = await this.resolvePeer(chatId) + + for (;;) { + const members = await this.getChatMembers(chat, { + offset, + limit, + query: params.query, + type: params.type + }) + + if (!members.length) break + + if (chat._ === 'inputPeerChat') { + total = members.length + } + + offset += members.length + + for (const m of members) { + const uid = m.user.id + + // handle duplicates + if (yielded.has(uid)) continue + yielded.add(uid) + + yield m + + current += 1 + if (current >= total) return + } + } +}