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 { 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<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.
*
@ -999,6 +1020,32 @@ export interface TelegramClient extends BaseTelegramClient {
_normalizeCommandScope(
scope: tl.TypeBotCommandScope | BotCommands.IntermediateScope,
): 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.
*
@ -1107,15 +1154,18 @@ export interface TelegramClient extends BaseTelegramClient {
*/
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,
* 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<Message | null>
banChatMember(chatId: InputPeerLike, peerId: InputPeerLike): Promise<Message | null>
/**
* Create a new broadcast channel
*
@ -1666,7 +1716,7 @@ export interface TelegramClient extends BaseTelegramClient {
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.
* 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<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.
* 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<void>
unrestrictChatMember(chatId: InputPeerLike, peerId: InputPeerLike): Promise<void>
/**
* 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<StickerSet>
getStickerSet(id: InputStickerSet): Promise<StickerSet>
/**
* 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<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
*
@ -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

View file

@ -48,6 +48,7 @@ import {
InputPeerLike,
InputPrivacyRule,
InputReaction,
InputStickerSet,
InputStickerSetItem,
MaybeDynamic,
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'
/**
* 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<Message | null> {
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')

View file

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

View file

@ -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,
)

View file

@ -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:

View file

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

View file

@ -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<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
}
export async function getStickerSet(this: TelegramClient, id: InputStickerSet): Promise<StickerSet> {
const res = await this.call({
_: 'messages.getStickerSet',
stickerset: input,
stickerset: normalizeInputStickerSet(id),
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 { 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<StickerSet> {
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 ?? {}),
})

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
*

View file

@ -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)
}
}
}

View file

@ -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
}
/**

View file

@ -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.

View file

@ -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'])

View file

@ -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<SecureValue> credentials:SecureCredentialsEncrypted
// messageActionSecureValuesSent#d95c6154 types:Vector<SecureValueType>
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
}

View file

@ -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

View file

@ -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
*/

View file

@ -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: "<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
*/

View file

@ -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
*/