diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index af611b25..23f4ce43 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -75,6 +75,8 @@ import { setDefaultParseMode, unregisterParseMode, } from './methods/parse-modes/parse-modes' +import { getInstalledStickers } from './methods/stickers/get-installed-stickers' +import { getStickerSet } from './methods/stickers/get-sticker-set' import { _fetchUpdatesState, _handleUpdate, @@ -105,6 +107,7 @@ import { PartialExcept, ReplyMarkup, SentCode, + StickerSet, TakeoutSession, TermsOfService, UploadFileLike, @@ -1675,6 +1678,24 @@ export interface TelegramClient extends BaseTelegramClient { * @throws MtCuteError When given parse mode is not registered. */ setDefaultParseMode(name: string): void + /** + * Get a list of all installed sticker packs + * + * > **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 + * + */ + getInstalledStickers(): Promise + /** + * Get a sticker pack and stickers inside of it. + * + * @param id Sticker pack short name, dice emoji, `"emoji"` for animated emojis or input ID + */ + getStickerSet( + id: string | { dice: string } | tl.TypeInputStickerSet + ): Promise /** * Base function for update handling. Replace or override this function * and implement your own update handler, and call this function @@ -1846,6 +1867,8 @@ export class TelegramClient extends BaseTelegramClient { unregisterParseMode = unregisterParseMode getParseMode = getParseMode setDefaultParseMode = setDefaultParseMode + getInstalledStickers = getInstalledStickers + getStickerSet = getStickerSet protected _fetchUpdatesState = _fetchUpdatesState protected _loadStorage = _loadStorage protected _saveStorage = _saveStorage diff --git a/packages/client/src/methods/_imports.ts b/packages/client/src/methods/_imports.ts index 6851d68b..83709d51 100644 --- a/packages/client/src/methods/_imports.ts +++ b/packages/client/src/methods/_imports.ts @@ -26,7 +26,8 @@ import { Message, ReplyMarkup, InputMediaLike, - TakeoutSession + TakeoutSession, + StickerSet } from '../types' // @copy diff --git a/packages/client/src/methods/stickers/get-installed-stickers.ts b/packages/client/src/methods/stickers/get-installed-stickers.ts new file mode 100644 index 00000000..81a2fdec --- /dev/null +++ b/packages/client/src/methods/stickers/get-installed-stickers.ts @@ -0,0 +1,26 @@ +import { TelegramClient } from '../../client' +import { StickerSet } from '../../types' +import { assertTypeIs } from '../../utils/type-assertion' + +/** + * Get a list of all installed sticker packs + * + * > **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 + * + * @internal + */ +export async function getInstalledStickers( + this: TelegramClient +): Promise { + const res = await this.call({ + _: 'messages.getAllStickers', + hash: 0 + }) + + assertTypeIs('getInstalledStickers', res, 'messages.allStickers') + + return res.sets.map((set) => new StickerSet(this, set)) +} diff --git a/packages/client/src/methods/stickers/get-sticker-set.ts b/packages/client/src/methods/stickers/get-sticker-set.ts new file mode 100644 index 00000000..951bfa42 --- /dev/null +++ b/packages/client/src/methods/stickers/get-sticker-set.ts @@ -0,0 +1,38 @@ +import { TelegramClient } from '../../client' +import { tl } from '@mtcute/tl' +import { StickerSet } from '../../types' + +/** + * Get a sticker pack and stickers inside of it. + * + * @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 { + 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 + } + + const res = await this.call({ + _: 'messages.getStickerSet', + stickerset: input + }) + + return new StickerSet(this, res) +} diff --git a/packages/client/src/types/media/sticker.ts b/packages/client/src/types/media/sticker.ts index 1bea1e0d..2a5ca39b 100644 --- a/packages/client/src/types/media/sticker.ts +++ b/packages/client/src/types/media/sticker.ts @@ -2,6 +2,7 @@ import { RawDocument } from './document' import { TelegramClient } from '../../client' import { tl } from '@mtcute/tl' import { makeInspectable } from '../utils' +import { StickerSet } from '../misc' /** * A sticker @@ -36,14 +37,15 @@ export class Sticker extends RawDocument { } /** - * Emoji associated with this sticker. + * Primary emoji associated with this sticker, + * that is displayed in dialogs list. * * If there is none, empty string is returned. * * **Note:** This only contains at most one emoji. * Some stickers have multiple associated emojis, * but only one is returned here. This is Telegram's - * limitation! Use {@link getAllEmojis} + * limitation! Use {@link getAllEmojis} instead. */ get emoji(): string { return this.attr.alt @@ -72,13 +74,10 @@ export class Sticker extends RawDocument { * * Returns `null` if there's no sticker set. */ - async getStickerSet(): Promise { + async getStickerSet(): Promise { if (!this.hasStickerSet) return null - return this.client.call({ - _: 'messages.getStickerSet', - stickerset: this.attr.stickerset, - }) + return this.client.getStickerSet(this.attr.stickerset) } /** @@ -88,18 +87,11 @@ export class Sticker extends RawDocument { * with a sticker pack. */ async getAllEmojis(): Promise { - let ret = '' - const set = await this.getStickerSet() if (!set) return '' - set.packs.forEach((pack) => { - if (pack.documents.some((doc) => doc.eq(this.doc.id))) { - ret += pack.emoticon - } - }) - - return ret + return set.stickers.find((it) => it.sticker.doc.id.eq(this.doc.id))! + .emoji } } diff --git a/packages/client/src/types/misc/index.ts b/packages/client/src/types/misc/index.ts index dab0e4f9..ca753e90 100644 --- a/packages/client/src/types/misc/index.ts +++ b/packages/client/src/types/misc/index.ts @@ -1 +1,2 @@ export * from './takeout-session' +export * from './sticker-set' diff --git a/packages/client/src/types/misc/sticker-set.ts b/packages/client/src/types/misc/sticker-set.ts new file mode 100644 index 00000000..3d8ac5bb --- /dev/null +++ b/packages/client/src/types/misc/sticker-set.ts @@ -0,0 +1,201 @@ +import { TelegramClient } from '../../client' +import { tl } from '@mtcute/tl' +import { makeInspectable } from '../utils' +import { Sticker } from '../media' +import { MtCuteEmptyError, MtCuteTypeAssertionError } from '../errors' +import { parseDocument } from '../media/document-utils' + +export namespace StickerSet { + /** + * Information about one sticker inside the set + */ + export interface StickerInfo { + /** + * Primary alt emoji that is displayed in dialogs list + */ + readonly alt: string + + /** + * One or more emojis representing this sticker + */ + readonly emoji: string + + /** + * Document with the actual sticker + */ + readonly sticker: Sticker + } +} + +/** + * A stickerset (aka sticker pack) + */ +export class StickerSet { + readonly client: TelegramClient + readonly brief: tl.RawStickerSet + readonly full?: tl.messages.RawStickerSet + + /** + * Whether this object contains information about stickers inside the set + */ + readonly isFull: boolean + + constructor( + client: TelegramClient, + raw: tl.RawStickerSet | tl.messages.RawStickerSet + ) { + this.client = client + if (raw._ === 'messages.stickerSet') { + this.full = raw + this.brief = raw.set + } else { + this.brief = raw + } + + this.isFull = raw._ === 'messages.stickerSet' + } + + /** + * Whether this stickerset was archived (due to too many saved stickers in the current account) + */ + get isArchived(): boolean { + return this.brief.archived! + } + + /** + * Whether this stickerset is official + */ + get isOfficial(): boolean { + return this.brief.official! + } + + /** + * Whether this stickerset is a set of masks + */ + get isMasks(): boolean { + return this.brief.masks! + } + + /** + * Whether this stickerset is animated + */ + get isAnimated(): boolean { + return this.brief.animated! + } + + /** + * Date when this stickerset was installed + */ + get installedDate(): Date | null { + return this.brief.installedDate + ? new Date(this.brief.installedDate * 1000) + : null + } + + /** + * Number of stickers in this stickerset + */ + get count(): number { + return this.brief.count + } + + /** + * Input sticker set to be used in other API methods + */ + get inputStickerSet(): tl.TypeInputStickerSet { + return { + _: 'inputStickerSetID', + id: this.brief.id, + accessHash: this.brief.accessHash + } + } + + /** + * Title of the stickerset + */ + get title(): string { + return this.brief.title + } + + /** + * Short name of stickerset to use in `tg://addstickers?set=short_name` + * or `https://t.me/addstickers/short_name` + */ + get shortName(): string { + return this.brief.shortName + } + + private _stickers?: StickerSet.StickerInfo[] + /** + * List of stickers inside this stickerset + * + * @throws MtCuteEmptyError + * In case this object does not contain info about stickers (i.e. {@link isFull} = false) + */ + get stickers(): StickerSet.StickerInfo[] { + if (!this.isFull) throw new MtCuteEmptyError() + + if (!this._stickers) { + this._stickers = [] + const index: Record> = {} + + this.full!.documents.forEach((doc) => { + const sticker = parseDocument( + this.client, + doc as tl.RawDocument + ) + if (!(sticker instanceof Sticker)) { + throw new MtCuteTypeAssertionError( + 'StickerSet#stickers (full.documents)', + 'Sticker', + sticker.mimeType + ) + } + + const info: tl.Mutable = { + alt: sticker.emoji, + emoji: '', // populated later + sticker + } + this._stickers!.push(info) + index[doc.id.toString()] = info + }) + + this.full!.packs.forEach((pack) => { + pack.documents.forEach((id) => { + const sid = id.toString() + if (sid in index) { + index[sid].emoji += pack.emoticon + } + }) + }) + } + + return this._stickers + } + + /** + * Find stickers given their emoji. + * + * @param emoji Emoji to search for + * @throws MtCuteEmptyError + * In case this object does not contain info about stickers (i.e. {@link isFull} = false) + */ + getStickersByEmoji(emoji: string): StickerSet.StickerInfo[] { + return this.stickers.filter(it => it.alt === emoji || it.emoji.indexOf(emoji) != -1) + } + + /** + * Get full stickerset object. + * + * If this object is already full, this method will just + * return `this` + */ + async getFull(): Promise { + if (this.isFull) return this + + return this.client.getStickerSet(this.inputStickerSet) + } +} + +makeInspectable(StickerSet, ['isFull'])