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 {
InputStickerSet,
InputStickerSetItem,
MASK_POSITION_POINT_TO_TL,
normalizeInputStickerSet,
StickerSet,
} from '../../types/index.js'
import { _normalizeFileToDocument } from '../files/normalize-file-to-document.js'
import { _normalizeInputStickerSetItem } from './_utils.js'
/**
* Add a sticker to a sticker set.
*
* Only for bots, and the sticker set must
* have been created by this bot.
* For bots the sticker set must have been created by this bot.
*
* @param setId Sticker set short name or TL object with input sticker set
* @param sticker Sticker to be added
@ -36,20 +35,7 @@ export async function addStickerToSet(
const res = await client.call({
_: 'stickers.addStickerToSet',
stickerset: normalizeInputStickerSet(setId),
sticker: {
_: '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,
},
sticker: await _normalizeInputStickerSetItem(client, sticker, params),
})
return new StickerSet(res)

View file

@ -5,20 +5,17 @@ import {
InputFileLike,
InputPeerLike,
InputStickerSetItem,
MASK_POSITION_POINT_TO_TL,
StickerSet,
StickerSourceType,
StickerType,
} from '../../types/index.js'
import { _normalizeFileToDocument } from '../files/normalize-file-to-document.js'
import { resolveUser } from '../users/resolve-peer.js'
import { _normalizeInputStickerSetItem } from './_utils.js'
/**
* 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
* @returns Newly created sticker set
*/
@ -106,22 +103,7 @@ export async function createStickerSet(
for (const sticker of params.stickers) {
const progressCallback = params.progressCallback?.bind(null, i)
inputStickers.push({
_: '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,
})
inputStickers.push(await _normalizeInputStickerSetItem(client, sticker, { progressCallback }))
i += 1
}

View file

@ -8,8 +8,7 @@ import { fileIdToInputDocument } from '../../utils/convert-file-id.js'
/**
* Delete a sticker from a sticker set
*
* Only for bots, and the sticker set must
* have been created by this bot.
* For bots the sticker set must have been created by this bot.
*
* @param sticker
* 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
* > the packs, that does not include the stickers themselves.
* > Use {@link StickerSet.getFull} or {@link getStickerSet}
* > to get a stickerset that will include the stickers
* > Use {@link getStickerSet} to get a stickerset that will include the stickers
*/
export async function getInstalledStickers(client: ITelegramClient): Promise<StickerSet[]> {
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'
/**
* 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> {
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
* to another position
*
* Only for bots, and the sticker set must
* have been created by this bot.
* 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 position New sticker position (starting from 0)
* @returns Modfiied sticker set
* @returns Modified sticker set
*/
export async function moveStickerInSet(
client: ITelegramClient,
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
/** @internal */
export function parseDocument(doc: tl.RawDocument, media?: tl.RawMessageMediaDocument): ParsedDocument {
export function parseSticker(doc: tl.RawDocument) {
const stickerAttr = doc.attributes.find(
(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)
}
}
/** @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) {
switch (attr._) {

View file

@ -6,7 +6,7 @@ import { makeInspectable } from '../../utils/index.js'
import { memoizeGetters } from '../../utils/memoize.js'
import { MtEmptyError } from '../errors.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'
/**
@ -98,24 +98,38 @@ export interface StickerInfo {
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)
*/
export class StickerSet {
readonly brief: tl.RawStickerSet
readonly full?: tl.messages.RawStickerSet
readonly cover?: tl.TypeStickerSetCovered
/**
* Whether this object contains information about stickers inside the set
*/
readonly isFull: boolean
constructor(raw: tl.TypeStickerSet | tl.messages.TypeStickerSet) {
constructor(raw: tl.TypeStickerSet | tl.messages.TypeStickerSet | tl.TypeStickerSetCovered) {
if (raw._ === 'messages.stickerSet') {
this.full = raw
this.brief = raw.set
} else if (raw._ === 'stickerSet') {
this.brief = raw
} else if (tl.isAnyStickerSetCovered(raw)) {
this.cover = raw
this.brief = raw.set
} else {
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)
*/
get stickers(): ReadonlyArray<StickerInfo> {
if (!this.isFull) throw new MtEmptyError()
if (!this.full) throw new MtEmptyError()
const stickers: StickerInfo[] = []
const index = new LongMap<tl.Mutable<StickerInfo>>()
this.full!.documents.forEach((doc) => {
const sticker = parseDocument(doc as tl.RawDocument)
if (!(sticker instanceof Sticker)) {
throw new MtTypeAssertionError('full.documents', 'Sticker', sticker.mimeType)
}
this.full.documents.forEach((doc) => {
const sticker = parseStickerOrThrow(doc as tl.RawDocument)
const info: tl.Mutable<StickerInfo> = {
alt: sticker.emoji,
@ -243,7 +253,7 @@ export class StickerSet {
index.set(doc.id, info)
})
this.full!.packs.forEach((pack) => {
this.full.packs.forEach((pack) => {
pack.documents.forEach((id) => {
const item = index.get(id)
@ -256,6 +266,22 @@ export class StickerSet {
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.
*