diff --git a/packages/client/scripts/generate-client.js b/packages/client/scripts/generate-client.js index 5b263a5f..3da6d69e 100644 --- a/packages/client/scripts/generate-client.js +++ b/packages/client/scripts/generate-client.js @@ -204,7 +204,7 @@ async function addSingleMethod(state, fileName) { comment: getLeadingComments(stmt), aliases, overload: isOverload, - hasOverloads: hasOverloads[name] && !isOverload + hasOverloads: hasOverloads[name] && !isOverload, }) const module = `./${relPath.replace(/\.ts$/, '')}` @@ -299,7 +299,15 @@ async function main() { const classContents = [] state.methods.list.forEach( - ({ name: origName, isPrivate, func, comment, aliases, overload, hasOverloads }) => { + ({ + name: origName, + isPrivate, + func, + comment, + aliases, + overload, + hasOverloads, + }) => { // create method that calls that function and passes `this` // first let's determine the signature const returnType = func.type ? ': ' + func.type.getText() : '' @@ -336,7 +344,9 @@ async function main() { ts.SyntaxKind.NumericLiteral || (it.initializer.kind === ts.SyntaxKind.Identifier && - it.initializer.escapedText === 'NaN') + (it.initializer.escapedText === 'NaN' || + it.initializer.escapedText === + 'Infinity')) ) { it.type = { kind: ts.SyntaxKind.NumberKeyword } } else { diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 937a226c..d0bd294e 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -60,6 +60,14 @@ import { _normalizeFileToDocument } from './methods/files/normalize-file-to-docu import { _normalizeInputFile } from './methods/files/normalize-input-file' import { _normalizeInputMedia } from './methods/files/normalize-input-media' import { uploadFile } from './methods/files/upload-file' +import { createInviteLink } from './methods/invite-links/create-invite-link' +import { editInviteLink } from './methods/invite-links/edit-invite-link' +import { exportInviteLink } from './methods/invite-links/export-invite-link' +import { getInviteLinkMembers } from './methods/invite-links/get-invite-link-members' +import { getInviteLink } from './methods/invite-links/get-invite-link' +import { getInviteLinks } from './methods/invite-links/get-invite-links' +import { getPrimaryInviteLink } from './methods/invite-links/get-primary-invite-link' +import { revokeInviteLink } from './methods/invite-links/revoke-invite-link' import { closePoll } from './methods/messages/close-poll' import { deleteMessages } from './methods/messages/delete-messages' import { editInlineMessage } from './methods/messages/edit-inline-message' @@ -1200,6 +1208,146 @@ export interface TelegramClient extends BaseTelegramClient { */ progressCallback?: (uploaded: number, total: number) => void }): Promise + /** + * Create an additional invite link for the chat. + * + * You must be an administrator and have appropriate rights. + * + * @param chatId Chat ID + * @param params + */ + createInviteLink( + chatId: InputPeerLike, + params?: { + /** + * Date when this link will expire. + * If `number` is passed, UNIX time in ms is expected. + */ + expires?: number | Date + + /** + * Maximum number of users that can be members of this chat + * at the same time after joining using this link. + * + * Integer in range `[1, 99999]` or `Infinity`, defaults to `Infinity` + */ + usageLimit?: number + } + ): Promise + /** + * Edit an invite link. You can only edit non-primary + * invite links. + * + * Only pass the fields that you want to modify. + * + * @param chatId Chat ID + * @param link Invite link to edit + * @param params + * @returns Modified invite link + */ + editInviteLink( + chatId: InputPeerLike, + link: string, + params: { + /** + * Date when this link will expire. + * If `number` is passed, UNIX time in ms is expected. + */ + expires?: number | Date + + /** + * Maximum number of users that can be members of this chat + * at the same time after joining using this link. + * + * Integer in range `[1, 99999]` or `Infinity`, + */ + usageLimit?: number + } + ): Promise + /** + * Generate a new primary invite link for a chat, + * old primary link is revoked. + * + * > **Note**: each administrator has their own primary invite link, + * > and bots by default don't have one. + * + * @param chatId Chat ID + */ + exportInviteLink(chatId: InputPeerLike): Promise + /** + * Iterate over users who have joined + * the chat with the given invite link. + * + * @param chatId Chat ID + * @param link Invite link + * @param limit (default: `Infinity`) Maximum number of users to return (by default returns all) + */ + getInviteLinkMembers( + chatId: InputPeerLike, + link: string, + limit?: number + ): AsyncIterableIterator + /** + * Get detailed information about an invite link + * + * @param chatId Chat ID + * @param link The invite link + */ + getInviteLink(chatId: InputPeerLike, link: string): Promise + /** + * Get invite links created by some administrator in the chat. + * + * As an administrator you can only get your own links + * (i.e. `adminId = "self"`), as a creator you can get + * any other admin's links. + * + * @param chatId Chat ID + * @param adminId Admin who created the links + * @param params + */ + getInviteLinks( + chatId: InputPeerLike, + adminId: InputPeerLike, + params?: { + /** + * Whether to fetch revoked invite links + */ + revoked?: boolean + + /** + * Limit the number of invite links to be fetched. + * By default, all links are fetched. + */ + limit?: number + + /** + * Size of chunks which are fetched. Usually not needed. + * + * Defaults to `100` + */ + chunkSize?: number + } + ): AsyncIterableIterator + /** + * Get primary invite link of a chat + * + * @param chatId Chat ID + */ + getPrimaryInviteLink(chatId: InputPeerLike): Promise + /** + * Revoke an invite link. + * + * If `link` is a primary invite link, a new invite link will be + * generated automatically by Telegram + * + * @param chatId Chat ID + * @param link Invite link to revoke + * @returns If `link` is a primary invite, newly generated invite link, otherwise the revoked link + */ + revokeInviteLink( + chatId: InputPeerLike, + link: string + ): Promise /** * Close a poll sent by you. * @@ -2556,6 +2704,14 @@ export class TelegramClient extends BaseTelegramClient { protected _normalizeInputFile = _normalizeInputFile protected _normalizeInputMedia = _normalizeInputMedia uploadFile = uploadFile + createInviteLink = createInviteLink + editInviteLink = editInviteLink + exportInviteLink = exportInviteLink + getInviteLinkMembers = getInviteLinkMembers + getInviteLink = getInviteLink + getInviteLinks = getInviteLinks + getPrimaryInviteLink = getPrimaryInviteLink + revokeInviteLink = revokeInviteLink closePoll = closePoll deleteMessages = deleteMessages editInlineMessage = editInlineMessage diff --git a/packages/client/src/methods/invite-links/create-invite-link.ts b/packages/client/src/methods/invite-links/create-invite-link.ts new file mode 100644 index 00000000..45244928 --- /dev/null +++ b/packages/client/src/methods/invite-links/create-invite-link.ts @@ -0,0 +1,44 @@ +import { TelegramClient } from '../../client' +import { ChatInviteLink, InputPeerLike } from '../../types' +import { normalizeToInputPeer } from '../../utils/peer-utils' +import { normalizeDate } from '../../utils/misc-utils' + +/** + * Create an additional invite link for the chat. + * + * You must be an administrator and have appropriate rights. + * + * @param chatId Chat ID + * @param params + * @internal + */ +export async function createInviteLink( + this: TelegramClient, + chatId: InputPeerLike, + params?: { + /** + * Date when this link will expire. + * If `number` is passed, UNIX time in ms is expected. + */ + expires?: number | Date + + /** + * Maximum number of users that can be members of this chat + * at the same time after joining using this link. + * + * Integer in range `[1, 99999]` or `Infinity`, defaults to `Infinity` + */ + usageLimit?: number + } +): Promise { + if (!params) params = {} + + const res = await this.call({ + _: 'messages.exportChatInvite', + peer: normalizeToInputPeer(await this.resolvePeer(chatId)), + expireDate: normalizeDate(params.expires), + usageLimit: params.usageLimit + }) + + return new ChatInviteLink(this, res) +} diff --git a/packages/client/src/methods/invite-links/edit-invite-link.ts b/packages/client/src/methods/invite-links/edit-invite-link.ts new file mode 100644 index 00000000..ff106d0a --- /dev/null +++ b/packages/client/src/methods/invite-links/edit-invite-link.ts @@ -0,0 +1,49 @@ +import { TelegramClient } from '../../client' +import { ChatInviteLink, InputPeerLike } from '../../types' +import { createUsersChatsIndex, normalizeToInputPeer } from '../../utils/peer-utils' +import { normalizeDate } from '../../utils/misc-utils' + +/** + * Edit an invite link. You can only edit non-primary + * invite links. + * + * Only pass the fields that you want to modify. + * + * @param chatId Chat ID + * @param link Invite link to edit + * @param params + * @returns Modified invite link + * @internal + */ +export async function editInviteLink( + this: TelegramClient, + chatId: InputPeerLike, + link: string, + params: { + /** + * Date when this link will expire. + * If `number` is passed, UNIX time in ms is expected. + */ + expires?: number | Date + + /** + * Maximum number of users that can be members of this chat + * at the same time after joining using this link. + * + * Integer in range `[1, 99999]` or `Infinity`, + */ + usageLimit?: number + } +): Promise { + const res = await this.call({ + _: 'messages.editExportedChatInvite', + peer: normalizeToInputPeer(await this.resolvePeer(chatId)), + link, + expireDate: normalizeDate(params.expires), + usageLimit: params.usageLimit + }) + + const { users } = createUsersChatsIndex(res) + + return new ChatInviteLink(this, res.invite, users) +} diff --git a/packages/client/src/methods/invite-links/export-invite-link.ts b/packages/client/src/methods/invite-links/export-invite-link.ts new file mode 100644 index 00000000..7123a8d0 --- /dev/null +++ b/packages/client/src/methods/invite-links/export-invite-link.ts @@ -0,0 +1,26 @@ +import { TelegramClient } from '../../client' +import { ChatInviteLink, InputPeerLike } from '../../types' +import { normalizeToInputPeer } from '../../utils/peer-utils' + +/** + * Generate a new primary invite link for a chat, + * old primary link is revoked. + * + * > **Note**: each administrator has their own primary invite link, + * > and bots by default don't have one. + * + * @param chatId Chat IDs + * @internal + */ +export async function exportInviteLink( + this: TelegramClient, + chatId: InputPeerLike +): Promise { + const res = await this.call({ + _: 'messages.exportChatInvite', + peer: normalizeToInputPeer(await this.resolvePeer(chatId)), + legacyRevokePermanent: true + }) + + return new ChatInviteLink(this, res) +} diff --git a/packages/client/src/methods/invite-links/get-invite-link-members.ts b/packages/client/src/methods/invite-links/get-invite-link-members.ts new file mode 100644 index 00000000..c0bb26af --- /dev/null +++ b/packages/client/src/methods/invite-links/get-invite-link-members.ts @@ -0,0 +1,63 @@ +import { TelegramClient } from '../../client' +import { ChatInviteLink, InputPeerLike, User } from '../../types' +import { createUsersChatsIndex, normalizeToInputPeer } from '../../utils/peer-utils' +import { tl } from '@mtcute/tl' + +/** + * Iterate over users who have joined + * the chat with the given invite link. + * + * @param chatId Chat ID + * @param link Invite link + * @param limit Maximum number of users to return (by default returns all) + * @internal + */ +export async function* getInviteLinkMembers( + this: TelegramClient, + chatId: InputPeerLike, + link: string, + limit = Infinity +): AsyncIterableIterator { + const peer = normalizeToInputPeer(await this.resolvePeer(chatId)) + + let current = 0 + + let offsetDate = 0 + let offsetUser: tl.TypeInputUser = { _: 'inputUserEmpty' } + + for (;;) { + // for some reason ts needs annotation, idk + const res: tl.RpcCallReturn['messages.getChatInviteImporters'] = await this.call({ + _: 'messages.getChatInviteImporters', + limit: Math.min(100, limit - current), + peer, + link, + offsetDate, + offsetUser, + }) + + if (!res.importers.length) break + + const { users } = createUsersChatsIndex(res) + + const last = res.importers[res.importers.length - 1] + offsetDate = last.date + offsetUser = { + _: 'inputUser', + userId: last.userId, + accessHash: (users[last.userId] as tl.RawUser).accessHash! + } + + for (const it of res.importers) { + const user = new User(this, users[it.userId]) + + yield { + user, + date: new Date(it.date * 1000), + } + } + + current += res.importers.length + if (current >= limit) break + } +} diff --git a/packages/client/src/methods/invite-links/get-invite-link.ts b/packages/client/src/methods/invite-links/get-invite-link.ts new file mode 100644 index 00000000..082214cb --- /dev/null +++ b/packages/client/src/methods/invite-links/get-invite-link.ts @@ -0,0 +1,26 @@ +import { TelegramClient } from '../../client' +import { ChatInviteLink, InputPeerLike } from '../../types' +import { createUsersChatsIndex, normalizeToInputPeer } from '../../utils/peer-utils' + +/** + * Get detailed information about an invite link + * + * @param chatId Chat ID + * @param link The invite link + * @internal + */ +export async function getInviteLink( + this: TelegramClient, + chatId: InputPeerLike, + link: string +): Promise { + const res = await this.call({ + _: 'messages.getExportedChatInvite', + peer: normalizeToInputPeer(await this.resolvePeer(chatId)), + link + }) + + const { users } = createUsersChatsIndex(res) + + return new ChatInviteLink(this, res.invite, users) +} diff --git a/packages/client/src/methods/invite-links/get-invite-links.ts b/packages/client/src/methods/invite-links/get-invite-links.ts new file mode 100644 index 00000000..65772d87 --- /dev/null +++ b/packages/client/src/methods/invite-links/get-invite-links.ts @@ -0,0 +1,91 @@ +import { TelegramClient } from '../../client' +import { + ChatInviteLink, + InputPeerLike, + MtCuteInvalidPeerTypeError, +} from '../../types' +import { + createUsersChatsIndex, + normalizeToInputPeer, + normalizeToInputUser, +} from '../../utils/peer-utils' +import { tl } from '@mtcute/tl' + +/** + * Get invite links created by some administrator in the chat. + * + * As an administrator you can only get your own links + * (i.e. `adminId = "self"`), as a creator you can get + * any other admin's links. + * + * @param chatId Chat ID + * @param adminId Admin who created the links + * @param params + * @internal + */ +export async function* getInviteLinks( + this: TelegramClient, + chatId: InputPeerLike, + adminId: InputPeerLike, + params?: { + /** + * Whether to fetch revoked invite links + */ + revoked?: boolean + + /** + * Limit the number of invite links to be fetched. + * By default, all links are fetched. + */ + limit?: number + + /** + * Size of chunks which are fetched. Usually not needed. + * + * Defaults to `100` + */ + chunkSize?: number + } +): AsyncIterableIterator { + if (!params) params = {} + + let current = 0 + const total = params.limit || Infinity + const chunkSize = Math.min(params.chunkSize ?? 100, total) + + const peer = normalizeToInputPeer(await this.resolvePeer(chatId)) + const admin = normalizeToInputUser(await this.resolvePeer(adminId)) + + if (!admin) throw new MtCuteInvalidPeerTypeError(adminId, 'user') + + let offsetDate: number | undefined = undefined + let offsetLink: string | undefined = undefined + + for (;;) { + const res: tl.RpcCallReturn['messages.getExportedChatInvites'] = await this.call( + { + _: 'messages.getExportedChatInvites', + peer, + adminId: admin, + limit: Math.min(chunkSize, total - current), + offsetDate, + offsetLink, + } + ) + + if (!res.invites.length) break + + const { users } = createUsersChatsIndex(res) + + const last = res.invites[res.invites.length - 1] + offsetDate = last.date + offsetLink = last.link + + for (const it of res.invites) { + yield new ChatInviteLink(this, it, users) + } + + current += res.invites.length + if (current >= total) break + } +} diff --git a/packages/client/src/methods/invite-links/get-primary-invite-link.ts b/packages/client/src/methods/invite-links/get-primary-invite-link.ts new file mode 100644 index 00000000..25e0ce2e --- /dev/null +++ b/packages/client/src/methods/invite-links/get-primary-invite-link.ts @@ -0,0 +1,40 @@ +import { TelegramClient } from '../../client' +import { + ChatInviteLink, + InputPeerLike, + MtCuteTypeAssertionError, +} from '../../types' +import { + createUsersChatsIndex, + normalizeToInputPeer, +} from '../../utils/peer-utils' + +/** + * Get primary invite link of a chat + * + * @param chatId Chat ID + * @internal + */ +export async function getPrimaryInviteLink( + this: TelegramClient, + chatId: InputPeerLike +): Promise { + const res = await this.call({ + _: 'messages.getExportedChatInvites', + peer: normalizeToInputPeer(await this.resolvePeer(chatId)), + adminId: { _: 'inputUserSelf' }, + limit: 1, + revoked: false, + }) + + if (!res.invites[0]?.permanent) + throw new MtCuteTypeAssertionError( + 'messages.getExportedChatInvites (@ .invites[0].permanent)', + 'true', + 'false' + ) + + const { users } = createUsersChatsIndex(res) + + return new ChatInviteLink(this, res.invites[0], users) +} diff --git a/packages/client/src/methods/invite-links/revoke-invite-link.ts b/packages/client/src/methods/invite-links/revoke-invite-link.ts new file mode 100644 index 00000000..9075c6b9 --- /dev/null +++ b/packages/client/src/methods/invite-links/revoke-invite-link.ts @@ -0,0 +1,33 @@ +import { TelegramClient } from '../../client' +import { ChatInviteLink, InputPeerLike } from '../../types' +import { createUsersChatsIndex, normalizeToInputPeer } from '../../utils/peer-utils' + +/** + * Revoke an invite link. + * + * If `link` is a primary invite link, a new invite link will be + * generated automatically by Telegram + * + * @param chatId Chat ID + * @param link Invite link to revoke + * @returns If `link` is a primary invite, newly generated invite link, otherwise the revoked link + * @internal + */ +export async function revokeInviteLink( + this: TelegramClient, + chatId: InputPeerLike, + link: string +): Promise { + const res = await this.call({ + _: 'messages.editExportedChatInvite', + peer: normalizeToInputPeer(await this.resolvePeer(chatId)), + link, + revoked: true + }) + + const { users } = createUsersChatsIndex(res) + + const invite = res._ === 'messages.exportedChatInviteReplaced' ? res.newInvite : res.invite + + return new ChatInviteLink(this, invite, users) +} diff --git a/packages/client/src/methods/users/iter-profile-photos.ts b/packages/client/src/methods/users/iter-profile-photos.ts index 5b0f1c1d..d022fb17 100644 --- a/packages/client/src/methods/users/iter-profile-photos.ts +++ b/packages/client/src/methods/users/iter-profile-photos.ts @@ -69,7 +69,10 @@ export async function* iterProfilePhotos( offset += res.photos.length - yield* res.photos.map((it) => new Photo(this, it as tl.RawPhoto)) + for (const it of res.photos) { + yield new Photo(this, it as tl.RawPhoto) + } + current += res.photos.length if (current >= total) break diff --git a/packages/client/src/types/peers/chat-invite-link.ts b/packages/client/src/types/peers/chat-invite-link.ts index 5146de0a..7c0bd2cb 100644 --- a/packages/client/src/types/peers/chat-invite-link.ts +++ b/packages/client/src/types/peers/chat-invite-link.ts @@ -1,6 +1,14 @@ import { makeInspectable } from '../utils' import { TelegramClient } from '../../client' import { tl } from '@mtcute/tl' +import { User } from './user' + +export namespace ChatInviteLink { + export interface JoinedMember { + user: User + date: Date + } +} /** * An invite link @@ -9,9 +17,12 @@ export class ChatInviteLink { readonly client: TelegramClient readonly raw: tl.RawChatInviteExported - constructor (client: TelegramClient, raw: tl.RawChatInviteExported) { + readonly _users?: Record + + constructor (client: TelegramClient, raw: tl.RawChatInviteExported, users?: Record) { this.client = client this.raw = raw + this._users = users } /** @@ -24,6 +35,20 @@ export class ChatInviteLink { return this.raw.link } + private _creator?: User + /** + * Creator of the invite link, if available + */ + get creator(): User | null { + if (!this._users) return null + + if (!this._creator) { + this._creator = new User(this.client, this._users[this.raw.adminId]) + } + + return this._creator + } + /** * Creation date of the link */ @@ -73,7 +98,7 @@ export class ChatInviteLink { * Number of users currently in the chat that joined using this link */ get usage(): number { - return this.raw.usageLimit ?? 0 + return this.raw.usage ?? 0 } } diff --git a/packages/client/src/utils/peer-utils.ts b/packages/client/src/utils/peer-utils.ts index c627f3ad..84a2caf3 100644 --- a/packages/client/src/utils/peer-utils.ts +++ b/packages/client/src/utils/peer-utils.ts @@ -95,12 +95,12 @@ export function peerToInputPeer(peer: tl.TypePeer, accessHash = bigInt.zero): tl export function createUsersChatsIndex( obj: { - users: tl.TypeUser[] - chats: tl.TypeChat[] + users?: tl.TypeUser[] + chats?: tl.TypeChat[] }, second?: { - users: tl.TypeUser[] - chats: tl.TypeChat[] + users?: tl.TypeUser[] + chats?: tl.TypeChat[] } ): { users: Record @@ -108,12 +108,12 @@ export function createUsersChatsIndex( } { const users: Record = {} const chats: Record = {} - obj.users.forEach((e) => (users[e.id] = e)) - obj.chats.forEach((e) => (chats[e.id] = e)) + obj.users?.forEach((e) => (users[e.id] = e)) + obj.chats?.forEach((e) => (chats[e.id] = e)) if (second) { - second.users.forEach((e) => (users[e.id] = e)) - second.chats.forEach((e) => (chats[e.id] = e)) + second.users?.forEach((e) => (users[e.id] = e)) + second.chats?.forEach((e) => (chats[e.id] = e)) } return { users, chats }