diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 744a9f0b..b80173cc 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -32,11 +32,13 @@ import { answerCallbackQuery } from './methods/bots/answer-callback-query' import { answerInlineQuery } from './methods/bots/answer-inline-query' import { answerPreCheckoutQuery } from './methods/bots/answer-pre-checkout-query' import { deleteMyCommands } from './methods/bots/delete-my-commands' +import { getBotInfo } from './methods/bots/get-bot-info' import { getBotMenuButton } from './methods/bots/get-bot-menu-button' import { getCallbackAnswer } from './methods/bots/get-callback-answer' import { getGameHighScores, getInlineGameHighScores } from './methods/bots/get-game-high-scores' import { getMyCommands } from './methods/bots/get-my-commands' import { _normalizeCommandScope } from './methods/bots/normalize-command-scope' +import { setBotInfo } from './methods/bots/set-bot-info' import { setBotMenuButton } from './methods/bots/set-bot-menu-button' import { setGameScore, setInlineGameScore } from './methods/bots/set-game-score' import { setMyCommands } from './methods/bots/set-my-commands' @@ -183,6 +185,7 @@ import { getCustomEmojis } from './methods/stickers/get-custom-emojis' import { getInstalledStickers } from './methods/stickers/get-installed-stickers' import { getStickerSet } from './methods/stickers/get-sticker-set' import { moveStickerInSet } from './methods/stickers/move-sticker-in-set' +import { setChatStickerSet } from './methods/stickers/set-chat-sticker-set' import { setStickerSetThumb } from './methods/stickers/set-sticker-set-thumb' import { applyBoost } from './methods/stories/apply-boost' import { canApplyBoost, CanApplyBoostResult } from './methods/stories/can-apply-boost' @@ -285,6 +288,7 @@ import { InputPeerLike, InputPrivacyRule, InputReaction, + InputStickerSet, InputStickerSetItem, MaybeDynamic, Message, @@ -916,6 +920,23 @@ export interface TelegramClient extends BaseTelegramClient { */ langCode?: string }): Promise + /** + * Gets information about a bot the current uzer owns (or the current bot) + * + */ + getBotInfo(params: { + /** + * When called by a user, a bot the user owns must be specified. + * When called by a bot, must be empty + */ + bot?: InputPeerLike + + /** + * If passed, will retrieve the bot's description in the given language. + * If left empty, will retrieve the fallback description. + */ + langCode?: string + }): Promise /** * Fetches the menu button set for the given user. * @@ -999,6 +1020,32 @@ export interface TelegramClient extends BaseTelegramClient { _normalizeCommandScope( scope: tl.TypeBotCommandScope | BotCommands.IntermediateScope, ): Promise + /** + * Sets information about a bot the current uzer owns (or the current bot) + * + */ + setBotInfo(params: { + /** + * When called by a user, a bot the user owns must be specified. + * When called by a bot, must be empty + */ + bot?: InputPeerLike + + /** + * If passed, will update the bot's description in the given language. + * If left empty, will change the fallback description. + */ + langCode?: string + + /** New bot name */ + name?: string + + /** New bio text (displayed in the profile) */ + bio?: string + + /** New description text (displayed when the chat is empty) */ + description?: string + }): Promise /** * Sets a menu button for the given user. * @@ -1107,15 +1154,18 @@ export interface TelegramClient extends BaseTelegramClient { */ archiveChats(chats: MaybeArray): Promise /** - * Ban a user from a legacy group, a supergroup or a channel. + * Ban a user/channel from a legacy group, a supergroup or a channel. * They will not be able to re-join the group on their own, - * manual administrator's action is required. + * manual administrator's action will be required. + * + * When banning a channel, the user won't be able to use + * any of their channels to post until the ban is lifted. * * @param chatId Chat ID - * @param userId User ID + * @param peerId User/Channel ID * @returns Service message about removed user, if one was generated. */ - banChatMember(chatId: InputPeerLike, userId: InputPeerLike): Promise + banChatMember(chatId: InputPeerLike, peerId: InputPeerLike): Promise /** * Create a new broadcast channel * @@ -1666,7 +1716,7 @@ export interface TelegramClient extends BaseTelegramClient { unarchiveChats(chats: MaybeArray): Promise /** - * Unban a user from a supergroup or a channel, + * Unban a user/channel from a supergroup or a channel, * or remove any restrictions that they have. * Unbanning does not add the user back to the chat, this * just allows the user to join the chat again, if they want. @@ -1674,12 +1724,12 @@ export interface TelegramClient extends BaseTelegramClient { * This method acts as a no-op in case a legacy group is passed. * * @param chatId Chat ID - * @param userId User ID + * @param peerId User/channel ID */ - unbanChatMember(chatId: InputPeerLike, userId: InputPeerLike): Promise + unbanChatMember(chatId: InputPeerLike, peerId: InputPeerLike): Promise /** - * Unban a user from a supergroup or a channel, + * Unban a user/channel from a supergroup or a channel, * or remove any restrictions that they have. * Unbanning does not add the user back to the chat, this * just allows the user to join the chat again, if they want. @@ -1687,9 +1737,9 @@ export interface TelegramClient extends BaseTelegramClient { * This method acts as a no-op in case a legacy group is passed. * * @param chatId Chat ID - * @param userId User ID + * @param peerId User/channel ID */ - unrestrictChatMember(chatId: InputPeerLike, userId: InputPeerLike): Promise + unrestrictChatMember(chatId: InputPeerLike, peerId: InputPeerLike): Promise /** * Add an existing Telegram user as a contact * @@ -3982,7 +4032,7 @@ export interface TelegramClient extends BaseTelegramClient { * @returns Modfiied sticker set */ addStickerToSet( - id: string | tl.TypeInputStickerSet, + id: InputStickerSet, sticker: InputStickerSetItem, params?: { /** @@ -4102,7 +4152,7 @@ export interface TelegramClient extends BaseTelegramClient { * * @param id Sticker pack short name, dice emoji, `"emoji"` for animated emojis or input ID */ - getStickerSet(id: string | { dice: string } | tl.TypeInputStickerSet): Promise + getStickerSet(id: InputStickerSet): Promise /** * Move a sticker in a sticker set * to another position @@ -4120,6 +4170,15 @@ export interface TelegramClient extends BaseTelegramClient { sticker: string | tdFileId.RawFullRemoteFileLocation | tl.TypeInputDocument, position: number, ): Promise + /** + * Set group sticker set for a supergroup + * + * @param id Sticker set short name or a TL object with input sticker set + * @param thumb Sticker set thumbnail + * @param params + * @returns Modified sticker set + */ + setChatStickerSet(chatId: InputPeerLike, id: InputStickerSet): Promise /** * Set sticker set thumbnail * @@ -4129,7 +4188,7 @@ export interface TelegramClient extends BaseTelegramClient { * @returns Modified sticker set */ setStickerSetThumb( - id: string | tl.TypeInputStickerSet, + id: InputStickerSet, thumb: InputFileLike | tl.TypeInputDocument, params?: { /** @@ -5056,12 +5115,14 @@ export class TelegramClient extends BaseTelegramClient { answerInlineQuery = answerInlineQuery answerPreCheckoutQuery = answerPreCheckoutQuery deleteMyCommands = deleteMyCommands + getBotInfo = getBotInfo getBotMenuButton = getBotMenuButton getCallbackAnswer = getCallbackAnswer getGameHighScores = getGameHighScores getInlineGameHighScores = getInlineGameHighScores getMyCommands = getMyCommands _normalizeCommandScope = _normalizeCommandScope + setBotInfo = setBotInfo setBotMenuButton = setBotMenuButton setGameScore = setGameScore setInlineGameScore = setInlineGameScore @@ -5213,6 +5274,7 @@ export class TelegramClient extends BaseTelegramClient { getInstalledStickers = getInstalledStickers getStickerSet = getStickerSet moveStickerInSet = moveStickerInSet + setChatStickerSet = setChatStickerSet setStickerSetThumb = setStickerSetThumb applyBoost = applyBoost canApplyBoost = canApplyBoost diff --git a/packages/client/src/methods/_imports.ts b/packages/client/src/methods/_imports.ts index 94ce0f23..b739a591 100644 --- a/packages/client/src/methods/_imports.ts +++ b/packages/client/src/methods/_imports.ts @@ -48,6 +48,7 @@ import { InputPeerLike, InputPrivacyRule, InputReaction, + InputStickerSet, InputStickerSetItem, MaybeDynamic, Message, diff --git a/packages/client/src/methods/bots/get-bot-info.ts b/packages/client/src/methods/bots/get-bot-info.ts new file mode 100644 index 00000000..78861640 --- /dev/null +++ b/packages/client/src/methods/bots/get-bot-info.ts @@ -0,0 +1,35 @@ +import { tl } from '@mtcute/core' + +import { TelegramClient } from '../../client' +import { InputPeerLike } from '../../types' +import { normalizeToInputUser } from '../../utils/peer-utils' + +/** + * Gets information about a bot the current uzer owns (or the current bot) + * + * @internal + */ +export async function getBotInfo( + this: TelegramClient, + params: { + /** + * When called by a user, a bot the user owns must be specified. + * When called by a bot, must be empty + */ + bot?: InputPeerLike + + /** + * If passed, will retrieve the bot's description in the given language. + * If left empty, will retrieve the fallback description. + */ + langCode?: string + }, +): Promise { + const { bot, langCode = '' } = params + + return this.call({ + _: 'bots.getBotInfo', + bot: bot ? normalizeToInputUser(await this.resolvePeer(bot), bot) : undefined, + langCode: langCode, + }) +} diff --git a/packages/client/src/methods/bots/set-bot-info.ts b/packages/client/src/methods/bots/set-bot-info.ts new file mode 100644 index 00000000..e31fc587 --- /dev/null +++ b/packages/client/src/methods/bots/set-bot-info.ts @@ -0,0 +1,45 @@ +import { TelegramClient } from '../../client' +import { InputPeerLike } from '../../types' +import { normalizeToInputUser } from '../../utils/peer-utils' + +/** + * Sets information about a bot the current uzer owns (or the current bot) + * + * @internal + */ +export async function setBotInfo( + this: TelegramClient, + params: { + /** + * When called by a user, a bot the user owns must be specified. + * When called by a bot, must be empty + */ + bot?: InputPeerLike + + /** + * If passed, will update the bot's description in the given language. + * If left empty, will change the fallback description. + */ + langCode?: string + + /** New bot name */ + name?: string + + /** New bio text (displayed in the profile) */ + bio?: string + + /** New description text (displayed when the chat is empty) */ + description?: string + }, +): Promise { + const { bot, langCode = '', name, bio, description } = params + + await this.call({ + _: 'bots.setBotInfo', + bot: bot ? normalizeToInputUser(await this.resolvePeer(bot), bot) : undefined, + langCode: langCode, + name, + about: bio, + description, + }) +} diff --git a/packages/client/src/methods/chats/ban-chat-member.ts b/packages/client/src/methods/chats/ban-chat-member.ts index b68c3529..2bef7f45 100644 --- a/packages/client/src/methods/chats/ban-chat-member.ts +++ b/packages/client/src/methods/chats/ban-chat-member.ts @@ -10,29 +10,32 @@ import { } from '../../utils/peer-utils' /** - * Ban a user from a legacy group, a supergroup or a channel. + * Ban a user/channel from a legacy group, a supergroup or a channel. * They will not be able to re-join the group on their own, - * manual administrator's action is required. + * manual administrator's action will be required. + * + * When banning a channel, the user won't be able to use + * any of their channels to post until the ban is lifted. * * @param chatId Chat ID - * @param userId User ID + * @param peerId User/Channel ID * @returns Service message about removed user, if one was generated. * @internal */ export async function banChatMember( this: TelegramClient, chatId: InputPeerLike, - userId: InputPeerLike, + peerId: InputPeerLike, ): Promise { const chat = await this.resolvePeer(chatId) - const user = await this.resolvePeer(userId) + const peer = await this.resolvePeer(peerId) let res if (isInputPeerChannel(chat)) { res = await this.call({ _: 'channels.editBanned', channel: normalizeToInputChannel(chat), - participant: user, + participant: peer, bannedRights: { _: 'chatBannedRights', // bans can't be temporary. @@ -44,7 +47,7 @@ export async function banChatMember( res = await this.call({ _: 'messages.deleteChatUser', chatId: chat.chatId, - userId: normalizeToInputUser(user), + userId: normalizeToInputUser(peer), }) } else throw new MtInvalidPeerTypeError(chatId, 'chat or channel') diff --git a/packages/client/src/methods/chats/unban-chat-member.ts b/packages/client/src/methods/chats/unban-chat-member.ts index 44dc44c6..09fd5453 100644 --- a/packages/client/src/methods/chats/unban-chat-member.ts +++ b/packages/client/src/methods/chats/unban-chat-member.ts @@ -4,7 +4,7 @@ import { isInputPeerChannel, isInputPeerChat, normalizeToInputChannel } from '.. // @alias=unrestrictChatMember /** - * Unban a user from a supergroup or a channel, + * Unban a user/channel from a supergroup or a channel, * or remove any restrictions that they have. * Unbanning does not add the user back to the chat, this * just allows the user to join the chat again, if they want. @@ -12,22 +12,22 @@ import { isInputPeerChannel, isInputPeerChat, normalizeToInputChannel } from '.. * This method acts as a no-op in case a legacy group is passed. * * @param chatId Chat ID - * @param userId User ID + * @param peerId User/channel ID * @internal */ export async function unbanChatMember( this: TelegramClient, chatId: InputPeerLike, - userId: InputPeerLike, + peerId: InputPeerLike, ): Promise { const chat = await this.resolvePeer(chatId) - const user = await this.resolvePeer(userId) + const peer = await this.resolvePeer(peerId) if (isInputPeerChannel(chat)) { const res = await this.call({ _: 'channels.editBanned', channel: normalizeToInputChannel(chat), - participant: user, + participant: peer, bannedRights: { _: 'chatBannedRights', untilDate: 0, diff --git a/packages/client/src/methods/files/normalize-input-media.ts b/packages/client/src/methods/files/normalize-input-media.ts index dc4ea2c1..0e45ac54 100644 --- a/packages/client/src/methods/files/normalize-input-media.ts +++ b/packages/client/src/methods/files/normalize-input-media.ts @@ -248,6 +248,7 @@ export async function _normalizeInputMedia( fileReference: res.photo.fileReference, }, ttlSeconds: media.ttlSeconds, + spoiler: media.type === 'video' && media.spoiler, } } assertTypeIs('normalizeInputMedia (@ messages.uploadMedia)', res, 'messageMediaDocument') @@ -262,6 +263,7 @@ export async function _normalizeInputMedia( fileReference: res.document.fileReference, }, ttlSeconds: media.ttlSeconds, + spoiler: media.type === 'video' && media.spoiler, } } @@ -323,6 +325,7 @@ export async function _normalizeInputMedia( _: 'inputMediaUploadedPhoto', file: inputFile, ttlSeconds: media.ttlSeconds, + spoiler: media.spoiler, }, true, ) @@ -387,6 +390,7 @@ export async function _normalizeInputMedia( mimeType: mime, attributes, ttlSeconds: media.ttlSeconds, + spoiler: media.type === 'video' && media.spoiler, }, false, ) diff --git a/packages/client/src/methods/files/upload-media.ts b/packages/client/src/methods/files/upload-media.ts index fcd8fff7..c4224819 100644 --- a/packages/client/src/methods/files/upload-media.ts +++ b/packages/client/src/methods/files/upload-media.ts @@ -70,7 +70,7 @@ export async function uploadMedia( assertTypeIs('uploadMedia', res, 'messageMediaPhoto') assertTypeIs('uploadMedia', res.photo!, 'photo') - return new Photo(this, res.photo) + return new Photo(this, res.photo, res) case 'inputMediaUploadedDocument': case 'inputMediaDocument': case 'inputMediaDocumentExternal': @@ -78,7 +78,7 @@ export async function uploadMedia( assertTypeIs('uploadMedia', res.document!, 'document') // eslint-disable-next-line - return parseDocument(this, res.document) as any + return parseDocument(this, res.document, res) as any case 'inputMediaStory': throw new MtArgumentError("This media (story) can't be uploaded") default: diff --git a/packages/client/src/methods/stickers/add-sticker-to-set.ts b/packages/client/src/methods/stickers/add-sticker-to-set.ts index 8d37f2aa..b76b9aee 100644 --- a/packages/client/src/methods/stickers/add-sticker-to-set.ts +++ b/packages/client/src/methods/stickers/add-sticker-to-set.ts @@ -1,7 +1,5 @@ -import { tl } from '@mtcute/core' - import { TelegramClient } from '../../client' -import { InputStickerSetItem, StickerSet } from '../../types' +import { InputStickerSet, InputStickerSetItem, normalizeInputStickerSet, StickerSet } from '../../types' const MASK_POS = { forehead: 0, @@ -24,7 +22,7 @@ const MASK_POS = { */ export async function addStickerToSet( this: TelegramClient, - id: string | tl.TypeInputStickerSet, + id: InputStickerSet, sticker: InputStickerSetItem, params?: { /** @@ -36,16 +34,9 @@ export async function addStickerToSet( progressCallback?: (uploaded: number, total: number) => void }, ): Promise { - if (typeof id === 'string') { - id = { - _: 'inputStickerSetShortName', - shortName: id, - } - } - const res = await this.call({ _: 'stickers.addStickerToSet', - stickerset: id, + stickerset: normalizeInputStickerSet(id), sticker: { _: 'inputStickerSetItem', document: await this._normalizeFileToDocument(sticker.file, params ?? {}), diff --git a/packages/client/src/methods/stickers/get-sticker-set.ts b/packages/client/src/methods/stickers/get-sticker-set.ts index 6b565b48..62027c9c 100644 --- a/packages/client/src/methods/stickers/get-sticker-set.ts +++ b/packages/client/src/methods/stickers/get-sticker-set.ts @@ -1,7 +1,5 @@ -import { tl } from '@mtcute/core' - import { TelegramClient } from '../../client' -import { StickerSet } from '../../types' +import { InputStickerSet, normalizeInputStickerSet, StickerSet } from '../../types' /** * Get a sticker pack and stickers inside of it. @@ -9,34 +7,10 @@ import { StickerSet } from '../../types' * @param id Sticker pack short name, dice emoji, `"emoji"` for animated emojis or input ID * @internal */ -export async function getStickerSet( - this: TelegramClient, - id: string | { dice: string } | tl.TypeInputStickerSet, -): Promise { - let input: tl.TypeInputStickerSet - - if (typeof id === 'string') { - input = - id === 'emoji' ? - { - _: 'inputStickerSetAnimatedEmoji', - } : - { - _: 'inputStickerSetShortName', - shortName: id, - } - } else if ('dice' in id) { - input = { - _: 'inputStickerSetDice', - emoticon: id.dice, - } - } else { - input = id - } - +export async function getStickerSet(this: TelegramClient, id: InputStickerSet): Promise { const res = await this.call({ _: 'messages.getStickerSet', - stickerset: input, + stickerset: normalizeInputStickerSet(id), hash: 0, }) diff --git a/packages/client/src/methods/stickers/set-chat-sticker-set.ts b/packages/client/src/methods/stickers/set-chat-sticker-set.ts new file mode 100644 index 00000000..384e1915 --- /dev/null +++ b/packages/client/src/methods/stickers/set-chat-sticker-set.ts @@ -0,0 +1,24 @@ +import { TelegramClient } from '../../client' +import { InputPeerLike, InputStickerSet, normalizeInputStickerSet } from '../../types' +import { normalizeToInputChannel } from '../../utils' + +/** + * Set group sticker set for a supergroup + * + * @param id Sticker set short name or a TL object with input sticker set + * @param thumb Sticker set thumbnail + * @param params + * @returns Modified sticker set + * @internal + */ +export async function setChatStickerSet( + this: TelegramClient, + chatId: InputPeerLike, + id: InputStickerSet, +): Promise { + await this.call({ + _: 'channels.setStickers', + channel: normalizeToInputChannel(await this.resolvePeer(chatId), chatId), + stickerset: normalizeInputStickerSet(id), + }) +} diff --git a/packages/client/src/methods/stickers/set-sticker-set-thumb.ts b/packages/client/src/methods/stickers/set-sticker-set-thumb.ts index c9b89803..ae250207 100644 --- a/packages/client/src/methods/stickers/set-sticker-set-thumb.ts +++ b/packages/client/src/methods/stickers/set-sticker-set-thumb.ts @@ -1,7 +1,7 @@ import { tl } from '@mtcute/core' import { TelegramClient } from '../../client' -import { InputFileLike, StickerSet } from '../../types' +import { InputFileLike, InputStickerSet, normalizeInputStickerSet, StickerSet } from '../../types' /** * Set sticker set thumbnail @@ -14,7 +14,7 @@ import { InputFileLike, StickerSet } from '../../types' */ export async function setStickerSetThumb( this: TelegramClient, - id: string | tl.TypeInputStickerSet, + id: InputStickerSet, thumb: InputFileLike | tl.TypeInputDocument, params?: { /** @@ -26,16 +26,9 @@ export async function setStickerSetThumb( progressCallback?: (uploaded: number, total: number) => void }, ): Promise { - if (typeof id === 'string') { - id = { - _: 'inputStickerSetShortName', - shortName: id, - } - } - const res = await this.call({ _: 'stickers.setStickerSetThumb', - stickerset: id, + stickerset: normalizeInputStickerSet(id), thumb: await this._normalizeFileToDocument(thumb, params ?? {}), }) diff --git a/packages/client/src/types/bots/keyboards.ts b/packages/client/src/types/bots/keyboards.ts index 85d8df3a..88f7ebc1 100644 --- a/packages/client/src/types/bots/keyboards.ts +++ b/packages/client/src/types/bots/keyboards.ts @@ -347,6 +347,26 @@ export namespace BotKeyboard { } } + /** + * Button to request a peer from the user + * + * @param text Text of the button + * @param buttonId ID of the button that will later be passed to the service message + * @param peerType Peer type, along with filters + */ + export function requestPeer( + text: string, + buttonId: number, + peerType: tl.TypeRequestPeerType, + ): tl.RawKeyboardButtonRequestPeer { + return { + _: 'keyboardButtonRequestPeer', + text, + buttonId, + peerType, + } + } + /** * Find a button in the keyboard by its text or by predicate * diff --git a/packages/client/src/types/media/document-utils.ts b/packages/client/src/types/media/document-utils.ts index 2ff16fc5..8773d8ab 100644 --- a/packages/client/src/types/media/document-utils.ts +++ b/packages/client/src/types/media/document-utils.ts @@ -10,7 +10,11 @@ import { Voice } from './voice' export type ParsedDocument = Sticker | Voice | Audio | Video | Document /** @internal */ -export function parseDocument(client: TelegramClient, doc: tl.RawDocument): ParsedDocument { +export function parseDocument( + client: TelegramClient, + doc: tl.RawDocument, + media?: tl.RawMessageMediaDocument, +): ParsedDocument { const stickerAttr = doc.attributes.find( (a) => a._ === 'documentAttributeSticker' || a._ === 'documentAttributeCustomEmoji', ) @@ -38,12 +42,12 @@ export function parseDocument(client: TelegramClient, doc: tl.RawDocument): Pars return new Audio(client, doc, attr) case 'documentAttributeVideo': - return new Video(client, doc, attr) + return new Video(client, doc, attr, media) case 'documentAttributeImageSize': // legacy gif if (doc.mimeType === 'image/gif') { - return new Video(client, doc, attr) + return new Video(client, doc, attr, media) } } } diff --git a/packages/client/src/types/media/input-media.ts b/packages/client/src/types/media/input-media.ts index 703e4b74..7a475a81 100644 --- a/packages/client/src/types/media/input-media.ts +++ b/packages/client/src/types/media/input-media.ts @@ -151,6 +151,11 @@ export interface InputMediaDocument extends FileMixin, CaptionMixin { */ export interface InputMediaPhoto extends FileMixin, CaptionMixin { type: 'photo' + + /** + * Whether this photo should be hidden with a spoiler + */ + spoiler?: boolean } /** @@ -238,6 +243,11 @@ export interface InputMediaVideo extends FileMixin, CaptionMixin { * Only applicable to newly uploaded files. */ isRound?: boolean + + /** + * Whether this video should be hidden with a spoiler + */ + spoiler?: boolean } /** diff --git a/packages/client/src/types/media/photo.ts b/packages/client/src/types/media/photo.ts index 35bf50ec..3949349b 100644 --- a/packages/client/src/types/media/photo.ts +++ b/packages/client/src/types/media/photo.ts @@ -11,9 +11,6 @@ import { Thumbnail } from './thumbnail' export class Photo extends FileLocation { readonly type: 'photo' - /** Raw TL object */ - readonly raw: tl.RawPhoto - /** Biggest available photo width */ readonly width: number @@ -22,7 +19,7 @@ export class Photo extends FileLocation { private _bestSize?: tl.RawPhotoSize | tl.RawPhotoSizeProgressive - constructor(client: TelegramClient, raw: tl.RawPhoto) { + constructor(client: TelegramClient, readonly raw: tl.RawPhoto, readonly media?: tl.RawMessageMediaPhoto) { const location = { _: 'inputPhotoFileLocation', id: raw.id, @@ -71,7 +68,6 @@ export class Photo extends FileLocation { super(client, location, size, raw.dcId) this._bestSize = bestSize - this.raw = raw this.width = width this.height = height this.type = 'photo' @@ -105,6 +101,16 @@ export class Photo extends FileLocation { ) } + /** Whether this photo is hidden with a spoiler */ + get hasSpoiler(): boolean { + return this.media?.spoiler ?? false + } + + /** For self-destructing photos, TTL in seconds */ + get ttlSeconds(): number | null { + return this.media?.ttlSeconds ?? null + } + private _thumbnails?: Thumbnail[] /** * Available thumbnails. diff --git a/packages/client/src/types/media/video.ts b/packages/client/src/types/media/video.ts index ad2151e1..aaa5cb6f 100644 --- a/packages/client/src/types/media/video.ts +++ b/packages/client/src/types/media/video.ts @@ -24,6 +24,7 @@ export class Video extends RawDocument { client: TelegramClient, doc: tl.RawDocument, readonly attr: tl.RawDocumentAttributeVideo | tl.RawDocumentAttributeImageSize, + readonly media?: tl.RawMessageMediaDocument, ) { super(client, doc) } @@ -75,6 +76,16 @@ export class Video extends RawDocument { get isLegacyGif(): boolean { return this.attr._ === 'documentAttributeImageSize' } + + /** Whether this video is hidden with a spoiler */ + get hasSpoiler(): boolean { + return this.media?.spoiler ?? false + } + + /** For self-destructing videos, TTL in seconds */ + get ttlSeconds(): number | null { + return this.media?.ttlSeconds ?? null + } } makeInspectable(Video, ['fileSize', 'dcId'], ['inputMedia', 'inputDocument']) diff --git a/packages/client/src/types/messages/message-action.ts b/packages/client/src/types/messages/message-action.ts index d0f3ee44..198875ed 100644 --- a/packages/client/src/types/messages/message-action.ts +++ b/packages/client/src/types/messages/message-action.ts @@ -1,4 +1,4 @@ -import { tl } from '@mtcute/core' +import { getMarkedPeerId, tl } from '@mtcute/core' import { _callDiscardReasonFromTl, CallDiscardReason } from '../calls' import { Photo } from '../media' @@ -122,6 +122,14 @@ export interface ActionUserJoinedLink { readonly inviter: number } +/** + * User has joined the group via an invite link + * and was approved by an administrator + */ +export interface ActionUserJoinedApproved { + readonly type: 'user_joined_approved' +} + /** A payment was received from a user (bot) */ export interface ActionPaymentReceived { readonly type: 'payment_received' @@ -230,6 +238,17 @@ export interface ActionGroupCallEnded { readonly duration: number } +/** Group call has been scheduled */ +export interface ActionGroupCallScheduled { + readonly type: 'group_call_scheduled' + + /** TL object representing the call */ + readonly call: tl.TypeInputGroupCall + + /** Date when the call will start */ + readonly date: Date +} + /** Group call has ended */ export interface ActionGroupInvite { readonly type: 'group_call_invite' @@ -242,8 +261,8 @@ export interface ActionGroupInvite { } /** Messages TTL changed */ -export interface ActionSetTtl { - readonly type: 'set_ttl' +export interface ActionTtlChanged { + readonly type: 'ttl_changed' /** New TTL period */ readonly period: number @@ -280,6 +299,106 @@ export interface ActionTopicEdited { hidden?: boolean } +/** A non-standard action has happened in the chat */ +export interface ActionCustom { + readonly type: 'custom' + + /** Text to be shown in the interface */ + action: string +} + +/** Chat theme was changed */ +export interface ActionThemeChanged { + readonly type: 'theme_changed' + + /** Emoji representing the new theme */ + emoji: string +} + +/** Data was sent from a WebView (user-side action) */ +export interface ActionWebviewDataSent { + readonly type: 'webview_sent' + + /** Text of the button that was pressed to open the WebView */ + text: string +} + +/** Data was received from a WebView (bot-side action) */ +export interface ActionWebviewDataReceived { + readonly type: 'webview_received' + + /** Text of the button that was pressed to open the WebView */ + text: string + + /** Data received from the WebView */ + data: string +} + +/** Premium subscription was gifted */ +export interface ActionPremiumGifted { + readonly type: 'premium_gifted' + + /** + * Currency in which it was paid for. + * Three-letter ISO 4217 currency code) + */ + currency: string + + /** + * Price of the product in the smallest units of the currency + * (integer, not float/double). For example, for a price of + * `US$ 1.45`, `amount = 145` + */ + amount: number + + /** Duration of the gifted subscription in months */ + months: number + + /** If the subscription was bought with crypto, information about it */ + crypto?: { + /** Crypto currency name */ + currency: string + /** Price in the smallest units */ + amount: number + } +} + +/** A photo has been suggested as a profile photo */ +export interface ActionPhotoSuggested { + readonly type: 'photo_suggested' + + /** Photo that was suggested */ + photo: Photo +} + +/** A peer was chosen by the user after clicking on a RequestPeer button */ +export interface ActionPeerChosen { + readonly type: 'peer_chosen' + + /** ID of the button passed earlier by the bot */ + buttonId: number + + /** Marked ID of the chosen peer */ + peerId: number + + /** Input peer of the chosen peer */ + inputPeer?: tl.TypeInputPeer +} + +/** A wallpaper of the chathas been changed */ +export interface ActionWallpaperChanged { + readonly type: 'wallpaper_changed' + + /** + * Whether the user has applied the same wallpaper + * as the other party previously set in the chat + */ + same: boolean + + /** TL object representing the new wallpaper */ + wallpaper: tl.TypeWallPaper +} + export type MessageAction = | ActionChatCreated | ActionChannelCreated @@ -304,14 +423,28 @@ export type MessageAction = | ActionGeoProximity | ActionGroupCallStarted | ActionGroupCallEnded + | ActionGroupCallScheduled | ActionGroupInvite - | ActionSetTtl + | ActionTtlChanged | ActionTopicCreated | ActionTopicEdited + | ActionCustom + | ActionThemeChanged + | ActionUserJoinedApproved + | ActionWebviewDataSent + | ActionWebviewDataReceived + | ActionPremiumGifted + | ActionPhotoSuggested + | ActionPeerChosen + | ActionWallpaperChanged | null /** @internal */ export function _messageActionFromTl(this: Message, act: tl.TypeMessageAction): MessageAction { + // todo - passport + // messageActionSecureValuesSentMe#1b287353 values:Vector credentials:SecureCredentialsEncrypted + // messageActionSecureValuesSent#d95c6154 types:Vector + switch (act._) { case 'messageActionChatCreate': return { @@ -447,7 +580,12 @@ export function _messageActionFromTl(this: Message, act: tl.TypeMessageAction): type: 'group_call_started', call: act.call, } - + case 'messageActionGroupCallScheduled': + return { + type: 'group_call_scheduled', + call: act.call, + date: new Date(act.scheduleDate * 1000), + } case 'messageActionInviteToGroupCall': return { type: 'group_call_invite', @@ -456,7 +594,7 @@ export function _messageActionFromTl(this: Message, act: tl.TypeMessageAction): } case 'messageActionSetMessagesTTL': return { - type: 'set_ttl', + type: 'ttl_changed', period: act.period, } case 'messageActionTopicCreate': @@ -474,6 +612,63 @@ export function _messageActionFromTl(this: Message, act: tl.TypeMessageAction): closed: act.closed, hidden: act.hidden, } + case 'messageActionCustomAction': + return { + type: 'custom', + action: act.message, + } + case 'messageActionSetChatTheme': + return { + type: 'theme_changed', + emoji: act.emoticon, + } + case 'messageActionChatJoinedByRequest': + return { + type: 'user_joined_approved', + } + case 'messageActionWebViewDataSent': + return { + type: 'webview_sent', + text: act.text, + } + case 'messageActionWebViewDataSentMe': + return { + type: 'webview_received', + text: act.text, + data: act.data, + } + case 'messageActionGiftPremium': + return { + type: 'premium_gifted', + currency: act.currency, + amount: act.amount.toNumber(), + months: act.months, + crypto: act.cryptoAmount ? + { + currency: act.cryptoCurrency!, + amount: act.cryptoAmount.toNumber(), + } : + undefined, + } + case 'messageActionSuggestProfilePhoto': + return { + type: 'photo_suggested', + photo: new Photo(this.client, act.photo as tl.RawPhoto), + } + case 'messageActionRequestedPeer': + return { + type: 'peer_chosen', + buttonId: act.buttonId, + peerId: getMarkedPeerId(act.peer), + // todo - pass the peer itself? + } + case 'messageActionSetChatWallPaper': + case 'messageActionSetSameChatWallPaper': + return { + type: 'wallpaper_changed', + same: act._ === 'messageActionSetSameChatWallPaper', + wallpaper: act.wallpaper, + } default: return null } diff --git a/packages/client/src/types/messages/message-media.ts b/packages/client/src/types/messages/message-media.ts index a1acc7b9..fc97fcf2 100644 --- a/packages/client/src/types/messages/message-media.ts +++ b/packages/client/src/types/messages/message-media.ts @@ -53,7 +53,7 @@ export function _messageMediaFromTl( case 'messageMediaPhoto': if (!(m.photo?._ === 'photo')) return null - return new Photo(client, m.photo) + return new Photo(client, m.photo, m) case 'messageMediaDice': return new Dice(m) case 'messageMediaContact': @@ -61,7 +61,7 @@ export function _messageMediaFromTl( case 'messageMediaDocument': if (!(m.document?._ === 'document')) return null - return parseDocument(client, m.document) as MessageMedia + return parseDocument(client, m.document, m) as MessageMedia case 'messageMediaGeo': if (!(m.geo._ === 'geoPoint')) return null diff --git a/packages/client/src/types/messages/message.ts b/packages/client/src/types/messages/message.ts index 37844c3e..fc9f70c1 100644 --- a/packages/client/src/types/messages/message.ts +++ b/packages/client/src/types/messages/message.ts @@ -269,6 +269,23 @@ export class Message { return this._forward } + /** + * Whether the message is a channel post that was + * automatically forwarded to the connected discussion group + */ + get isAutomaticForward(): boolean { + if (this.raw._ === 'messageService' || !this.raw.fwdFrom) return false + + const fwd = this.raw.fwdFrom + + return Boolean( + this.chat.chatType === 'supergroup' && + fwd.channelPost && + fwd.savedFromMsgId && + fwd.savedFromPeer?._ === 'peerChannel', + ) + } + private _replies?: MessageRepliesInfo | MessageCommentsInfo /** * Information about comments (for channels) or replies (for groups) @@ -309,8 +326,8 @@ export class Message { } /** - * For replies, ID of the thread (i.e. ID of the top message - * in the thread) + * For replies, ID of the thread/topic + * (i.e. ID of the top message in the thread/topic) */ get replyToThreadId(): number | null { if (this.raw.replyTo?._ !== 'messageReplyHeader') return null @@ -327,6 +344,13 @@ export class Message { return this.raw.replyTo } + /** Whether this message is in a forum topic */ + get isTopicMessage(): boolean { + if (this.raw.replyTo?._ !== 'messageReplyHeader') return false + + return this.raw.replyTo.forumTopic! + } + /** * Whether this message contains mention of the current user */ diff --git a/packages/client/src/types/misc/sticker-set.ts b/packages/client/src/types/misc/sticker-set.ts index df3619e5..7ef4273c 100644 --- a/packages/client/src/types/misc/sticker-set.ts +++ b/packages/client/src/types/misc/sticker-set.ts @@ -8,6 +8,68 @@ import { InputFileLike } from '../files' import { MaskPosition, Sticker, StickerSourceType, StickerType, Thumbnail } from '../media' import { parseDocument } from '../media/document-utils' +/** + * Input sticker set. + * Can be one of: + * - Raw TL object + * - Sticker set short name + * - `{ dice: "" }` (e.g. `{ dice: "🎲" }`) - Used for fetching animated dice stickers + * - `{ system: string }` - for system stickersets: + * - `"animated"` - Animated emojis stickerset + * - `"animated_animations"` - Animated emoji reaction stickerset + * (contains animations to play when a user clicks on a given animated emoji) + * - `"premium_gifts"` - Stickers to show when receiving a gifted Telegram Premium subscription, + * - `"generic_animations"` - Generic animation stickerset containing animations to play + * when reacting to messages using a normal emoji without a custom animation + * - `"default_statuses"` - Default custom emoji status stickerset + * - `"default_topic_icons"` - Default custom emoji stickerset for forum topic icons + */ +export type InputStickerSet = + | tl.TypeInputStickerSet + | { dice: string } + | { + system: + | 'animated' + | 'animated_animations' + | 'premium_gifts' + | 'generic_animations' + | 'default_statuses' + | 'default_topic_icons' + } + | string + +export function normalizeInputStickerSet(input: InputStickerSet): tl.TypeInputStickerSet { + if (typeof input === 'string') { + return { + _: 'inputStickerSetShortName', + shortName: input, + } + } + if ('_' in input) return input + + if ('dice' in input) { + return { + _: 'inputStickerSetDice', + emoticon: input.dice, + } + } + + switch (input.system) { + case 'animated': + return { _: 'inputStickerSetAnimatedEmoji' } + case 'animated_animations': + return { _: 'inputStickerSetAnimatedEmojiAnimations' } + case 'premium_gifts': + return { _: 'inputStickerSetPremiumGifts' } + case 'generic_animations': + return { _: 'inputStickerSetEmojiGenericAnimations' } + case 'default_statuses': + return { _: 'inputStickerSetEmojiDefaultStatuses' } + case 'default_topic_icons': + return { _: 'inputStickerSetEmojiDefaultTopicIcons' } + } +} + /** * Information about one sticker inside the set */ diff --git a/packages/client/src/types/peers/chat.ts b/packages/client/src/types/peers/chat.ts index 30952834..7dff3b95 100644 --- a/packages/client/src/types/peers/chat.ts +++ b/packages/client/src/types/peers/chat.ts @@ -231,6 +231,15 @@ export class Chat { return (this.peer._ === 'channel' || this.peer._ === 'chat') && this.peer.noforwards! } + /** + * Whether this chat (user) has restricted sending them voice/video messages. + * + * Returned only in {@link TelegramClient.getFullChat} + */ + get hasBlockedVoices(): boolean { + return this.fullPeer?._ === 'userFull' && this.fullPeer.voiceMessagesForbidden! + } + /** * Title, for supergroups, channels and groups */