feat(core): new stickers features

This commit is contained in:
alina 🌸 2024-05-05 20:56:23 +03:00
parent 756b99e12f
commit 00da6736e6
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
11 changed files with 161 additions and 58 deletions

View file

@ -0,0 +1,32 @@
import { tl } from '@mtcute/tl'
import { ITelegramClient } from '../../client.types.js'
import { InputStickerSetItem, MASK_POSITION_POINT_TO_TL } from '../../types/index.js'
import { _normalizeFileToDocument } from '../files/normalize-file-to-document.js'
/**
* @internal
* @noemit
*/
export async function _normalizeInputStickerSetItem(
client: ITelegramClient,
sticker: InputStickerSetItem,
params?: {
progressCallback?: (uploaded: number, total: number) => void
},
): Promise<tl.TypeInputStickerSetItem> {
return {
_: 'inputStickerSetItem',
document: await _normalizeFileToDocument(client, sticker.file, params ?? {}),
emoji: sticker.emojis,
maskCoords: sticker.maskPosition ?
{
_: 'maskCoords',
n: MASK_POSITION_POINT_TO_TL[sticker.maskPosition.point],
x: sticker.maskPosition.x,
y: sticker.maskPosition.y,
zoom: sticker.maskPosition.scale,
} :
undefined,
}
}

View file

