diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 3d054a26..4cbbf1f0 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -21,6 +21,7 @@ import { createSupergroup } from './methods/chats/create-supergroup' import { deleteChannel } from './methods/chats/delete-channel' import { deleteGroup } from './methods/chats/delete-group' import { deleteHistory } from './methods/chats/delete-history' +import { getChatMember } from './methods/chats/get-chat-member' import { getChatPreview } from './methods/chats/get-chat-preview' import { getChat } from './methods/chats/get-chat' import { getFullChat } from './methods/chats/get-full-chat' @@ -69,6 +70,7 @@ import { IMessageEntityParser } from './parser' import { Readable } from 'stream' import { Chat, + ChatMember, ChatPreview, FileDownloadParameters, InputFileLike, @@ -444,6 +446,19 @@ export class TelegramClient extends BaseTelegramClient { ): Promise { return deleteHistory.apply(this, arguments) } + /** + * Get information about a single chat member + * + * @param chatId Chat ID or username + * @param userId User ID, username, phone number, `"me"` or `"self"` + * @throws MtCuteNotFoundError In case given user is not a participant of a given chat + */ + getChatMember( + chatId: InputPeerLike, + userId: InputPeerLike + ): Promise { + return getChatMember.apply(this, arguments) + } /** * Get preview information about a private chat. * diff --git a/packages/client/src/methods/_imports.ts b/packages/client/src/methods/_imports.ts index fea66b6b..ea1e722e 100644 --- a/packages/client/src/methods/_imports.ts +++ b/packages/client/src/methods/_imports.ts @@ -11,6 +11,7 @@ import { User, Chat, ChatPreview, + ChatMember, TermsOfService, SentCode, MaybeDynamic, diff --git a/packages/client/src/methods/chats/get-chat-member.ts b/packages/client/src/methods/chats/get-chat-member.ts new file mode 100644 index 00000000..3e9ad8ac --- /dev/null +++ b/packages/client/src/methods/chats/get-chat-member.ts @@ -0,0 +1,78 @@ +import { TelegramClient } from '../../client' +import { + InputPeerLike, + MtCuteInvalidPeerTypeError, +} from '../../types' +import { + createUsersChatsIndex, + normalizeToInputChannel, + normalizeToInputPeer, +} from '../../utils/peer-utils' +import { assertTypeIs } from '../../utils/type-assertion' +import { tl } from '@mtcute/tl' +import { ChatMember } from '../../types' +import { UserNotParticipantError } from '@mtcute/tl/errors' + +/** + * Get information about a single chat member + * + * @param chatId Chat ID or username + * @param userId User ID, username, phone number, `"me"` or `"self"` + * @throws UserNotParticipantError In case given user is not a participant of a given chat + * @internal + */ +export async function getChatMember( + this: TelegramClient, + chatId: InputPeerLike, + userId: InputPeerLike +): Promise { + const user = normalizeToInputPeer(await this.resolvePeer(userId)) + const chat = await this.resolvePeer(chatId) + const chatInput = normalizeToInputPeer(chat) + + if (chatInput._ === 'inputPeerChat') { + const res = await this.call({ + _: 'messages.getFullChat', + chatId: chatInput.chatId, + }) + + assertTypeIs( + 'getChatMember (@ messages.getFullChat)', + res.fullChat, + 'chatFull' + ) + + const members = + res.fullChat.participants._ === 'chatParticipantsForbidden' + ? [] + : res.fullChat.participants.participants + + const { users } = createUsersChatsIndex(res) + + for (const m of members) { + if ( + (user._ === 'inputPeerSelf' && + (users[m.userId] as tl.RawUser).self) || + (user._ === 'inputPeerUser' && m.userId === user.userId) + ) { + return new ChatMember(this, m, users) + } + } + + throw new UserNotParticipantError() + } else if (chatInput._ === 'inputPeerChannel') { + const res = await this.call({ + _: 'channels.getParticipant', + channel: normalizeToInputChannel(chat)!, + participant: user, + }) + + const { users } = createUsersChatsIndex(res) + + return new ChatMember( + this, + res.participant, + users + ) + } else throw new MtCuteInvalidPeerTypeError(chatId, 'chat or channel') +} diff --git a/packages/client/src/types/peers/chat-member.ts b/packages/client/src/types/peers/chat-member.ts new file mode 100644 index 00000000..e0136b4c --- /dev/null +++ b/packages/client/src/types/peers/chat-member.ts @@ -0,0 +1,253 @@ +import { makeInspectable } from '../utils' +import { TelegramClient } from '../../client' +import { tl } from '@mtcute/tl' +import { User } from './user' +import { assertTypeIs } from '../../utils/type-assertion' +import { ChatPermissions } from './chat-permissions' + +export namespace ChatMember { + /** + * Status of the member: + * - `creator`: user is the creator of the chat + * - `admin`: user has admin rights in the chat + * - `member`: user is a normal member of the chat + * - `restricted`: user has some restrictions applied + * - `banned`: user was banned from the chat + * - `left`: user left the chat on their own + */ + export type Status = + | 'creator' + | 'admin' + | 'member' + | 'restricted' + | 'banned' + | 'left' +} + +/** + * Information about one chat member + */ +export class ChatMember { + readonly client: TelegramClient + readonly raw: tl.TypeChatParticipant | tl.TypeChannelParticipant + + /** Map of users in this object. Mainly for internal use */ + readonly _users: Record + + constructor( + client: TelegramClient, + raw: tl.TypeChatParticipant | tl.TypeChannelParticipant, + users: Record, + ) { + this.client = client + this.raw = raw + this._users = users + } + + private _user?: User + /** + * Information about the user + */ + get user(): User { + if (this._user === undefined) { + if ( + this.raw._ === 'channelParticipantBanned' || + this.raw._ === 'channelParticipantLeft' + ) { + assertTypeIs( + 'ChatMember#user (raw.peer)', + this.raw.peer, + 'peerUser' + ) + this._user = new User( + this.client, + this._users[this.raw.peer.userId] as tl.RawUser + ) + } else { + this._user = new User( + this.client, + this._users[this.raw.userId] as tl.RawUser + ) + } + } + + return this._user + } + + /** + * Get the chat member status + */ + get status(): ChatMember.Status { + if ( + this.raw._ === 'channelParticipant' || + this.raw._ === 'channelParticipantSelf' || + this.raw._ === 'chatParticipant' + ) { + return 'member' + } + + if ( + this.raw._ === 'channelParticipantCreator' || + this.raw._ === 'chatParticipantCreator' + ) { + return 'creator' + } + + if ( + this.raw._ === 'channelParticipantAdmin' || + this.raw._ === 'chatParticipantAdmin' + ) { + return 'admin' + } + + if (this.raw._ === 'channelParticipantLeft') { + return 'left' + } + + if (this.raw._ === 'channelParticipantBanned') { + return this.raw.bannedRights.viewMessages ? 'banned' : 'restricted' + } + + // fallback + return 'member' + } + + /** + * Custom title (for creators and admins). + * + * `null` for non-admins and in case custom title is not set. + */ + get title(): string | null { + return this.raw._ === 'channelParticipantCreator' || + this.raw._ === 'channelParticipantAdmin' + ? this.raw.rank ?? null + : null + } + + /** + * Date when the user has joined the chat. + * + * Not available for creators and left members + */ + get joinedDate(): Date | null { + return this.raw._ === 'channelParticipantCreator' || + this.raw._ === 'chatParticipantCreator' || + this.raw._ === 'channelParticipantLeft' + ? null + : new Date(this.raw.date * 1000) + } + + private _invitedBy?: User | null + /** + * Information about whoever invited this member to the chat. + * + * Only available in the following cases: + * - `user` is yourself + * - `chat` is a legacy group + * - `chat` is a supergroup/channel, and `user` is an admin + */ + get invitedBy(): User | null { + if (this._invitedBy === undefined) { + if ( + this.raw._ !== 'chatParticipantCreator' && + this.raw._ !== 'channelParticipantCreator' && + this.raw._ !== 'channelParticipant' && + this.raw._ !== 'channelParticipantBanned' && + this.raw._ !== 'channelParticipantLeft' && + this.raw.inviterId + ) { + this._invitedBy = new User( + this.client, + this._users[this.raw.inviterId] as tl.RawUser + ) + } else { + this._invitedBy = null + } + } + + return this._invitedBy + } + + private _promotedBy?: User | null + /** + * Information about whoever promoted this admin. + * + * Only available if `status = admin`. + */ + get promotedBy(): User | null { + if (this._promotedBy === undefined) { + if (this.raw._ === 'channelParticipantAdmin') { + this._promotedBy = new User( + this.client, + this._users[this.raw.promotedBy] as tl.RawUser + ) + } else { + this._promotedBy = null + } + } + + return this._promotedBy + } + + private _restrictedBy?: User | null + /** + * Information about whoever restricted this user. + * + * Only available if `status = restricted or status = banned` + */ + get restrictedBy(): User | null { + if (this._restrictedBy === undefined) { + if (this.raw._ === 'channelParticipantBanned') { + this._restrictedBy = new User( + this.client, + this._users[this.raw.kickedBy] as tl.RawUser + ) + } else { + this._restrictedBy = null + } + } + + return this._restrictedBy + } + + private _restrictions?: ChatPermissions + /** + * For restricted and banned users, + * information about the restrictions + */ + get restrictions(): ChatPermissions | null { + if (this.raw._ !== 'channelParticipantBanned') return null + + if (!this._restrictions) { + this._restrictions = new ChatPermissions(this.raw.bannedRights) + } + + return this._restrictions + } + + /** + * Whether this member is a part of the chat now. + * + * Makes sense only when `status = restricted or staus = banned` + */ + get isMember(): boolean { + return this.raw._ === 'channelParticipantBanned' + ? !this.raw.left + : this.raw._ !== 'channelParticipantLeft' + } + + /** + * For admins and creator of supergroup/channels, + * list of their admin permissions. + * + * Also contains whether this admin is anonymous. + */ + get permissions(): tl.RawChatAdminRights | null { + return this.raw._ === 'channelParticipantAdmin' || + this.raw._ === 'channelParticipantCreator' + ? this.raw.adminRights + : null + } +} + +makeInspectable(ChatMember) diff --git a/packages/client/src/types/peers/chat-permissions.ts b/packages/client/src/types/peers/chat-permissions.ts index c5ab3dfa..b05ac74a 100644 --- a/packages/client/src/types/peers/chat-permissions.ts +++ b/packages/client/src/types/peers/chat-permissions.ts @@ -2,7 +2,7 @@ import { tl } from '@mtcute/tl' import { makeInspectable } from '../utils' /** - * Represents the rights of a normal user in a {@link Chat}. + * Represents the permissions of a user in a {@link Chat}. */ export class ChatPermissions { readonly _bannedRights: tl.RawChatBannedRights @@ -115,6 +115,9 @@ export class ChatPermissions { /** * UNIX date until which these permissions are valid, * or `null` if forever. + * + * For example, represents the time when the restrictions + * will be lifted from a {@link ChatMember} */ get untilDate(): Date | null { return this._bannedRights.untilDate === 0 diff --git a/packages/client/src/types/peers/index.ts b/packages/client/src/types/peers/index.ts index 6fc74031..a73bce01 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/tl' export * from './user' export * from './chat' export * from './chat-preview' +export * from './chat-member' /** * Peer types that have one-to-one relation to tl.Peer* types.