feat: feature parity with botapi 6.9

well mostly, and assuming i didn't miss anything. closes MTQ-72
This commit is contained in:
alina 🌸 2023-10-04 23:34:55 +03:00
parent e7dc8f0ec7
commit 2bde1c4f3e
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
22 changed files with 569 additions and 96 deletions

View file

@ -32,11 +32,13 @@ import { answerCallbackQuery } from './methods/bots/answer-callback-query'
import { answerInlineQuery } from './methods/bots/answer-inline-query' import { answerInlineQuery } from './methods/bots/answer-inline-query'
import { answerPreCheckoutQuery } from './methods/bots/answer-pre-checkout-query' import { answerPreCheckoutQuery } from './methods/bots/answer-pre-checkout-query'
import { deleteMyCommands } from './methods/bots/delete-my-commands' 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 { getBotMenuButton } from './methods/bots/get-bot-menu-button'
import { getCallbackAnswer } from './methods/bots/get-callback-answer' import { getCallbackAnswer } from './methods/bots/get-callback-answer'
import { getGameHighScores, getInlineGameHighScores } from './methods/bots/get-game-high-scores' import { getGameHighScores, getInlineGameHighScores } from './methods/bots/get-game-high-scores'
import { getMyCommands } from './methods/bots/get-my-commands' import { getMyCommands } from './methods/bots/get-my-commands'
import { _normalizeCommandScope } from './methods/bots/normalize-command-scope' 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 { setBotMenuButton } from './methods/bots/set-bot-menu-button'
import { setGameScore, setInlineGameScore } from './methods/bots/set-game-score' import { setGameScore, setInlineGameScore } from './methods/bots/set-game-score'
import { setMyCommands } from './methods/bots/set-my-commands' 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 { getInstalledStickers } from './methods/stickers/get-installed-stickers'
import { getStickerSet } from './methods/stickers/get-sticker-set' import { getStickerSet } from './methods/stickers/get-sticker-set'
import { moveStickerInSet } from './methods/stickers/move-sticker-in-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 { setStickerSetThumb } from './methods/stickers/set-sticker-set-thumb'
import { applyBoost } from './methods/stories/apply-boost' import { applyBoost } from './methods/stories/apply-boost'
import { canApplyBoost, CanApplyBoostResult } from './methods/stories/can-apply-boost' import { canApplyBoost, CanApplyBoostResult } from './methods/stories/can-apply-boost'
@ -285,6 +288,7 @@ import {
InputPeerLike, InputPeerLike,
InputPrivacyRule, InputPrivacyRule,
InputReaction, InputReaction,
InputStickerSet,
InputStickerSetItem, InputStickerSetItem,
MaybeDynamic, MaybeDynamic,
Message, Message,
@ -916,6 +920,23 @@ export interface TelegramClient extends BaseTelegramClient {
*/ */
langCode?: string langCode?: string
}): Promise<void> }): Promise<void>
/**
* 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<tl.bots.RawBotInfo>
/** /**
* Fetches the menu button set for the given user. * Fetches the menu button set for the given user.
* *
@ -999,6 +1020,32 @@ export interface TelegramClient extends BaseTelegramClient {
_normalizeCommandScope( _normalizeCommandScope(
scope: tl.TypeBotCommandScope | BotCommands.IntermediateScope, scope: tl.TypeBotCommandScope | BotCommands.IntermediateScope,
): Promise<tl.TypeBotCommandScope> ): Promise<tl.TypeBotCommandScope>
/**
* 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<void>
/** /**
* Sets a menu button for the given user. * Sets a menu button for the given user.
* *
@ -1107,15 +1154,18 @@ export interface TelegramClient extends BaseTelegramClient {
*/ */
archiveChats(chats: MaybeArray<InputPeerLike>): Promise<void> archiveChats(chats: MaybeArray<InputPeerLike>): Promise<void>
/** /**
* 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, * 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 chatId Chat ID
* @param userId User ID * @param peerId User/Channel ID
* @returns Service message about removed user, if one was generated. * @returns Service message about removed user, if one was generated.
*/ */
banChatMember(chatId: InputPeerLike, userId: InputPeerLike): Promise<Message | null> banChatMember(chatId: InputPeerLike, peerId: InputPeerLike): Promise<Message | null>
/** /**
* Create a new broadcast channel * Create a new broadcast channel
* *
@ -1666,7 +1716,7 @@ export interface TelegramClient extends BaseTelegramClient {
unarchiveChats(chats: MaybeArray<InputPeerLike>): Promise<void> unarchiveChats(chats: MaybeArray<InputPeerLike>): Promise<void>
/** /**
* 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. * or remove any restrictions that they have.
* Unbanning does not add the user back to the chat, this * Unbanning does not add the user back to the chat, this
* just allows the user to join the chat again, if they want. * 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. * This method acts as a no-op in case a legacy group is passed.
* *
* @param chatId Chat ID * @param chatId Chat ID
* @param userId User ID * @param peerId User/channel ID
*/ */
unbanChatMember(chatId: InputPeerLike, userId: InputPeerLike): Promise<void> unbanChatMember(chatId: InputPeerLike, peerId: InputPeerLike): Promise<void>
/** /**
* 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. * or remove any restrictions that they have.
* Unbanning does not add the user back to the chat, this * Unbanning does not add the user back to the chat, this
* just allows the user to join the chat again, if they want. * 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. * This method acts as a no-op in case a legacy group is passed.
* *
* @param chatId Chat ID * @param chatId Chat ID
* @param userId User ID * @param peerId User/channel ID
*/ */
unrestrictChatMember(chatId: InputPeerLike, userId: InputPeerLike): Promise<void> unrestrictChatMember(chatId: InputPeerLike, peerId: InputPeerLike): Promise<void>
/** /**
* Add an existing Telegram user as a contact * Add an existing Telegram user as a contact
* *
@ -3982,7 +4032,7 @@ export interface TelegramClient extends BaseTelegramClient {
* @returns Modfiied sticker set * @returns Modfiied sticker set
*/ */
addStickerToSet( addStickerToSet(
id: string | tl.TypeInputStickerSet, id: InputStickerSet,
sticker: InputStickerSetItem, sticker: InputStickerSetItem,
params?: { 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 * @param id Sticker pack short name, dice emoji, `"emoji"` for animated emojis or input ID
*/ */
getStickerSet(id: string | { dice: string } | tl.TypeInputStickerSet): Promise<StickerSet> getStickerSet(id: InputStickerSet): Promise<StickerSet>
/** /**
* Move a sticker in a sticker set * Move a sticker in a sticker set
* to another position * to another position
@ -4120,6 +4170,15 @@ export interface TelegramClient extends BaseTelegramClient {
sticker: string | tdFileId.RawFullRemoteFileLocation | tl.TypeInputDocument, sticker: string | tdFileId.RawFullRemoteFileLocation | tl.TypeInputDocument,
position: number, position: number,
): Promise<StickerSet> ): Promise<StickerSet>
/**
* 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<void>
/** /**
* Set sticker set thumbnail * Set sticker set thumbnail
* *
@ -4129,7 +4188,7 @@ export interface TelegramClient extends BaseTelegramClient {
* @returns Modified sticker set * @returns Modified sticker set
*/ */
setStickerSetThumb( setStickerSetThumb(
id: string | tl.TypeInputStickerSet, id: InputStickerSet,
thumb: InputFileLike | tl.TypeInputDocument, thumb: InputFileLike | tl.TypeInputDocument,
params?: { params?: {
/** /**
@ -5056,12 +5115,14 @@ export class TelegramClient extends BaseTelegramClient {
answerInlineQuery = answerInlineQuery answerInlineQuery = answerInlineQuery
answerPreCheckoutQuery = answerPreCheckoutQuery answerPreCheckoutQuery = answerPreCheckoutQuery
deleteMyCommands = deleteMyCommands deleteMyCommands = deleteMyCommands
getBotInfo = getBotInfo
getBotMenuButton = getBotMenuButton getBotMenuButton = getBotMenuButton
getCallbackAnswer = getCallbackAnswer getCallbackAnswer = getCallbackAnswer
getGameHighScores = getGameHighScores getGameHighScores = getGameHighScores
getInlineGameHighScores = getInlineGameHighScores getInlineGameHighScores = getInlineGameHighScores
getMyCommands = getMyCommands getMyCommands = getMyCommands
_normalizeCommandScope = _normalizeCommandScope _normalizeCommandScope = _normalizeCommandScope
setBotInfo = setBotInfo
setBotMenuButton = setBotMenuButton setBotMenuButton = setBotMenuButton
setGameScore = setGameScore setGameScore = setGameScore
setInlineGameScore = setInlineGameScore setInlineGameScore = setInlineGameScore
@ -5213,6 +5274,7 @@ export class TelegramClient extends BaseTelegramClient {
getInstalledStickers = getInstalledStickers getInstalledStickers = getInstalledStickers
getStickerSet = getStickerSet getStickerSet = getStickerSet
moveStickerInSet = moveStickerInSet moveStickerInSet = moveStickerInSet
setChatStickerSet = setChatStickerSet
setStickerSetThumb = setStickerSetThumb setStickerSetThumb = setStickerSetThumb
applyBoost = applyBoost applyBoost = applyBoost
canApplyBoost = canApplyBoost canApplyBoost = canApplyBoost

View file

@ -48,6 +48,7 @@ import {
InputPeerLike, InputPeerLike,
InputPrivacyRule, InputPrivacyRule,
InputReaction, InputReaction,
InputStickerSet,
InputStickerSetItem, InputStickerSetItem,
MaybeDynamic, MaybeDynamic,
Message, Message,

View file

@ -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<tl.bots.RawBotInfo> {
const { bot, langCode = '' } = params
return this.call({
_: 'bots.getBotInfo',
bot: bot ? normalizeToInputUser(await this.resolvePeer(bot), bot) : undefined,
langCode: langCode,
})
}

View file

@ -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<void> {
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,
})
}

View file

@ -10,29 +10,32 @@ import {
} from '../../utils/peer-utils' } 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, * 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 chatId Chat ID
* @param userId User ID * @param peerId User/Channel ID
* @returns Service message about removed user, if one was generated. * @returns Service message about removed user, if one was generated.
* @internal * @internal
*/ */
export async function banChatMember( export async function banChatMember(
this: TelegramClient, this: TelegramClient,
chatId: InputPeerLike, chatId: InputPeerLike,
userId: InputPeerLike, peerId: InputPeerLike,
): Promise<Message | null> { ): Promise<Message | null> {
const chat = await this.resolvePeer(chatId) const chat = await this.resolvePeer(chatId)
const user = await this.resolvePeer(userId) const peer = await this.resolvePeer(peerId)
let res let res
if (isInputPeerChannel(chat)) { if (isInputPeerChannel(chat)) {
res = await this.call({ res = await this.call({
_: 'channels.editBanned', _: 'channels.editBanned',
channel: normalizeToInputChannel(chat), channel: normalizeToInputChannel(chat),
participant: user, participant: peer,
bannedRights: { bannedRights: {
_: 'chatBannedRights', _: 'chatBannedRights',
// bans can't be temporary. // bans can't be temporary.
@ -44,7 +47,7 @@ export async function banChatMember(
res = await this.call({ res = await this.call({
_: 'messages.deleteChatUser', _: 'messages.deleteChatUser',
chatId: chat.chatId, chatId: chat.chatId,
userId: normalizeToInputUser(user), userId: normalizeToInputUser(peer),
}) })
} else throw new MtInvalidPeerTypeError(chatId, 'chat or channel') } else throw new MtInvalidPeerTypeError(chatId, 'chat or channel')

View file

@ -4,7 +4,7 @@ import { isInputPeerChannel, isInputPeerChat, normalizeToInputChannel } from '..
// @alias=unrestrictChatMember // @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. * or remove any restrictions that they have.
* Unbanning does not add the user back to the chat, this * Unbanning does not add the user back to the chat, this
* just allows the user to join the chat again, if they want. * 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. * This method acts as a no-op in case a legacy group is passed.
* *
* @param chatId Chat ID * @param chatId Chat ID
* @param userId User ID * @param peerId User/channel ID
* @internal * @internal
*/ */
export async function unbanChatMember( export async function unbanChatMember(
this: TelegramClient, this: TelegramClient,
chatId: InputPeerLike, chatId: InputPeerLike,
userId: InputPeerLike, peerId: InputPeerLike,
): Promise<void> { ): Promise<void> {
const chat = await this.resolvePeer(chatId) const chat = await this.resolvePeer(chatId)
const user = await this.resolvePeer(userId) const peer = await this.resolvePeer(peerId)
if (isInputPeerChannel(chat)) { if (isInputPeerChannel(chat)) {
const res = await this.call({ const res = await this.call({
_: 'channels.editBanned', _: 'channels.editBanned',
channel: normalizeToInputChannel(chat), channel: normalizeToInputChannel(chat),
participant: user, participant: peer,
bannedRights: { bannedRights: {
_: 'chatBannedRights', _: 'chatBannedRights',
untilDate: 0, untilDate: 0,

View file

@ -248,6 +248,7 @@ export async function _normalizeInputMedia(
fileReference: res.photo.fileReference, fileReference: res.photo.fileReference,
}, },
ttlSeconds: media.ttlSeconds, ttlSeconds: media.ttlSeconds,
spoiler: media.type === 'video' && media.spoiler,
} }
} }
assertTypeIs('normalizeInputMedia (@ messages.uploadMedia)', res, 'messageMediaDocument') assertTypeIs('normalizeInputMedia (@ messages.uploadMedia)', res, 'messageMediaDocument')
@ -262,6 +263,7 @@ export async function _normalizeInputMedia(
fileReference: res.document.fileReference, fileReference: res.document.fileReference,
}, },
ttlSeconds: media.ttlSeconds, ttlSeconds: media.ttlSeconds,
spoiler: media.type === 'video' && media.spoiler,
} }
} }
@ -323,6 +325,7 @@ export async function _normalizeInputMedia(
_: 'inputMediaUploadedPhoto', _: 'inputMediaUploadedPhoto',
file: inputFile, file: inputFile,
ttlSeconds: media.ttlSeconds, ttlSeconds: media.ttlSeconds,
spoiler: media.spoiler,
}, },
true, true,
) )
@ -387,6 +390,7 @@ export async function _normalizeInputMedia(
mimeType: mime, mimeType: mime,
attributes, attributes,
ttlSeconds: media.ttlSeconds, ttlSeconds: media.ttlSeconds,
spoiler: media.type === 'video' && media.spoiler,
}, },
false, false,
) )

View file

@ -70,7 +70,7 @@ export async function uploadMedia(
assertTypeIs('uploadMedia', res, 'messageMediaPhoto') assertTypeIs('uploadMedia', res, 'messageMediaPhoto')
assertTypeIs('uploadMedia', res.photo!, 'photo') assertTypeIs('uploadMedia', res.photo!, 'photo')
return new Photo(this, res.photo) return new Photo(this, res.photo, res)
case 'inputMediaUploadedDocument': case 'inputMediaUploadedDocument':
case 'inputMediaDocument': case 'inputMediaDocument':
case 'inputMediaDocumentExternal': case 'inputMediaDocumentExternal':
@ -78,7 +78,7 @@ export async function uploadMedia(
assertTypeIs('uploadMedia', res.document!, 'document') assertTypeIs('uploadMedia', res.document!, 'document')
// eslint-disable-next-line // eslint-disable-next-line
return parseDocument(this, res.document) as any return parseDocument(this, res.document, res) as any
case 'inputMediaStory': case 'inputMediaStory':
throw new MtArgumentError("This media (story) can't be uploaded") throw new MtArgumentError("This media (story) can't be uploaded")
default: default:

View file

@ -1,7 +1,5 @@
import { tl } from '@mtcute/core'
import { TelegramClient } from '../../client' import { TelegramClient } from '../../client'
import { InputStickerSetItem, StickerSet } from '../../types' import { InputStickerSet, InputStickerSetItem, normalizeInputStickerSet, StickerSet } from '../../types'
const MASK_POS = { const MASK_POS = {
forehead: 0, forehead: 0,
@ -24,7 +22,7 @@ const MASK_POS = {
*/ */
export async function addStickerToSet( export async function addStickerToSet(
this: TelegramClient, this: TelegramClient,
id: string | tl.TypeInputStickerSet, id: InputStickerSet,
sticker: InputStickerSetItem, sticker: InputStickerSetItem,
params?: { params?: {
/** /**
@ -36,16 +34,9 @@ export async function addStickerToSet(
progressCallback?: (uploaded: number, total: number) => void progressCallback?: (uploaded: number, total: number) => void
}, },
): Promise<StickerSet> { ): Promise<StickerSet> {
if (typeof id === 'string') {
id = {
_: 'inputStickerSetShortName',
shortName: id,
}
}
const res = await this.call({ const res = await this.call({
_: 'stickers.addStickerToSet', _: 'stickers.addStickerToSet',
stickerset: id, stickerset: normalizeInputStickerSet(id),
sticker: { sticker: {
_: 'inputStickerSetItem', _: 'inputStickerSetItem',
document: await this._normalizeFileToDocument(sticker.file, params ?? {}), document: await this._normalizeFileToDocument(sticker.file, params ?? {}),

View file

@ -1,7 +1,5 @@
import { tl } from '@mtcute/core'
import { TelegramClient } from '../../client' import { TelegramClient } from '../../client'
import { StickerSet } from '../../types' import { InputStickerSet, normalizeInputStickerSet, StickerSet } from '../../types'
/** /**
* Get a sticker pack and stickers inside of it. * 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 * @param id Sticker pack short name, dice emoji, `"emoji"` for animated emojis or input ID
* @internal * @internal
*/ */
export async function getStickerSet( export async function getStickerSet(this: TelegramClient, id: InputStickerSet): Promise<StickerSet> {
this: TelegramClient,
id: string | { dice: string } | tl.TypeInputStickerSet,
): Promise<StickerSet> {
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
}
const res = await this.call({ const res = await this.call({
_: 'messages.getStickerSet', _: 'messages.getStickerSet',
stickerset: input, stickerset: normalizeInputStickerSet(id),
hash: 0, hash: 0,
}) })

View file

@ -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<void> {
await this.call({
_: 'channels.setStickers',
channel: normalizeToInputChannel(await this.resolvePeer(chatId), chatId),
stickerset: normalizeInputStickerSet(id),
})
}

View file

@ -1,7 +1,7 @@
import { tl } from '@mtcute/core' import { tl } from '@mtcute/core'
import { TelegramClient } from '../../client' import { TelegramClient } from '../../client'
import { InputFileLike, StickerSet } from '../../types' import { InputFileLike, InputStickerSet, normalizeInputStickerSet, StickerSet } from '../../types'
/** /**
* Set sticker set thumbnail * Set sticker set thumbnail
@ -14,7 +14,7 @@ import { InputFileLike, StickerSet } from '../../types'
*/ */
export async function setStickerSetThumb( export async function setStickerSetThumb(
this: TelegramClient, this: TelegramClient,
id: string | tl.TypeInputStickerSet, id: InputStickerSet,
thumb: InputFileLike | tl.TypeInputDocument, thumb: InputFileLike | tl.TypeInputDocument,
params?: { params?: {
/** /**
@ -26,16 +26,9 @@ export async function setStickerSetThumb(
progressCallback?: (uploaded: number, total: number) => void progressCallback?: (uploaded: number, total: number) => void
}, },
): Promise<StickerSet> { ): Promise<StickerSet> {
if (typeof id === 'string') {
id = {
_: 'inputStickerSetShortName',
shortName: id,
}
}
const res = await this.call({ const res = await this.call({
_: 'stickers.setStickerSetThumb', _: 'stickers.setStickerSetThumb',
stickerset: id, stickerset: normalizeInputStickerSet(id),
thumb: await this._normalizeFileToDocument(thumb, params ?? {}), thumb: await this._normalizeFileToDocument(thumb, params ?? {}),
}) })

View file

@ -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 * Find a button in the keyboard by its text or by predicate
* *

View file

@ -10,7 +10,11 @@ import { Voice } from './voice'
export type ParsedDocument = Sticker | Voice | Audio | Video | Document export type ParsedDocument = Sticker | Voice | Audio | Video | Document
/** @internal */ /** @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( const stickerAttr = doc.attributes.find(
(a) => a._ === 'documentAttributeSticker' || a._ === 'documentAttributeCustomEmoji', (a) => a._ === 'documentAttributeSticker' || a._ === 'documentAttributeCustomEmoji',
) )
@ -38,12 +42,12 @@ export function parseDocument(client: TelegramClient, doc: tl.RawDocument): Pars
return new Audio(client, doc, attr) return new Audio(client, doc, attr)
case 'documentAttributeVideo': case 'documentAttributeVideo':
return new Video(client, doc, attr) return new Video(client, doc, attr, media)
case 'documentAttributeImageSize': case 'documentAttributeImageSize':
// legacy gif // legacy gif
if (doc.mimeType === 'image/gif') { if (doc.mimeType === 'image/gif') {
return new Video(client, doc, attr) return new Video(client, doc, attr, media)
} }
} }
} }

View file

@ -151,6 +151,11 @@ export interface InputMediaDocument extends FileMixin, CaptionMixin {
*/ */
export interface InputMediaPhoto extends FileMixin, CaptionMixin { export interface InputMediaPhoto extends FileMixin, CaptionMixin {
type: 'photo' 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. * Only applicable to newly uploaded files.
*/ */
isRound?: boolean isRound?: boolean
/**
* Whether this video should be hidden with a spoiler
*/
spoiler?: boolean
} }
/** /**

View file

@ -11,9 +11,6 @@ import { Thumbnail } from './thumbnail'
export class Photo extends FileLocation { export class Photo extends FileLocation {
readonly type: 'photo' readonly type: 'photo'
/** Raw TL object */
readonly raw: tl.RawPhoto
/** Biggest available photo width */ /** Biggest available photo width */
readonly width: number readonly width: number
@ -22,7 +19,7 @@ export class Photo extends FileLocation {
private _bestSize?: tl.RawPhotoSize | tl.RawPhotoSizeProgressive 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 = { const location = {
_: 'inputPhotoFileLocation', _: 'inputPhotoFileLocation',
id: raw.id, id: raw.id,
@ -71,7 +68,6 @@ export class Photo extends FileLocation {
super(client, location, size, raw.dcId) super(client, location, size, raw.dcId)
this._bestSize = bestSize this._bestSize = bestSize
this.raw = raw
this.width = width this.width = width
this.height = height this.height = height
this.type = 'photo' 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[] private _thumbnails?: Thumbnail[]
/** /**
* Available thumbnails. * Available thumbnails.

View file

@ -24,6 +24,7 @@ export class Video extends RawDocument {
client: TelegramClient, client: TelegramClient,
doc: tl.RawDocument, doc: tl.RawDocument,
readonly attr: tl.RawDocumentAttributeVideo | tl.RawDocumentAttributeImageSize, readonly attr: tl.RawDocumentAttributeVideo | tl.RawDocumentAttributeImageSize,
readonly media?: tl.RawMessageMediaDocument,
) { ) {
super(client, doc) super(client, doc)
} }
@ -75,6 +76,16 @@ export class Video extends RawDocument {
get isLegacyGif(): boolean { get isLegacyGif(): boolean {
return this.attr._ === 'documentAttributeImageSize' 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']) makeInspectable(Video, ['fileSize', 'dcId'], ['inputMedia', 'inputDocument'])

View file

@ -1,4 +1,4 @@
import { tl } from '@mtcute/core' import { getMarkedPeerId, tl } from '@mtcute/core'
import { _callDiscardReasonFromTl, CallDiscardReason } from '../calls' import { _callDiscardReasonFromTl, CallDiscardReason } from '../calls'
import { Photo } from '../media' import { Photo } from '../media'
@ -122,6 +122,14 @@ export interface ActionUserJoinedLink {
readonly inviter: number 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) */ /** A payment was received from a user (bot) */
export interface ActionPaymentReceived { export interface ActionPaymentReceived {
readonly type: 'payment_received' readonly type: 'payment_received'
@ -230,6 +238,17 @@ export interface ActionGroupCallEnded {
readonly duration: number 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 */ /** Group call has ended */
export interface ActionGroupInvite { export interface ActionGroupInvite {
readonly type: 'group_call_invite' readonly type: 'group_call_invite'
@ -242,8 +261,8 @@ export interface ActionGroupInvite {
} }
/** Messages TTL changed */ /** Messages TTL changed */
export interface ActionSetTtl { export interface ActionTtlChanged {
readonly type: 'set_ttl' readonly type: 'ttl_changed'
/** New TTL period */ /** New TTL period */
readonly period: number readonly period: number
@ -280,6 +299,106 @@ export interface ActionTopicEdited {
hidden?: boolean 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 = export type MessageAction =
| ActionChatCreated | ActionChatCreated
| ActionChannelCreated | ActionChannelCreated
@ -304,14 +423,28 @@ export type MessageAction =
| ActionGeoProximity | ActionGeoProximity
| ActionGroupCallStarted | ActionGroupCallStarted
| ActionGroupCallEnded | ActionGroupCallEnded
| ActionGroupCallScheduled
| ActionGroupInvite | ActionGroupInvite
| ActionSetTtl | ActionTtlChanged
| ActionTopicCreated | ActionTopicCreated
| ActionTopicEdited | ActionTopicEdited
| ActionCustom
| ActionThemeChanged
| ActionUserJoinedApproved
| ActionWebviewDataSent
| ActionWebviewDataReceived
| ActionPremiumGifted
| ActionPhotoSuggested
| ActionPeerChosen
| ActionWallpaperChanged
| null | null
/** @internal */ /** @internal */
export function _messageActionFromTl(this: Message, act: tl.TypeMessageAction): MessageAction { export function _messageActionFromTl(this: Message, act: tl.TypeMessageAction): MessageAction {
// todo - passport
// messageActionSecureValuesSentMe#1b287353 values:Vector<SecureValue> credentials:SecureCredentialsEncrypted
// messageActionSecureValuesSent#d95c6154 types:Vector<SecureValueType>
switch (act._) { switch (act._) {
case 'messageActionChatCreate': case 'messageActionChatCreate':
return { return {
@ -447,7 +580,12 @@ export function _messageActionFromTl(this: Message, act: tl.TypeMessageAction):
type: 'group_call_started', type: 'group_call_started',
call: act.call, call: act.call,
} }
case 'messageActionGroupCallScheduled':
return {
type: 'group_call_scheduled',
call: act.call,
date: new Date(act.scheduleDate * 1000),
}
case 'messageActionInviteToGroupCall': case 'messageActionInviteToGroupCall':
return { return {
type: 'group_call_invite', type: 'group_call_invite',
@ -456,7 +594,7 @@ export function _messageActionFromTl(this: Message, act: tl.TypeMessageAction):
} }
case 'messageActionSetMessagesTTL': case 'messageActionSetMessagesTTL':
return { return {
type: 'set_ttl', type: 'ttl_changed',
period: act.period, period: act.period,
} }
case 'messageActionTopicCreate': case 'messageActionTopicCreate':
@ -474,6 +612,63 @@ export function _messageActionFromTl(this: Message, act: tl.TypeMessageAction):
closed: act.closed, closed: act.closed,
hidden: act.hidden, 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: default:
return null return null
} }

View file

@ -53,7 +53,7 @@ export function _messageMediaFromTl(
case 'messageMediaPhoto': case 'messageMediaPhoto':
if (!(m.photo?._ === 'photo')) return null if (!(m.photo?._ === 'photo')) return null
return new Photo(client, m.photo) return new Photo(client, m.photo, m)
case 'messageMediaDice': case 'messageMediaDice':
return new Dice(m) return new Dice(m)
case 'messageMediaContact': case 'messageMediaContact':
@ -61,7 +61,7 @@ export function _messageMediaFromTl(
case 'messageMediaDocument': case 'messageMediaDocument':
if (!(m.document?._ === 'document')) return null if (!(m.document?._ === 'document')) return null
return parseDocument(client, m.document) as MessageMedia return parseDocument(client, m.document, m) as MessageMedia
case 'messageMediaGeo': case 'messageMediaGeo':
if (!(m.geo._ === 'geoPoint')) return null if (!(m.geo._ === 'geoPoint')) return null

View file

@ -269,6 +269,23 @@ export class Message {
return this._forward 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 private _replies?: MessageRepliesInfo | MessageCommentsInfo
/** /**
* Information about comments (for channels) or replies (for groups) * 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 * For replies, ID of the thread/topic
* in the thread) * (i.e. ID of the top message in the thread/topic)
*/ */
get replyToThreadId(): number | null { get replyToThreadId(): number | null {
if (this.raw.replyTo?._ !== 'messageReplyHeader') return null if (this.raw.replyTo?._ !== 'messageReplyHeader') return null
@ -327,6 +344,13 @@ export class Message {
return this.raw.replyTo 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 * Whether this message contains mention of the current user
*/ */

View file

@ -8,6 +8,68 @@ import { InputFileLike } from '../files'
import { MaskPosition, Sticker, StickerSourceType, StickerType, Thumbnail } from '../media' import { MaskPosition, Sticker, StickerSourceType, StickerType, Thumbnail } from '../media'
import { parseDocument } from '../media/document-utils' import { parseDocument } from '../media/document-utils'
/**
* Input sticker set.
* Can be one of:
* - Raw TL object
* - Sticker set short name
* - `{ dice: "<emoji>" }` (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 * Information about one sticker inside the set
*/ */

View file

@ -231,6 +231,15 @@ export class Chat {
return (this.peer._ === 'channel' || this.peer._ === 'chat') && this.peer.noforwards! 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 * Title, for supergroups, channels and groups
*/ */