@ -2,17 +2,16 @@ import { ITelegramClient } from '../../client.types.js'
import { import {
InputStickerSet, InputStickerSet,
InputStickerSetItem, InputStickerSetItem,
MASK_POSITION_POINT_TO_TL,
normalizeInputStickerSet, normalizeInputStickerSet,
StickerSet, StickerSet,
} from '../../types/index.js' } from '../../types/index.js'
import { _normalizeFileToDocument } from '../files/normalize-file-to-document.js' import { _normalizeFileToDocument } from '../files/normalize-file-to-document.js'
import { _normalizeInputStickerSetItem } from './_utils.js'
/** /**
* Add a sticker to a sticker set. * Add a sticker to a sticker set.
* *
* Only for bots, and the sticker set must * For bots the sticker set must have been created by this bot.
* have been created by this bot.
* *
* @param setId Sticker set short name or TL object with input sticker set * @param setId Sticker set short name or TL object with input sticker set
* @param sticker Sticker to be added * @param sticker Sticker to be added
@ -36,20 +35,7 @@ export async function addStickerToSet(
const res = await client.call({ const res = await client.call({
_: 'stickers.addStickerToSet', _: 'stickers.addStickerToSet',
stickerset: normalizeInputStickerSet(setId), stickerset: normalizeInputStickerSet(setId),
sticker: { sticker: await _normalizeInputStickerSetItem(client, sticker, params),
_: 'inputStickerSetItem',
document: await _normalizeFileToDocument(client, sticker.file, params ?? {}),
emoji: sticker.emojis,
maskCoords: sticker.maskPosition ?
{
_: 'maskCoords',
n: MASK_POSITION_POINT_TO_TL[sticker.maskPosition.point],
x: sticker.maskPosition.x,
y: sticker.maskPosition.y,
zoom: sticker.maskPosition.scale,
} :
undefined,
},
}) })
return new StickerSet(res) return new StickerSet(res)

View file

@ -5,20 +5,17 @@ import {
InputFileLike, InputFileLike,
InputPeerLike, InputPeerLike,
InputStickerSetItem, InputStickerSetItem,
MASK_POSITION_POINT_TO_TL,
StickerSet, StickerSet,
StickerSourceType, StickerSourceType,
StickerType, StickerType,
} from '../../types/index.js' } from '../../types/index.js'
import { _normalizeFileToDocument } from '../files/normalize-file-to-document.js' import { _normalizeFileToDocument } from '../files/normalize-file-to-document.js'
import { resolveUser } from '../users/resolve-peer.js' import { resolveUser } from '../users/resolve-peer.js'
import { _normalizeInputStickerSetItem } from './_utils.js'
/** /**
* Create a new sticker set. * Create a new sticker set.
* *
* This is the only sticker-related method that
* users can use (they allowed it with the "import stickers" update)
*
* @param params * @param params
* @returns Newly created sticker set * @returns Newly created sticker set
*/ */
@ -106,22 +103,7 @@ export async function createStickerSet(
for (const sticker of params.stickers) { for (const sticker of params.stickers) {
const progressCallback = params.progressCallback?.bind(null, i) const progressCallback = params.progressCallback?.bind(null, i)
inputStickers.push({ inputStickers.push(await _normalizeInputStickerSetItem(client, sticker, { progressCallback }))
_: 'inputStickerSetItem',
document: await _normalizeFileToDocument(client, sticker.file, {
progressCallback,
}),
emoji: sticker.emojis,
maskCoords: sticker.maskPosition ?
{
_: 'maskCoords',
n: MASK_POSITION_POINT_TO_TL[sticker.maskPosition.point],
x: sticker.maskPosition.x,
y: sticker.maskPosition.y,
zoom: sticker.maskPosition.scale,
} :
undefined,
})
i += 1 i += 1
} }

View file

@ -8,8 +8,7 @@ import { fileIdToInputDocument } from '../../utils/convert-file-id.js'
/** /**
* Delete a sticker from a sticker set * Delete a sticker from a sticker set
* *
* Only for bots, and the sticker set must * For bots the sticker set must have been created by this bot.
* have been created by this bot.
* *
* @param sticker * @param sticker
* TDLib and Bot API compatible File ID, or a * TDLib and Bot API compatible File ID, or a

View file

@ -9,8 +9,7 @@ import { StickerSet } from '../../types/index.js'
* *
* > **Note**: This method returns *brief* meta information about * > **Note**: This method returns *brief* meta information about
* > the packs, that does not include the stickers themselves. * > the packs, that does not include the stickers themselves.
* > Use {@link StickerSet.getFull} or {@link getStickerSet} * > Use {@link getStickerSet} to get a stickerset that will include the stickers
* > to get a stickerset that will include the stickers
*/ */
export async function getInstalledStickers(client: ITelegramClient): Promise<StickerSet[]> { export async function getInstalledStickers(client: ITelegramClient): Promise<StickerSet[]> {
const res = await client.call({ const res = await client.call({

View file

@ -0,0 +1,30 @@
import Long from 'long'
import { ITelegramClient } from '../../client.types.js'
import { StickerSet } from '../../types/misc/sticker-set.js'
import { ArrayPaginated } from '../../types/utils.js'
import { makeArrayPaginated } from '../../utils/misc-utils.js'
// @available=user
/**
* Get the list of sticker sets that were created by the current user
*/
export async function getMyStickerSets(
client: ITelegramClient,
params?: {
/** Offset for pagination */
offset?: Long
/** Limit for pagination */
limit?: number
},
): Promise<ArrayPaginated<StickerSet, Long>> {
const res = await client.call({
_: 'messages.getMyStickers',
offsetId: params?.offset ?? Long.ZERO,
limit: params?.limit ?? 100,
})
const items = res.sets.map((x) => new StickerSet(x))
return makeArrayPaginated(items, res.count, items[items.length - 1]?.brief.id)
}

View file

@ -2,9 +2,9 @@ import { ITelegramClient } from '../../client.types.js'
import { InputStickerSet, normalizeInputStickerSet, StickerSet } from '../../types/index.js' import { InputStickerSet, normalizeInputStickerSet, StickerSet } from '../../types/index.js'
/** /**
* Get a sticker pack and stickers inside of it. * Get a sticker set and stickers inside of it.
* *
* @param setId Sticker pack short name, dice emoji, `"emoji"` for animated emojis or input ID * @param setId Sticker set identifier
*/ */
export async function getStickerSet(client: ITelegramClient, setId: InputStickerSet): Promise<StickerSet> { export async function getStickerSet(client: ITelegramClient, setId: InputStickerSet): Promise<StickerSet> {
const res = await client.call({ const res = await client.call({

View file

@ -9,16 +9,14 @@ import { fileIdToInputDocument } from '../../utils/convert-file-id.js'
* Move a sticker in a sticker set * Move a sticker in a sticker set
* to another position * to another position
* *
* Only for bots, and the sticker set must * For bots the sticker set must have been created by this bot.
* have been created by this bot.
* *
* @param sticker * @param sticker
* TDLib and Bot API compatible File ID, or a * TDLib and Bot API compatible File ID, or a
* TL object representing a sticker to be removed * TL object representing a sticker to be removed
* @param position New sticker position (starting from 0) * @param position New sticker position (starting from 0)
* @returns Modfiied sticker set * @returns Modified sticker set
*/ */
export async function moveStickerInSet( export async function moveStickerInSet(
client: ITelegramClient, client: ITelegramClient,
sticker: string | tdFileId.RawFullRemoteFileLocation | tl.TypeInputDocument, sticker: string | tdFileId.RawFullRemoteFileLocation | tl.TypeInputDocument,

View file

@ -0,0 +1,45 @@
import { tdFileId } from '@mtcute/file-id'
import { tl } from '@mtcute/tl'
import { ITelegramClient } from '../../client.types.js'
import { InputStickerSetItem, StickerSet } from '../../types/index.js'
import { fileIdToInputDocument } from '../../utils/convert-file-id.js'
import { _normalizeInputStickerSetItem } from './_utils.js'
/**
* Replace a sticker in a sticker set with another sticker
*
* For bots 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 newSticker New sticker to replace the old one with
* @returns Modfiied sticker set
*/
export async function replaceStickerInSet(
client: ITelegramClient,
sticker: string | tdFileId.RawFullRemoteFileLocation | tl.TypeInputDocument,
newSticker: 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 (tdFileId.isFileIdLike(sticker)) {
sticker = fileIdToInputDocument(sticker)
}
const res = await client.call({
_: 'stickers.replaceSticker',
sticker,
newSticker: await _normalizeInputStickerSetItem(client, newSticker, params),
})
return new StickerSet(res)
}

View file

@ -9,7 +9,7 @@ import { Voice } from './voice.js'
export type ParsedDocument = Sticker | Voice | Audio | Video | Document export type ParsedDocument = Sticker | Voice | Audio | Video | Document
/** @internal */ /** @internal */
export function parseDocument(doc: tl.RawDocument, media?: tl.RawMessageMediaDocument): ParsedDocument { export function parseSticker(doc: tl.RawDocument) {
const stickerAttr = doc.attributes.find( const stickerAttr = doc.attributes.find(
(a) => a._ === 'documentAttributeSticker' || a._ === 'documentAttributeCustomEmoji', (a) => a._ === 'documentAttributeSticker' || a._ === 'documentAttributeCustomEmoji',
) )
@ -21,6 +21,12 @@ export function parseDocument(doc: tl.RawDocument, media?: tl.RawMessageMediaDoc
return new Sticker(doc, stickerAttr as tl.RawDocumentAttributeSticker | tl.RawDocumentAttributeCustomEmoji, sz) return new Sticker(doc, stickerAttr as tl.RawDocumentAttributeSticker | tl.RawDocumentAttributeCustomEmoji, sz)
} }
}
/** @internal */
export function parseDocument(doc: tl.RawDocument, media?: tl.RawMessageMediaDocument): ParsedDocument {
const sticker = parseSticker(doc)
if (sticker) return sticker
for (const attr of doc.attributes) { for (const attr of doc.attributes) {
switch (attr._) { switch (attr._) {

View file

@ -6,7 +6,7 @@ import { makeInspectable } from '../../utils/index.js'
import { memoizeGetters } from '../../utils/memoize.js' import { memoizeGetters } from '../../utils/memoize.js'
import { MtEmptyError } from '../errors.js' import { MtEmptyError } from '../errors.js'
import { InputFileLike } from '../files/index.js' import { InputFileLike } from '../files/index.js'
import { parseDocument } from '../media/document-utils.js' import { parseSticker } from '../media/document-utils.js'
import { MaskPosition, Sticker, StickerSourceType, StickerType, Thumbnail } from '../media/index.js' import { MaskPosition, Sticker, StickerSourceType, StickerType, Thumbnail } from '../media/index.js'
/** /**
@ -98,24 +98,38 @@ export interface StickerInfo {
readonly sticker: Sticker readonly sticker: Sticker
} }
function parseStickerOrThrow(doc: tl.RawDocument): Sticker {
const sticker = parseSticker(doc)
if (!sticker) {
throw new MtTypeAssertionError('full.documents', 'sticker', 'not a sticker')
}
return sticker
}
/** /**
* A sticker set (aka sticker pack) * A sticker set (aka sticker pack)
*/ */
export class StickerSet { export class StickerSet {
readonly brief: tl.RawStickerSet readonly brief: tl.RawStickerSet
readonly full?: tl.messages.RawStickerSet readonly full?: tl.messages.RawStickerSet
readonly cover?: tl.TypeStickerSetCovered
/** /**
* Whether this object contains information about stickers inside the set * Whether this object contains information about stickers inside the set
*/ */
readonly isFull: boolean readonly isFull: boolean
constructor(raw: tl.TypeStickerSet | tl.messages.TypeStickerSet) { constructor(raw: tl.TypeStickerSet | tl.messages.TypeStickerSet | tl.TypeStickerSetCovered) {
if (raw._ === 'messages.stickerSet') { if (raw._ === 'messages.stickerSet') {
this.full = raw this.full = raw
this.brief = raw.set this.brief = raw.set
} else if (raw._ === 'stickerSet') { } else if (raw._ === 'stickerSet') {
this.brief = raw this.brief = raw
} else if (tl.isAnyStickerSetCovered(raw)) {
this.cover = raw
this.brief = raw.set
} else { } else {
throw new MtTypeAssertionError('StickerSet', 'messages.stickerSet | stickerSet', raw._) throw new MtTypeAssertionError('StickerSet', 'messages.stickerSet | stickerSet', raw._)
} }
@ -222,17 +236,13 @@ export class StickerSet {
* In case this object does not contain info about stickers (i.e. {@link isFull} = false) * In case this object does not contain info about stickers (i.e. {@link isFull} = false)
*/ */
get stickers(): ReadonlyArray<StickerInfo> { get stickers(): ReadonlyArray<StickerInfo> {
if (!this.isFull) throw new MtEmptyError() if (!this.full) throw new MtEmptyError()
const stickers: StickerInfo[] = [] const stickers: StickerInfo[] = []
const index = new LongMap<tl.Mutable<StickerInfo>>() const index = new LongMap<tl.Mutable<StickerInfo>>()
this.full!.documents.forEach((doc) => { this.full.documents.forEach((doc) => {
const sticker = parseDocument(doc as tl.RawDocument) const sticker = parseStickerOrThrow(doc as tl.RawDocument)
if (!(sticker instanceof Sticker)) {
throw new MtTypeAssertionError('full.documents', 'Sticker', sticker.mimeType)
}
const info: tl.Mutable<StickerInfo> = { const info: tl.Mutable<StickerInfo> = {
alt: sticker.emoji, alt: sticker.emoji,
@ -243,7 +253,7 @@ export class StickerSet {
index.set(doc.id, info) index.set(doc.id, info)
}) })
this.full!.packs.forEach((pack) => { this.full.packs.forEach((pack) => {
pack.documents.forEach((id) => { pack.documents.forEach((id) => {
const item = index.get(id) const item = index.get(id)
@ -256,6 +266,22 @@ export class StickerSet {
return stickers return stickers
} }
/** Cover stickers of the sticker set. Not the same as {@link thumbnails} */
get covers(): ReadonlyArray<Sticker> {
if (!this.cover) return []
switch (this.cover._) {
case 'stickerSetCovered':
return [parseStickerOrThrow(this.cover.cover as tl.RawDocument)]
case 'stickerSetMultiCovered':
return this.cover.covers.map((it) => parseStickerOrThrow(it as tl.RawDocument))
case 'stickerSetFullCovered':
return this.cover.documents.map((it) => parseStickerOrThrow(it as tl.RawDocument))
case 'stickerSetNoCovered':
return []
}
}
/** /**
* Available sticker set thumbnails. * Available sticker set thumbnails.
* *