diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index a406d775..f856ad11 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -87,6 +87,14 @@ import { setDefaultParseMode, unregisterParseMode, } from './methods/parse-modes/parse-modes' +import { changeCloudPassword } from './methods/pasword/change-cloud-password' +import { enableCloudPassword } from './methods/pasword/enable-cloud-password' +import { + cancelPasswordEmail, + resendPasswordEmail, + verifyPasswordEmail, +} from './methods/pasword/password-email' +import { removeCloudPassword } from './methods/pasword/remove-cloud-password' import { addStickerToSet } from './methods/stickers/add-sticker-to-set' import { createStickerSet } from './methods/stickers/create-sticker-set' import { deleteStickerFromSet } from './methods/stickers/delete-sticker-from-set' @@ -103,11 +111,18 @@ import { dispatchUpdate, } from './methods/updates' import { blockUser } from './methods/users/block-user' +import { deleteProfilePhotos } from './methods/users/delete-profile-photos' import { getCommonChats } from './methods/users/get-common-chats' import { getMe } from './methods/users/get-me' +import { getProfilePhotos } from './methods/users/get-profile-photos' import { getUsers } from './methods/users/get-users' +import { iterProfilePhotos } from './methods/users/iter-profile-photos' import { resolvePeer } from './methods/users/resolve-peer' import { setOffline } from './methods/users/set-offline' +import { setProfilePhoto } from './methods/users/set-profile-photo' +import { unblockUser } from './methods/users/unblock-user' +import { updateProfile } from './methods/users/update-profile' +import { updateUsername } from './methods/users/update-username' import { IMessageEntityParser } from './parser' import { Readable } from 'stream' import { @@ -126,6 +141,7 @@ import { Message, PartialExcept, PartialOnly, + Photo, Poll, ReplyMarkup, SentCode, @@ -2026,6 +2042,57 @@ export interface TelegramClient extends BaseTelegramClient { * @throws MtCuteError When given parse mode is not registered. */ setDefaultParseMode(name: string): void + /** + * Change your 2FA password + * + * @param currentPassword Current password as plaintext + * @param newPassword New password as plaintext + * @param hint Hint for the new password + */ + changeCloudPassword( + currentPassword: string, + newPassword: string, + hint?: string + ): Promise + /** + * Enable 2FA password on your account + * + * Note that if you pass `email`, `EmailUnconfirmedError` may be + * thrown, and you should use {@link verifyPasswordEmail}, + * {@link resendPasswordEmail} or {@link cancelPasswordEmail}, + * and the call this method again + * + * @param password 2FA password as plaintext + * @param hint Hint for the new password + * @param email Recovery email + */ + enableCloudPassword( + password: string, + hint?: string, + email?: string + ): Promise + /** + * Verify an email to use as 2FA recovery method + * + * @param code Code which was sent via email + */ + verifyPasswordEmail(code: string): Promise + /** + * Resend the code to verify an email to use as 2FA recovery method. + * + */ + resendPasswordEmail(): Promise + /** + * Cancel the code that was sent to verify an email to use as 2FA recovery method + * + */ + cancelPasswordEmail(): Promise + /** + * Remove 2FA password from your account + * + * @param password 2FA password as plaintext + */ + removeCloudPassword(password: string): Promise /** * Add a sticker to a sticker set. * @@ -2226,10 +2293,17 @@ export interface TelegramClient extends BaseTelegramClient { /** * Block a user * - * @param id User ID, its username or phone number - * @returns Whether the action was successful + * @param id User ID, username or phone number */ - blockUser(id: InputPeerLike): Promise + blockUser(id: InputPeerLike): Promise + /** + * Delete your own profile photos + * + * @param ids ID(s) of the photos. Can be file IDs or raw TL objects + */ + deleteProfilePhotos( + ids: MaybeArray + ): Promise /** * Get a list of common chats you have with a given user * @@ -2242,6 +2316,30 @@ export interface TelegramClient extends BaseTelegramClient { * */ getMe(): Promise + /** + * Get a list of profile pictures of a user + * + * @param userId User ID, username, phone number, `"me"` or `"self"` + * @param params + */ + getProfilePhotos( + userId: InputPeerLike, + params?: { + /** + * Offset from which to fetch. + * + * Defaults to `0` + */ + offset?: number + + /** + * Maximum number of items to fetch (up to 100) + * + * Defaults to `100` + */ + limit?: number + } + ): Promise /** * Get information about a single user. * @@ -2255,6 +2353,43 @@ export interface TelegramClient extends BaseTelegramClient { * @param ids Users' identifiers. Can be ID, username, phone number, `"me"`, `"self"` or TL object */ getUsers(ids: InputPeerLike[]): Promise + /** + * Iterate over profile photos + * + * @param userId User ID, username, phone number, `"me"` or `"self"` + * @param params + */ + iterProfilePhotos( + userId: InputPeerLike, + params?: { + /** + * Offset from which to fetch. + * + * Defaults to `0` + */ + offset?: number + + /** + * Maximum number of items to fetch + * + * Defaults to `Infinity`, i.e. all items are fetched + */ + limit?: number + + /** + * Size of chunks which are fetched. Usually not needed. + * + * Defaults to `100` + */ + chunkSize?: number + + /** + * If set, the method will return only photos + * with IDs less than the set one + */ + maxId?: tl.Long + } + ): AsyncIterableIterator /** * Get the `InputPeer` of a known peer id. * Useful when an `InputPeer` is needed. @@ -2270,6 +2405,58 @@ export interface TelegramClient extends BaseTelegramClient { * @param offline (default: `true`) Whether the user is currently offline */ setOffline(offline?: boolean): Promise + /** + * Set a new profile photo or video. + * + * @param type Media type (photo or video) + * @param media Input media file + * @param previewSec + * When `type = video`, timestamp in seconds which will be shown + * as a static preview. + */ + setProfilePhoto( + type: 'photo' | 'video', + media: InputFileLike, + previewSec?: number + ): Promise + /** + * Unblock a user + * + * @param id User ID, username or phone number + */ + unblockUser(id: InputPeerLike): Promise + /** + * Update your profile details. + * + * Only pass fields that you want to change. + * + * @param params + */ + updateProfile(params: { + /** + * New first name + */ + firstName?: string + + /** + * New last name. Pass `''` (empty string) to remove it + */ + lastName?: string + + /** + * New bio (max 70 chars). Pass `''` (empty string) to remove it + */ + bio?: string + }): Promise + /** + * Change username of the current user. + * + * Note that bots usernames must be changed through + * bot support or re-created from scratch. + * + * @param username New username (5-32 chars, allowed chars: `a-zA-Z0-9_`), or `null` to remove + */ + updateUsername(username: string | null): Promise } /** @internal */ export class TelegramClient extends BaseTelegramClient { @@ -2386,6 +2573,12 @@ export class TelegramClient extends BaseTelegramClient { unregisterParseMode = unregisterParseMode getParseMode = getParseMode setDefaultParseMode = setDefaultParseMode + changeCloudPassword = changeCloudPassword + enableCloudPassword = enableCloudPassword + verifyPasswordEmail = verifyPasswordEmail + resendPasswordEmail = resendPasswordEmail + cancelPasswordEmail = cancelPasswordEmail + removeCloudPassword = removeCloudPassword addStickerToSet = addStickerToSet createStickerSet = createStickerSet deleteStickerFromSet = deleteStickerFromSet @@ -2400,9 +2593,16 @@ export class TelegramClient extends BaseTelegramClient { protected _handleUpdate = _handleUpdate catchUp = catchUp blockUser = blockUser + deleteProfilePhotos = deleteProfilePhotos getCommonChats = getCommonChats getMe = getMe + getProfilePhotos = getProfilePhotos getUsers = getUsers + iterProfilePhotos = iterProfilePhotos resolvePeer = resolvePeer setOffline = setOffline + setProfilePhoto = setProfilePhoto + unblockUser = unblockUser + updateProfile = updateProfile + updateUsername = updateUsername } diff --git a/packages/client/src/methods/_imports.ts b/packages/client/src/methods/_imports.ts index de3d79b4..3d8a7741 100644 --- a/packages/client/src/methods/_imports.ts +++ b/packages/client/src/methods/_imports.ts @@ -32,7 +32,8 @@ import { TakeoutSession, StickerSet, Poll, - TypingStatus + TypingStatus, + Photo } from '../types' // @copy diff --git a/packages/client/src/methods/chats/set-chat-photo.ts b/packages/client/src/methods/chats/set-chat-photo.ts index d20f87be..726849f9 100644 --- a/packages/client/src/methods/chats/set-chat-photo.ts +++ b/packages/client/src/methods/chats/set-chat-photo.ts @@ -6,7 +6,10 @@ import { MtCuteArgumentError, MtCuteInvalidPeerTypeError, } from '../../types' -import { normalizeToInputChannel, normalizeToInputPeer } from '../../utils/peer-utils' +import { + normalizeToInputChannel, + normalizeToInputPeer, +} from '../../utils/peer-utils' import { tl } from '@mtcute/tl' import { fileIdToInputPhoto, tdFileId } from '@mtcute/file-id' @@ -31,7 +34,13 @@ export async function setChatPhoto( previewSec?: number ): Promise { const chat = normalizeToInputPeer(await this.resolvePeer(chatId)) - if (!(chat._ === 'inputPeerChat' || chat._ === 'inputPeerChannel')) + if ( + !( + chat._ === 'inputPeerChat' || + chat._ === 'inputPeerChannel' || + chat._ === 'inputPeerChannelFromMessage' + ) + ) throw new MtCuteInvalidPeerTypeError(chatId, 'chat or channel') let photo: tl.TypeInputChatPhoto | undefined = undefined @@ -49,11 +58,16 @@ export async function setChatPhoto( const input = fileIdToInputPhoto(media) photo = { _: 'inputChatPhoto', - id: input + id: input, } } } else if (typeof media === 'object' && tl.isAnyInputMedia(media)) { - throw new MtCuteArgumentError("Chat photo can't be InputMedia") + if (media._ === 'inputMediaPhoto') { + photo = { + _: 'inputChatPhoto', + id: media.id, + } + } else throw new MtCuteArgumentError("Chat photo can't be InputMedia") } else if (isUploadedFile(media)) { inputFile = media.inputFile } else if (typeof media === 'object' && tl.isAnyInputFile(media)) { @@ -69,7 +83,7 @@ export async function setChatPhoto( photo = { _: 'inputChatUploadedPhoto', [type === 'photo' ? 'file' : 'video']: inputFile!, - videoStartTs: previewSec + videoStartTs: previewSec, } } @@ -78,13 +92,13 @@ export async function setChatPhoto( res = await this.call({ _: 'messages.editChatPhoto', chatId: chat.chatId, - photo + photo, }) } else { res = await this.call({ _: 'channels.editPhoto', channel: normalizeToInputChannel(chat)!, - photo + photo, }) } this._handleUpdate(res) diff --git a/packages/client/src/methods/users/block-user.ts b/packages/client/src/methods/users/block-user.ts index 82fd1c4e..44a58bc0 100644 --- a/packages/client/src/methods/users/block-user.ts +++ b/packages/client/src/methods/users/block-user.ts @@ -5,15 +5,14 @@ import { normalizeToInputPeer } from '../../utils/peer-utils' /** * Block a user * - * @param id User ID, its username or phone number - * @returns Whether the action was successful + * @param id User ID, username or phone number * @internal */ export async function blockUser( this: TelegramClient, id: InputPeerLike -): Promise { - return this.call({ +): Promise { + await this.call({ _: 'contacts.block', id: normalizeToInputPeer(await this.resolvePeer(id)), }) diff --git a/packages/client/src/methods/users/delete-profile-photos.ts b/packages/client/src/methods/users/delete-profile-photos.ts new file mode 100644 index 00000000..b7a7f3cf --- /dev/null +++ b/packages/client/src/methods/users/delete-profile-photos.ts @@ -0,0 +1,30 @@ +import { TelegramClient } from '../../client' +import { MaybeArray } from '@mtcute/core' +import { tl } from '@mtcute/tl' +import { fileIdToInputPhoto } from '../../../../file-id' + +/** + * Delete your own profile photos + * + * @param ids ID(s) of the photos. Can be file IDs or raw TL objects + * @internal + */ +export async function deleteProfilePhotos( + this: TelegramClient, + ids: MaybeArray +): Promise { + if (!Array.isArray(ids)) ids = [ids] + + const photos = ids.map((id) => { + if (typeof id === 'string') { + return fileIdToInputPhoto(id) + } + + return id + }) + + await this.call({ + _: 'photos.deletePhotos', + id: photos + }) +} diff --git a/packages/client/src/methods/users/get-profile-photos.ts b/packages/client/src/methods/users/get-profile-photos.ts new file mode 100644 index 00000000..d3f31c97 --- /dev/null +++ b/packages/client/src/methods/users/get-profile-photos.ts @@ -0,0 +1,47 @@ +import { TelegramClient } from '../../client' +import { InputPeerLike, MtCuteInvalidPeerTypeError, Photo } from '../../types' +import { normalizeToInputUser } from '../../utils/peer-utils' +import bigInt from 'big-integer' +import { tl } from '@mtcute/tl' + +/** + * Get a list of profile pictures of a user + * + * @param userId User ID, username, phone number, `"me"` or `"self"` + * @param params + * @internal + */ +export async function getProfilePhotos( + this: TelegramClient, + userId: InputPeerLike, + params?: { + /** + * Offset from which to fetch. + * + * Defaults to `0` + */ + offset?: number + + /** + * Maximum number of items to fetch (up to 100) + * + * Defaults to `100` + */ + limit?: number + } +): Promise { + if (!params) params = {} + + const peer = normalizeToInputUser(await this.resolvePeer(userId)) + if (!peer) throw new MtCuteInvalidPeerTypeError(userId, 'user') + + const res = await this.call({ + _: 'photos.getUserPhotos', + userId: peer, + offset: params.offset ?? 0, + limit: params.limit ?? 100, + maxId: bigInt.zero + }) + + return res.photos.map((it) => new Photo(this, it as tl.RawPhoto)) +} diff --git a/packages/client/src/methods/users/iter-profile-photos.ts b/packages/client/src/methods/users/iter-profile-photos.ts new file mode 100644 index 00000000..5b0f1c1d --- /dev/null +++ b/packages/client/src/methods/users/iter-profile-photos.ts @@ -0,0 +1,77 @@ +import { TelegramClient } from '../../client' +import { InputPeerLike, MtCuteInvalidPeerTypeError, Photo } from '../../types' +import { normalizeToInputUser } from '../../utils/peer-utils' +import { tl } from '@mtcute/tl' +import bigInt from 'big-integer' + +/** + * Iterate over profile photos + * + * @param userId User ID, username, phone number, `"me"` or `"self"` + * @param params + * @internal + */ +export async function* iterProfilePhotos( + this: TelegramClient, + userId: InputPeerLike, + params?: { + /** + * Offset from which to fetch. + * + * Defaults to `0` + */ + offset?: number + + /** + * Maximum number of items to fetch + * + * Defaults to `Infinity`, i.e. all items are fetched + */ + limit?: number + + /** + * Size of chunks which are fetched. Usually not needed. + * + * Defaults to `100` + */ + chunkSize?: number + + /** + * If set, the method will return only photos + * with IDs less than the set one + */ + maxId?: tl.Long + } +): AsyncIterableIterator { + if (!params) params = {} + + const peer = normalizeToInputUser(await this.resolvePeer(userId)) + if (!peer) throw new MtCuteInvalidPeerTypeError(userId, 'user') + + let offset = params.offset || 0 + let current = 0 + const total = params.limit || Infinity + + const limit = Math.min(params.chunkSize || 100, total) + + const maxId = params.maxId || bigInt.zero + + for (;;) { + const res = await this.call({ + _: 'photos.getUserPhotos', + userId: peer, + limit: Math.min(limit, total - current), + offset, + maxId + }) + + if (!res.photos.length) break + + offset += res.photos.length + + yield* res.photos.map((it) => new Photo(this, it as tl.RawPhoto)) + current += res.photos.length + + if (current >= total) break + } +} diff --git a/packages/client/src/methods/users/set-profile-photo.ts b/packages/client/src/methods/users/set-profile-photo.ts new file mode 100644 index 00000000..c55db7e6 --- /dev/null +++ b/packages/client/src/methods/users/set-profile-photo.ts @@ -0,0 +1,28 @@ +import { TelegramClient } from '../../client' +import { InputFileLike, Photo } from '../../types' +import { tl } from '@mtcute/tl' + +/** + * Set a new profile photo or video. + * + * @param type Media type (photo or video) + * @param media Input media file + * @param previewSec + * When `type = video`, timestamp in seconds which will be shown + * as a static preview. + * @internal + */ +export async function setProfilePhoto( + this: TelegramClient, + type: 'photo' | 'video', + media: InputFileLike, + previewSec?: number +): Promise { + const res = await this.call({ + _: 'photos.uploadProfilePhoto', + [type === 'photo' ? 'file' : 'video']: await this._normalizeInputFile(media, {}), + videoStartTs: previewSec + }) + + return new Photo(this, res.photo as tl.RawPhoto) +} diff --git a/packages/client/src/methods/users/unblock-user.ts b/packages/client/src/methods/users/unblock-user.ts new file mode 100644 index 00000000..c340503e --- /dev/null +++ b/packages/client/src/methods/users/unblock-user.ts @@ -0,0 +1,19 @@ +import { TelegramClient } from '../../client' +import { InputPeerLike } from '../../types' +import { normalizeToInputPeer } from '../../utils/peer-utils' + +/** + * Unblock a user + * + * @param id User ID, username or phone number + * @internal + */ +export async function unblockUser( + this: TelegramClient, + id: InputPeerLike +): Promise { + await this.call({ + _: 'contacts.unblock', + id: normalizeToInputPeer(await this.resolvePeer(id)), + }) +} diff --git a/packages/client/src/methods/users/update-profile.ts b/packages/client/src/methods/users/update-profile.ts new file mode 100644 index 00000000..539efe38 --- /dev/null +++ b/packages/client/src/methods/users/update-profile.ts @@ -0,0 +1,39 @@ +import { TelegramClient } from '../../client' +import { User } from '../../types' + +/** + * Update your profile details. + * + * Only pass fields that you want to change. + * + * @param params + * @internal + */ +export async function updateProfile( + this: TelegramClient, + params: { + /** + * New first name + */ + firstName?: string + + /** + * New last name. Pass `''` (empty string) to remove it + */ + lastName?: string + + /** + * New bio (max 70 chars). Pass `''` (empty string) to remove it + */ + bio?: string + } +): Promise { + const res = await this.call({ + _: 'account.updateProfile', + firstName: params.firstName, + lastName: params.lastName, + about: params.bio + }) + + return new User(this, res) +} diff --git a/packages/client/src/methods/users/update-username.ts b/packages/client/src/methods/users/update-username.ts new file mode 100644 index 00000000..f43730e2 --- /dev/null +++ b/packages/client/src/methods/users/update-username.ts @@ -0,0 +1,25 @@ +import { TelegramClient } from '../../client' +import { User } from '../../types' + +/** + * Change username of the current user. + * + * Note that bots usernames must be changed through + * bot support or re-created from scratch. + * + * @param username New username (5-32 chars, allowed chars: `a-zA-Z0-9_`), or `null` to remove + * @internal + */ +export async function updateUsername( + this: TelegramClient, + username: string | null +): Promise { + if (username === null) username = '' + + const res = await this.call({ + _: 'account.updateUsername', + username + }) + + return new User(this, res) +}