feat(client): sticker set methods for bots, and overall better sticker support

This commit is contained in:
teidesu 2021-05-05 23:26:28 +03:00
parent 5ea2ed67d7
commit bbb8b20420
10 changed files with 611 additions and 7 deletions

View file

@ -56,6 +56,7 @@ import { downloadAsBuffer } from './methods/files/download-buffer'
import { downloadToFile } from './methods/files/download-file'
import { downloadAsIterable } from './methods/files/download-iterable'
import { downloadAsStream } from './methods/files/download-stream'
import { _normalizeFileToDocument } from './methods/files/normalize-file-to-document'
import { _normalizeInputFile } from './methods/files/normalize-input-file'
import { _normalizeInputMedia } from './methods/files/normalize-input-media'
import { uploadFile } from './methods/files/upload-file'
@ -84,8 +85,12 @@ import {
setDefaultParseMode,
unregisterParseMode,
} from './methods/parse-modes/parse-modes'
import { addStickerToSet } from './methods/stickers/add-sticker-to-set'
import { createStickerSet } from './methods/stickers/create-sticker-set'
import { deleteStickerFromSet } from './methods/stickers/delete-sticker-from-set'
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 {
_fetchUpdatesState,
_handleUpdate,
@ -112,6 +117,7 @@ import {
InputInlineResult,
InputMediaLike,
InputPeerLike,
InputStickerSetItem,
MaybeDynamic,
Message,
PartialExcept,
@ -127,6 +133,7 @@ import {
} from './types'
import { MaybeArray, MaybeAsync, TelegramConnection } from '@mtcute/core'
import { Lock } from './utils/lock'
import { tdFileId } from '@mtcute/file-id'
export interface TelegramClient extends BaseTelegramClient {
/**
@ -1973,6 +1980,116 @@ export interface TelegramClient extends BaseTelegramClient {
* @throws MtCuteError When given parse mode is not registered.
*/
setDefaultParseMode(name: string): void
/**
* Add a sticker to a sticker set.
*
* Only for bots, and the sticker set must
* have been created by this bot.
*
* @param id Sticker set short name or TL object with input sticker set
* @param sticker Sticker to be added
* @param params
* @returns Modfiied sticker set
*/
addStickerToSet(
id: string | tl.TypeInputStickerSet,
sticker: InputStickerSetItem,
params?: {
/**
* Upload progress callback
*
* @param uploaded Number of bytes uploaded
* @param total Total file size
*/
progressCallback?: (uploaded: number, total: number) => void
}
): Promise<StickerSet>
/**
* Create a new sticker set (only for bots)
*
* Only for bots.
*
* @param params
* @returns Newly created sticker set
*/
createStickerSet(params: {
/**
* Owner of the sticker set (must be user)
*/
owner: InputPeerLike
/**
* Title of the sticker set (1-64 chars)
*/
title: string
/**
* Short name of the sticker set.
* Can only contain English letters, digits and underscores
* (i.e. must match `/^[a-zA-Z0-9_]+$/),
* and must end with `_by_<bot username>` (`<bot username>` is
* case-insensitive).
*/
shortName: string
/**
* Whether this is a set of masks
*/
masks?: boolean
/**
* Whether this is a set of animated stickers
*/
animated?: boolean
/**
* List of stickers to be immediately added into the pack.
* There must be at least one sticker in this list.
*/
stickers: InputStickerSetItem[]
/**
* Thumbnail for the set.
*
* The file must be either a `.png` file
* up to 128kb, having size of exactly `100x100` px,
* or a `.tgs` file up to 32kb.
*
* If not set, Telegram will use the first sticker
* in the sticker set as the thumbnail
*/
thumb?: InputFileLike
/**
* Upload progress callback.
*
* @param idx Index of the sticker
* @param uploaded Number of bytes uploaded
* @param total Total file size
*/
progressCallback?: (
idx: number,
uploaded: number,
total: number
) => void
}): Promise<StickerSet>
/**
* Delete a sticker from a sticker set
*
* Only for bots, and the sticker set must
* have been created by this bot.
*
* @param sticker
* TDLib and Bot API compatible File ID, or a
* TL object representing a sticker to be removed
* @returns Modfiied sticker set
*/
deleteStickerFromSet(
sticker:
| string
| tdFileId.RawFullRemoteFileLocation
| tl.TypeInputDocument
): Promise<StickerSet>
/**
* Get a list of all installed sticker packs
*
@ -1991,6 +2108,26 @@ export interface TelegramClient extends BaseTelegramClient {
getStickerSet(
id: string | { dice: string } | tl.TypeInputStickerSet
): Promise<StickerSet>
/**
* Move a sticker in a sticker set
* to another position
*
* Only for bots, and the sticker set must
* have been created by this bot.
*
* @param sticker
* TDLib and Bot API compatible File ID, or a
* TL object representing a sticker to be removed
* @param position New sticker position (starting from 0)
* @returns Modfiied sticker set
*/
moveStickerInSet(
sticker:
| string
| tdFileId.RawFullRemoteFileLocation
| tl.TypeInputDocument,
position: number
): Promise<StickerSet>
/**
* Base function for update handling. Replace or override this function
* and implement your own update handler, and call this function
@ -2147,6 +2284,7 @@ export class TelegramClient extends BaseTelegramClient {
downloadToFile = downloadToFile
downloadAsIterable = downloadAsIterable
downloadAsStream = downloadAsStream
protected _normalizeFileToDocument = _normalizeFileToDocument
protected _normalizeInputFile = _normalizeInputFile
protected _normalizeInputMedia = _normalizeInputMedia
uploadFile = uploadFile
@ -2173,8 +2311,12 @@ export class TelegramClient extends BaseTelegramClient {
unregisterParseMode = unregisterParseMode
getParseMode = getParseMode
setDefaultParseMode = setDefaultParseMode
addStickerToSet = addStickerToSet
createStickerSet = createStickerSet
deleteStickerFromSet = deleteStickerFromSet
getInstalledStickers = getInstalledStickers
getStickerSet = getStickerSet
moveStickerInSet = moveStickerInSet
protected _fetchUpdatesState = _fetchUpdatesState
protected _loadStorage = _loadStorage
protected _saveStorage = _saveStorage

View file

@ -28,6 +28,7 @@ import {
ReplyMarkup,
InputMediaLike,
InputInlineResult,
InputStickerSetItem,
TakeoutSession,
StickerSet
} from '../types'
@ -37,3 +38,6 @@ import { MaybeArray, MaybeAsync, TelegramConnection } from '@mtcute/core'
// @copy
import { Lock } from '../utils/lock'
// @copy
import { tdFileId } from '@mtcute/file-id'

View file

@ -0,0 +1,25 @@
import { TelegramClient } from '../../client'
import { InputFileLike } from '../../types'
import { tl } from '@mtcute/tl'
import { assertTypeIs } from '../../utils/type-assertion'
/**
* @internal
*/
export async function _normalizeFileToDocument(
this: TelegramClient,
file: InputFileLike,
params: {
progressCallback?: (uploaded: number, total: number) => void
},
): Promise<tl.TypeInputDocument> {
const media = await this._normalizeInputMedia({
type: 'document',
file,
}, params, true)
assertTypeIs('createStickerSet', media, 'inputMediaDocument')
assertTypeIs('createStickerSet', media.id, 'inputDocument')
return media.id
}

View file

@ -0,0 +1,65 @@
import { TelegramClient } from '../../client'
import { InputFileLike, InputStickerSetItem, StickerSet } from '../../types'
import { tl } from '@mtcute/tl'
const MASK_POS = {
forehead: 0,
eyes: 1,
mouth: 2,
chin: 3,
} as const
/**
* Add a sticker to a sticker set.
*
* Only for bots, and the sticker set must
* have been created by this bot.
*
* @param id Sticker set short name or TL object with input sticker set
* @param sticker Sticker to be added
* @param params
* @returns Modfiied sticker set
* @internal
*/
export async function addStickerToSet(
this: TelegramClient,
id: string | tl.TypeInputStickerSet,
sticker: InputStickerSetItem,
params?: {
/**
* Upload progress callback
*
* @param uploaded Number of bytes uploaded
* @param total Total file size
*/
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,
sticker: {
_: 'inputStickerSetItem',
document: await this._normalizeFileToDocument(sticker.file, params ?? {}),
emoji: sticker.emojis,
maskCoords: sticker.maskPosition
? {
_: 'maskCoords',
n: MASK_POS[sticker.maskPosition.point],
x: sticker.maskPosition.x,
y: sticker.maskPosition.y,
zoom: sticker.maskPosition.scale,
}
: undefined,
}
})
return new StickerSet(this, res)
}

View file

@ -0,0 +1,135 @@
import { TelegramClient } from '../../client'
import {
InputFileLike,
InputPeerLike,
InputStickerSetItem,
MtCuteInvalidPeerTypeError,
StickerSet,
} from '../../types'
import { tl } from '@mtcute/tl'
import { normalizeToInputUser } from '../../utils/peer-utils'
const MASK_POS = {
forehead: 0,
eyes: 1,
mouth: 2,
chin: 3,
} as const
/**
* Create a new sticker set (only for bots)
*
* Only for bots.
*
* @param params
* @returns Newly created sticker set
* @internal
*/
export async function createStickerSet(
this: TelegramClient,
params: {
/**
* Owner of the sticker set (must be user)
*/
owner: InputPeerLike
/**
* Title of the sticker set (1-64 chars)
*/
title: string
/**
* Short name of the sticker set.
* Can only contain English letters, digits and underscores
* (i.e. must match `/^[a-zA-Z0-9_]+$/),
* and must end with `_by_<bot username>` (`<bot username>` is
* case-insensitive).
*/
shortName: string
/**
* Whether this is a set of masks
*/
masks?: boolean
/**
* Whether this is a set of animated stickers
*/
animated?: boolean
/**
* List of stickers to be immediately added into the pack.
* There must be at least one sticker in this list.
*/
stickers: InputStickerSetItem[]
/**
* Thumbnail for the set.
*
* The file must be either a `.png` file
* up to 128kb, having size of exactly `100x100` px,
* or a `.tgs` file up to 32kb.
*
* If not set, Telegram will use the first sticker
* in the sticker set as the thumbnail
*/
thumb?: InputFileLike
/**
* Upload progress callback.
*
* @param idx Index of the sticker
* @param uploaded Number of bytes uploaded
* @param total Total file size
*/
progressCallback?: (
idx: number,
uploaded: number,
total: number
) => void
}
): Promise<StickerSet> {
const owner = normalizeToInputUser(await this.resolvePeer(params.owner))
if (!owner) throw new MtCuteInvalidPeerTypeError(params.owner, 'user')
const inputStickers: tl.TypeInputStickerSetItem[] = []
let i = 0
for (const sticker of params.stickers) {
const progressCallback = params.progressCallback?.bind(null, i)
inputStickers.push({
_: 'inputStickerSetItem',
document: await this._normalizeFileToDocument(sticker.file, {
progressCallback,
}),
emoji: sticker.emojis,
maskCoords: sticker.maskPosition
? {
_: 'maskCoords',
n: MASK_POS[sticker.maskPosition.point],
x: sticker.maskPosition.x,
y: sticker.maskPosition.y,
zoom: sticker.maskPosition.scale,
}
: undefined,
})
i += 1
}
const res = await this.call({
_: 'stickers.createStickerSet',
animated: params.animated,
masks: params.masks,
userId: owner,
title: params.title,
shortName: params.shortName,
stickers: inputStickers,
thumb: params.thumb
? await this._normalizeFileToDocument(params.thumb, {})
: undefined,
})
return new StickerSet(this, res)
}

View file

@ -0,0 +1,32 @@
import { TelegramClient } from '../../client'
import { tl } from '@mtcute/tl'
import { StickerSet } from '../../types'
import { fileIdToInputDocument, tdFileId } from '../../../../file-id'
/**
* Delete a sticker from a sticker set
*
* Only for bots, and the sticker set must
* have been created by this bot.
*
* @param sticker
* TDLib and Bot API compatible File ID, or a
* TL object representing a sticker to be removed
* @returns Modfiied sticker set
* @internal
*/
export async function deleteStickerFromSet(
this: TelegramClient,
sticker: string | tdFileId.RawFullRemoteFileLocation | tl.TypeInputDocument
): Promise<StickerSet> {
if (tdFileId.isFileIdLike(sticker)) {
sticker = fileIdToInputDocument(sticker)
}
const res = await this.call({
_: 'stickers.removeStickerFromSet',
sticker
})
return new StickerSet(this, res)
}

View file

@ -0,0 +1,37 @@
import { TelegramClient } from '../../client'
import { fileIdToInputDocument, tdFileId } from '../../../../file-id'
import { tl } from '@mtcute/tl'
import { StickerSet } from '../../types'
/**
* Move a sticker in a sticker set
* to another position
*
* Only for bots, and the sticker set must
* have been created by this bot.
*
* @param sticker
* TDLib and Bot API compatible File ID, or a
* TL object representing a sticker to be removed
* @param position New sticker position (starting from 0)
* @returns Modfiied sticker set
* @internal
*/
export async function moveStickerInSet(
this: TelegramClient,
sticker: string | tdFileId.RawFullRemoteFileLocation | tl.TypeInputDocument,
position: number
): Promise<StickerSet> {
if (tdFileId.isFileIdLike(sticker)) {
sticker = fileIdToInputDocument(sticker)
}
const res = await this.call({
_: 'stickers.changeStickerPosition',
sticker,
position
})
return new StickerSet(this, res)
}

View file

@ -96,19 +96,27 @@ export class RawDocument extends FileLocation {
return this.thumbnails.find((it) => it.raw.type === type) ?? null
}
/**
* Input document TL object generated from this object,
* to be used with methods that use it
*/
get inputDocument(): tl.TypeInputDocument {
return {
_: 'inputDocument',
id: this.doc.id,
accessHash: this.doc.accessHash,
fileReference: this.doc.fileReference
}
}
/**
* Input media TL object generated from this object,
* to be used inside {@link InputMediaLike} or {@link TelegramClient.sendPhoto}
* to be used inside {@link InputMediaLike}
*/
get inputMediaTl(): tl.TypeInputMedia {
return {
_: 'inputMediaDocument',
id: {
_: 'inputDocument',
id: this.doc.id,
accessHash: this.doc.accessHash,
fileReference: this.doc.fileReference
}
id: this.inputDocument
}
}

View file

@ -5,6 +5,37 @@ import { makeInspectable } from '../utils'
import { StickerSet } from '../misc'
import { tdFileId } from '@mtcute/file-id'
export namespace Sticker {
export interface MaskPosition {
/**
* The part of the face relative where the mask should be placed
*/
point: 'forehead' | 'eyes' | 'mouth' | 'chin'
/**
* Shift by X-axis measured in widths of the mask scaled to
* the face size, from left to right. For example, choosing
* -1.0 will place mask just to the left of the default
* mask position.
*/
x: number
/**
* Shift by Y-axis measured in heights of the mask scaled to
* the face size, from top to bottom. For example, 1.0
* will place the mask just below the default mask position.
*/
y: number
/**
* Mask scaling coefficient. For example, 2.0 means double size.
*/
scale: number
}
}
const MASK_POS = ['forehead', 'eyes', 'mouth', 'chin'] as const
/**
* A sticker
*/
@ -67,6 +98,13 @@ export class Sticker extends RawDocument {
return this.mimeType === 'application/x-tgsticker'
}
/**
* Whether this is a mask
*/
get isMask(): boolean {
return !!this.attr.mask
}
/**
* Whether this sticker has an associated public sticker set.
*/
@ -74,6 +112,26 @@ export class Sticker extends RawDocument {
return this.attr.stickerset._ === 'inputStickerSetID'
}
private _maskPosition?: Sticker.MaskPosition
/**
* Position where this mask should be placed
*/
get maskPosition(): Sticker.MaskPosition | null {
if (!this.attr.maskCoords) return null
const raw = this.attr.maskCoords
if (!this._maskPosition) {
this._maskPosition = {
point: MASK_POS[raw.n],
x: raw.x,
y: raw.y,
scale: raw.zoom
}
}
return this._maskPosition
}
/**
* Get the sticker set that this sticker belongs to.
*

View file

@ -4,6 +4,7 @@ import { makeInspectable } from '../utils'
import { Sticker } from '../media'
import { MtCuteEmptyError, MtCuteTypeAssertionError } from '../errors'
import { parseDocument } from '../media/document-utils'
import { InputFileLike } from '../files'
export namespace StickerSet {
/**
@ -196,6 +197,103 @@ export class StickerSet {
return this.client.getStickerSet(this.inputStickerSet)
}
/**
* Add a new sticker to this sticker set.
*
* Only for bots, and the sticker set must
* have been created by this bot.
*
* Note that this method returns a new
* {@link StickerSet} object instead of modifying current.
*/
async addSticker(sticker: InputStickerSetItem): Promise<StickerSet> {
return this.client.addStickerToSet(this.inputStickerSet, sticker)
}
/**
* Delete a sticker from this set.
*
* Only for bots, and the sticker set must
* have been created by this bot.
*
* @param sticker
* Sticker File ID. In case this is a full sticker set object,
* you can also pass index (even negative), and that sticker will be removed
*/
async deleteSticker(sticker: number | Parameters<TelegramClient['deleteStickerFromSet']>[0]): Promise<StickerSet> {
if (typeof sticker === 'number') {
if (!this.full) throw new MtCuteEmptyError()
if (sticker < 0) sticker = this.full!.documents.length - sticker
const doc = this.full!.documents[sticker] as tl.RawDocument
sticker = {
_: 'inputDocument',
id: doc.id,
accessHash: doc.accessHash,
fileReference: doc.fileReference
}
}
return this.client.deleteStickerFromSet(sticker)
}
/**
* Move a sticker in this set.
*
* Only for bots, and the sticker set must
* have been created by this bot.
*
* @param sticker
* Sticker File ID. In case this is a full sticker set object,
* you can also pass index (even negative), and that sticker will be removed
* @param position New sticker position
*/
async moveSticker(sticker: number | Parameters<TelegramClient['moveStickerInSet']>[0], position: number): Promise<StickerSet> {
if (typeof sticker === 'number') {
if (!this.full) throw new MtCuteEmptyError()
if (sticker < 0) sticker = this.full!.documents.length - sticker
const doc = this.full!.documents[sticker] as tl.RawDocument
sticker = {
_: 'inputDocument',
id: doc.id,
accessHash: doc.accessHash,
fileReference: doc.fileReference
}
}
return this.client.moveStickerInSet(sticker, position)
}
}
makeInspectable(StickerSet, ['isFull'])
export interface InputStickerSetItem {
/**
* File containing the sticker.
*
* For normal stickers: must be a `.png` or `.webp` file
* up to 512kb, having both dimensions `<=512px`, and having
* one of the dimensions `==512px`
*
* For animated stickers: must be a `.tgs` file
* up to 64kb, having canvas dimensions exactly
* `512x512`px, duration no more than 3 seconds
* and animated at 60fps ([source](https://core.telegram.org/animated_stickers#technical-requirements))
*/
file: InputFileLike
/**
* One or more emojis that represent this sticker
*/
emojis: string
/**
* In case this is a mask sticker,
* position of the mask
*/
maskPosition?: Sticker.MaskPosition
}