From 383f133292cc00af59b997ca6939522046751958 Mon Sep 17 00:00:00 2001 From: teidesu Date: Sat, 10 Apr 2021 17:11:25 +0300 Subject: [PATCH] feat(client): chats and chat joining related methods, bound methods and classes --- packages/client/src/client.ts | 49 +++++++++ packages/client/src/methods/_imports.ts | 1 + .../src/methods/chats/get-chat-preview.ts | 35 ++++++ packages/client/src/methods/chats/get-chat.ts | 65 +++++++++++ .../client/src/methods/chats/get-full-chat.ts | 63 +++++++++++ .../client/src/methods/chats/join-chat.ts | 57 ++++++++++ .../client/src/types/peers/chat-preview.ts | 101 ++++++++++++++++++ packages/client/src/types/peers/chat.ts | 23 +++- packages/client/src/types/peers/index.ts | 1 + packages/client/src/utils/peer-utils.ts | 3 + 10 files changed, 396 insertions(+), 2 deletions(-) create mode 100644 packages/client/src/methods/chats/get-chat-preview.ts create mode 100644 packages/client/src/methods/chats/get-chat.ts create mode 100644 packages/client/src/methods/chats/get-full-chat.ts create mode 100644 packages/client/src/methods/chats/join-chat.ts create mode 100644 packages/client/src/types/peers/chat-preview.ts diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 4022ed42..3b76060d 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -13,6 +13,10 @@ import { signInBot } from './methods/auth/sign-in-bot' import { signIn } from './methods/auth/sign-in' import { signUp } from './methods/auth/sign-up' import { start } from './methods/auth/start' +import { getChatPreview } from './methods/chats/get-chat-preview' +import { getChat } from './methods/chats/get-chat' +import { getFullChat } from './methods/chats/get-full-chat' +import { joinChat } from './methods/chats/join-chat' import { downloadAsBuffer } from './methods/files/download-buffer' import { downloadToFile } from './methods/files/download-file' import { downloadAsIterable } from './methods/files/download-iterable' @@ -55,6 +59,7 @@ import { IMessageEntityParser } from './parser' import { Readable } from 'stream' import { Chat, + ChatPreview, FileDownloadParameters, InputFileLike, InputMediaLike, @@ -320,6 +325,50 @@ export class TelegramClient extends BaseTelegramClient { }): Promise { return start.apply(this, arguments) } + /** + * Get preview information about a private chat. + * + * @param inviteLink Invite link + * @throws MtCuteArgumentError In case invite link has invalid format + * @throws MtCuteNotFoundError + * In case you are trying to get info about private chat that you have already joined. + * Use {@link getChat} or {@link getFullChat} instead. + */ + getChatPreview(inviteLink: string): Promise { + return getChatPreview.apply(this, arguments) + } + /** + * Get basic information about a chat. + * + * @param chatId ID of the chat, its username or invite link + * @throws MtCuteArgumentError + * In case you are trying to get info about private chat that you haven't joined. + * Use {@link getChatPreview} instead. + */ + getChat(chatId: InputPeerLike): Promise { + return getChat.apply(this, arguments) + } + /** + * Get full information about a chat. + * + * @param chatId ID of the chat, its username or invite link + * @throws MtCuteArgumentError + * In case you are trying to get info about private chat that you haven't joined. + * Use {@link getChatPreview} instead. + */ + getFullChat(chatId: InputPeerLike): Promise { + return getFullChat.apply(this, arguments) + } + /** + * Join a channel or supergroup + * + * @param chatId + * Chat identifier. Either an invite link (`t.me/joinchat/*`), a username (`@username`) + * or ID of the linked supergroup or channel. + */ + joinChat(chatId: InputPeerLike): Promise { + return joinChat.apply(this, arguments) + } /** * Download a file and return its contents as a Buffer. * diff --git a/packages/client/src/methods/_imports.ts b/packages/client/src/methods/_imports.ts index 1e1d5103..fea66b6b 100644 --- a/packages/client/src/methods/_imports.ts +++ b/packages/client/src/methods/_imports.ts @@ -10,6 +10,7 @@ import { Readable } from 'stream' import { User, Chat, + ChatPreview, TermsOfService, SentCode, MaybeDynamic, diff --git a/packages/client/src/methods/chats/get-chat-preview.ts b/packages/client/src/methods/chats/get-chat-preview.ts new file mode 100644 index 00000000..01b66997 --- /dev/null +++ b/packages/client/src/methods/chats/get-chat-preview.ts @@ -0,0 +1,35 @@ +import { MtCuteArgumentError, MtCuteNotFoundError } from '../../types' +import { TelegramClient } from '../../client' +import { INVITE_LINK_REGEX } from '../../utils/peer-utils' +import { ChatPreview } from '../../types' + +/** + * Get preview information about a private chat. + * + * @param inviteLink Invite link + * @throws MtCuteArgumentError In case invite link has invalid format + * @throws MtCuteNotFoundError + * In case you are trying to get info about private chat that you have already joined. + * Use {@link getChat} or {@link getFullChat} instead. + * @internal + */ +export async function getChatPreview( + this: TelegramClient, + inviteLink: string +): Promise { + const m = inviteLink.match(INVITE_LINK_REGEX) + if (!m) throw new MtCuteArgumentError('Invalid invite link') + + const res = await this.call({ + _: 'messages.checkChatInvite', + hash: m[1], + }) + + if (res._ !== 'chatInvite') { + throw new MtCuteNotFoundError( + `You have already joined this chat!` + ) + } + + return new ChatPreview(this, res, inviteLink) +} diff --git a/packages/client/src/methods/chats/get-chat.ts b/packages/client/src/methods/chats/get-chat.ts new file mode 100644 index 00000000..773957eb --- /dev/null +++ b/packages/client/src/methods/chats/get-chat.ts @@ -0,0 +1,65 @@ +import { Chat, InputPeerLike, MtCuteArgumentError } from '../../types' +import { TelegramClient } from '../../client' +import { + INVITE_LINK_REGEX, + normalizeToInputChannel, + normalizeToInputPeer, + normalizeToInputUser, +} from '../../utils/peer-utils' +import { tl } from '@mtcute/tl' + +/** + * Get basic information about a chat. + * + * @param chatId ID of the chat, its username or invite link + * @throws MtCuteArgumentError + * In case you are trying to get info about private chat that you haven't joined. + * Use {@link getChatPreview} instead. + * @internal + */ +export async function getChat( + this: TelegramClient, + chatId: InputPeerLike +): Promise { + if (typeof chatId === 'string') { + const m = chatId.match(INVITE_LINK_REGEX) + if (m) { + const res = await this.call({ + _: 'messages.checkChatInvite', + hash: m[1] + }) + + if (res._ === 'chatInvite') { + throw new MtCuteArgumentError(`You haven't joined ${JSON.stringify(res.title)}`) + } + + return new Chat(this, res.chat) + } + } + + const peer = await this.resolvePeer(chatId) + const input = normalizeToInputPeer(peer) + + let res: tl.TypeChat | tl.TypeUser + if (input._ === 'inputPeerChannel') { + const r = await this.call({ + _: 'channels.getChannels', + id: [normalizeToInputChannel(peer)!] + }) + res = r.chats[0] + } else if (input._ === 'inputPeerUser' || input._ === 'inputPeerSelf') { + const r = await this.call({ + _: 'users.getUsers', + id: [normalizeToInputUser(peer)!] + }) + res = r[0] + } else if (input._ === 'inputPeerChat') { + const r = await this.call({ + _: 'messages.getChats', + id: [input.chatId] + }) + res = r.chats[0] + } else throw new Error('should not happen') + + return new Chat(this, res) +} diff --git a/packages/client/src/methods/chats/get-full-chat.ts b/packages/client/src/methods/chats/get-full-chat.ts new file mode 100644 index 00000000..db85ee66 --- /dev/null +++ b/packages/client/src/methods/chats/get-full-chat.ts @@ -0,0 +1,63 @@ +import { Chat, InputPeerLike, MtCuteArgumentError } from '../../types' +import { TelegramClient } from '../../client' +import { + INVITE_LINK_REGEX, + normalizeToInputChannel, + normalizeToInputPeer, + normalizeToInputUser, +} from '../../utils/peer-utils' +import { tl } from '@mtcute/tl' + +/** + * Get full information about a chat. + * + * @param chatId ID of the chat, its username or invite link + * @throws MtCuteArgumentError + * In case you are trying to get info about private chat that you haven't joined. + * Use {@link getChatPreview} instead. + * @internal + */ +export async function getFullChat( + this: TelegramClient, + chatId: InputPeerLike +): Promise { + if (typeof chatId === 'string') { + const m = chatId.match(INVITE_LINK_REGEX) + if (m) { + const res = await this.call({ + _: 'messages.checkChatInvite', + hash: m[1] + }) + + if (res._ === 'chatInvite') { + throw new MtCuteArgumentError(`You haven't joined ${JSON.stringify(res.title)}`) + } + + // we still need to fetch full chat info + chatId = res.chat.id + } + } + + const peer = await this.resolvePeer(chatId) + const input = normalizeToInputPeer(peer) + + let res: tl.messages.TypeChatFull | tl.TypeUserFull + if (input._ === 'inputPeerChannel') { + res = await this.call({ + _: 'channels.getFullChannel', + channel: normalizeToInputChannel(peer)! + }) + } else if (input._ === 'inputPeerUser' || input._ === 'inputPeerSelf') { + res = await this.call({ + _: 'users.getFullUser', + id: normalizeToInputUser(peer)! + }) + } else if (input._ === 'inputPeerChat') { + res = await this.call({ + _: 'messages.getFullChat', + chatId: input.chatId + }) + } else throw new Error('should not happen') + + return Chat._parseFull(this, res) +} diff --git a/packages/client/src/methods/chats/join-chat.ts b/packages/client/src/methods/chats/join-chat.ts new file mode 100644 index 00000000..e3611ce3 --- /dev/null +++ b/packages/client/src/methods/chats/join-chat.ts @@ -0,0 +1,57 @@ +import { TelegramClient } from '../../client' +import { + Chat, + InputPeerLike, + MtCuteNotFoundError, + MtCuteTypeAssertionError, +} from '../../types' +import { INVITE_LINK_REGEX, normalizeToInputChannel } from '../../utils/peer-utils' + +/** + * Join a channel or supergroup + * + * @param chatId + * Chat identifier. Either an invite link (`t.me/joinchat/*`), a username (`@username`) + * or ID of the linked supergroup or channel. + * @internal + */ +export async function joinChat( + this: TelegramClient, + chatId: InputPeerLike +): Promise { + if (typeof chatId === 'string') { + const m = chatId.match(INVITE_LINK_REGEX) + if (m) { + const res = await this.call({ + _: 'messages.importChatInvite', + hash: m[1], + }) + if (!(res._ === 'updates' || res._ === 'updatesCombined')) { + throw new MtCuteTypeAssertionError( + 'joinChat, (@ messages.importChatInvite)', + 'updates | updatesCombined', + res._ + ) + } + + return new Chat(this, res.chats[0]) + } + } + + const peer = normalizeToInputChannel(await this.resolvePeer(chatId)) + if (!peer) throw new MtCuteNotFoundError() + + const res = await this.call({ + _: 'channels.joinChannel', + channel: peer, + }) + if (!(res._ === 'updates' || res._ === 'updatesCombined')) { + throw new MtCuteTypeAssertionError( + 'joinChat, (@ channels.joinChannel)', + 'updates | updatesCombined', + res._ + ) + } + + return new Chat(this, res.chats[0]) +} diff --git a/packages/client/src/types/peers/chat-preview.ts b/packages/client/src/types/peers/chat-preview.ts new file mode 100644 index 00000000..2c98a14f --- /dev/null +++ b/packages/client/src/types/peers/chat-preview.ts @@ -0,0 +1,101 @@ +import { tl } from '@mtcute/tl' +import { TelegramClient } from '../../client' +import { makeInspectable } from '../utils' +import { Photo } from '../media' +import { User } from './user' +import { Chat } from './chat' + +export namespace ChatPreview { + /** + * Chat type. Can be: + * - `group`: Legacy group + * - `supergroup`: Supergroup + * - `channel`: Broadcast channel + * - `broadcast`: Broadcast group + */ + export type Type = 'group' | 'supergroup' | 'channel' | 'broadcast' +} + +export class ChatPreview { + readonly client: TelegramClient + readonly invite: tl.RawChatInvite + + /** + * Original invite link used to fetch + * this preview + */ + readonly link: string + + constructor(client: TelegramClient, raw: tl.RawChatInvite, link: string) { + this.client = client + this.invite = raw + this.link = link + } + + /** + * Title of the chat + */ + get title(): string { + return this.invite.title + } + + /** + * Type of the chat + */ + get type(): ChatPreview.Type { + if (!this.invite.channel) return 'group' + if (this.invite.broadcast) return 'channel' + if (this.invite.megagroup) return 'broadcast' + return 'supergroup' + } + + /** + * Total chat member count + */ + get memberCount(): number { + return this.invite.participantsCount + } + + _photo?: Photo + /** + * Chat photo + */ + get photo(): Photo | null { + if (this.invite.photo._ === 'photoEmpty') return null + + if (!this._photo) { + this._photo = new Photo(this.client, this.invite.photo) + } + + return this._photo + } + + private _someMembers?: User[] + /** + * Preview of some of the chat members. + * + * This usually contains around 10 members, + * and members that are inside your contacts list are + * ordered before others. + */ + get someMembers(): User[] { + if (!this._someMembers) { + this._someMembers = this.invite.participants + ? this.invite.participants.map( + (it) => new User(this.client, it as tl.RawUser) + ) + : [] + } + + return this._someMembers + } + + /** + * Join this chat + */ + async join(): Promise { + return this.client.joinChat(this.link) + } +} + +makeInspectable(ChatPreview, ['link']) diff --git a/packages/client/src/types/peers/chat.ts b/packages/client/src/types/peers/chat.ts index 0927356d..03453baf 100644 --- a/packages/client/src/types/peers/chat.ts +++ b/packages/client/src/types/peers/chat.ts @@ -14,8 +14,15 @@ export namespace Chat { * - `group`: Legacy group * - `supergroup`: Supergroup * - `channel`: Broadcast channel + * - `broadcast`: Broadcast group */ - export type Type = 'private' | 'bot' | 'group' | 'supergroup' | 'channel' + export type Type = + | 'private' + | 'bot' + | 'group' + | 'supergroup' + | 'channel' + | 'broadcast' } /** @@ -109,7 +116,11 @@ export class Chat { } else if (this.peer._ === 'chat') { this._type = 'group' } else if (this.peer._ === 'channel') { - this._type = this.peer.broadcast ? 'channel' : 'supergroup' + this._type = this.peer.megagroup + ? 'broadcast' + : this.peer.broadcast + ? 'channel' + : 'supergroup' } } @@ -379,6 +390,7 @@ export class Chat { return new Chat(client, chats[peer.channelId]) } + /** @internal */ static _parseFull( client: TelegramClient, full: tl.messages.RawChatFull | tl.RawUserFull @@ -409,6 +421,13 @@ export class Chat { } // todo: bound methods https://github.com/pyrogram/pyrogram/blob/a86656aefcc93cc3d2f5c98227d5da28fcddb136/pyrogram/types/user_and_chats/chat.py#L319 + + /** + * Join this chat. + */ + async join(): Promise { + await this.client.joinChat(this.inputPeer) + } } makeInspectable(Chat) diff --git a/packages/client/src/types/peers/index.ts b/packages/client/src/types/peers/index.ts index 1c208d28..6fc74031 100644 --- a/packages/client/src/types/peers/index.ts +++ b/packages/client/src/types/peers/index.ts @@ -2,6 +2,7 @@ import { tl } from '@mtcute/tl' export * from './user' export * from './chat' +export * from './chat-preview' /** * Peer types that have one-to-one relation to tl.Peer* types. diff --git a/packages/client/src/utils/peer-utils.ts b/packages/client/src/utils/peer-utils.ts index 2f715ceb..28dd6262 100644 --- a/packages/client/src/utils/peer-utils.ts +++ b/packages/client/src/utils/peer-utils.ts @@ -1,5 +1,8 @@ import { tl } from '@mtcute/tl' +export const INVITE_LINK_REGEX = /^(?:https?:\/\/)?(?:www\.)?(?:t(?:elegram)?\.(?:org|me|dog)\/joinchat\/)([\w-]+)$/i + + // helpers to normalize result of `resolvePeer` function export function normalizeToInputPeer(