feat: feature parity with botapi 6.9
well mostly, and assuming i didn't miss anything. closes MTQ-72
This commit is contained in:
parent
e7dc8f0ec7
commit
2bde1c4f3e
22 changed files with 569 additions and 96 deletions
|
@ -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
|
||||
|
|
|
@ -48,6 +48,7 @@ import {
|
|||
InputPeerLike,
|
||||
InputPrivacyRule,
|
||||
InputReaction,
|
||||
InputStickerSet,
|
||||
InputStickerSetItem,
|
||||
MaybeDynamic,
|
||||
Message,
|
||||
|
|
35
packages/client/src/methods/bots/get-bot-info.ts
Normal file
35
packages/client/src/methods/bots/get-bot-info.ts
Normal 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,
|
||||
})
|
||||
}
|
45
packages/client/src/methods/bots/set-bot-info.ts
Normal file
45
packages/client/src/methods/bots/set-bot-info.ts
Normal 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,
|
||||
})
|
||||
}
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 ?? {}),
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
24
packages/client/src/methods/stickers/set-chat-sticker-set.ts
Normal file
24
packages/client/src/methods/stickers/set-chat-sticker-set.ts
Normal 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),
|
||||
})
|
||||
}
|
|
@ -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 ?? {}),
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
Loading…
Reference in a new issue