From eaa517a5c3435ff533d8b93d67bb3a80387eac68 Mon Sep 17 00:00:00 2001 From: teidesu <86301490+teidesu@users.noreply.github.com> Date: Thu, 18 Aug 2022 19:52:24 +0300 Subject: [PATCH] feat(client): support custom emojis --- packages/client/src/client.ts | 27 +++++-- packages/client/src/methods/_imports.ts | 1 + .../invite-links/get-invite-link-members.ts | 4 +- .../methods/stickers/create-sticker-set.ts | 27 +++++-- .../src/methods/stickers/get-custom-emojis.ts | 37 ++++++++++ .../client/src/types/media/document-utils.ts | 4 +- packages/client/src/types/media/sticker.ts | 74 ++++++++++++++----- .../src/types/messages/message-entity.ts | 11 +++ packages/client/src/types/messages/message.ts | 12 ++- packages/client/src/types/misc/sticker-set.ts | 56 ++++++++------ packages/dispatcher/src/filters.ts | 28 +++---- 11 files changed, 204 insertions(+), 77 deletions(-) create mode 100644 packages/client/src/methods/stickers/get-custom-emojis.ts diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 2e1ff8fa..2e613fec 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -46,6 +46,7 @@ import { RawDocument, ReplyMarkup, SentCode, + Sticker, StickerSet, TakeoutSession, TermsOfService, @@ -217,6 +218,7 @@ import { removeCloudPassword } from './methods/pasword/remove-cloud-password' 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 { 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' @@ -2018,9 +2020,9 @@ export interface TelegramClient extends BaseTelegramClient { /** * Search for a user in the pending join requests list - * (only works if {@see requested} is true) + * (only works if {@link requested} is true) * - * Doesn't work when {@see link} is set (Telegram limitation) + * Doesn't work when {@link link} is set (Telegram limitation) */ requestedSearch?: string } @@ -3559,14 +3561,18 @@ export interface TelegramClient extends BaseTelegramClient { shortName: string /** - * Whether this is a set of masks + * Type of the stickers in this set. + * Defaults to `sticker`, i.e. regular stickers. + * + * Creating `emoji` stickers via API is not supported yet */ - masks?: boolean + type?: Sticker.Type /** - * Whether this is a set of animated stickers + * File source type for the stickers in this set. + * Defaults to `static`, i.e. regular WEBP stickers. */ - animated?: boolean + sourceType?: Sticker.SourceType /** * List of stickers to be immediately added into the pack. @@ -3616,6 +3622,12 @@ export interface TelegramClient extends BaseTelegramClient { | tdFileId.RawFullRemoteFileLocation | tl.TypeInputDocument ): Promise + /** + * Get custom emoji stickers by their IDs + * + * @param ids IDs of the stickers (as defined in {@link MessageEntity.emojiId}) + */ + getCustomEmojis(ids: tl.Long[]): Promise /** * Get a list of all installed sticker packs * @@ -3933,7 +3945,7 @@ export interface TelegramClient extends BaseTelegramClient { */ updateUsername(username: string | null): Promise } -/** @internal */ + export class TelegramClient extends BaseTelegramClient { protected _userId: number | null protected _isBot: boolean @@ -4161,6 +4173,7 @@ export class TelegramClient extends BaseTelegramClient { addStickerToSet = addStickerToSet createStickerSet = createStickerSet deleteStickerFromSet = deleteStickerFromSet + getCustomEmojis = getCustomEmojis getInstalledStickers = getInstalledStickers getStickerSet = getStickerSet moveStickerInSet = moveStickerInSet diff --git a/packages/client/src/methods/_imports.ts b/packages/client/src/methods/_imports.ts index adabc6e4..25624d4b 100644 --- a/packages/client/src/methods/_imports.ts +++ b/packages/client/src/methods/_imports.ts @@ -57,6 +57,7 @@ import { ChatJoinRequestUpdate, PeerReaction, MessageReactions, + Sticker } from '../types' // @copy diff --git a/packages/client/src/methods/invite-links/get-invite-link-members.ts b/packages/client/src/methods/invite-links/get-invite-link-members.ts index 1c94eb29..d23f7e1c 100644 --- a/packages/client/src/methods/invite-links/get-invite-link-members.ts +++ b/packages/client/src/methods/invite-links/get-invite-link-members.ts @@ -33,9 +33,9 @@ export async function* getInviteLinkMembers( /** * Search for a user in the pending join requests list - * (only works if {@see requested} is true) + * (only works if {@link requested} is true) * - * Doesn't work when {@see link} is set (Telegram limitation) + * Doesn't work when {@link link} is set (Telegram limitation) */ requestedSearch?: string } diff --git a/packages/client/src/methods/stickers/create-sticker-set.ts b/packages/client/src/methods/stickers/create-sticker-set.ts index 22c861d4..95ef59c0 100644 --- a/packages/client/src/methods/stickers/create-sticker-set.ts +++ b/packages/client/src/methods/stickers/create-sticker-set.ts @@ -5,7 +5,9 @@ import { InputFileLike, InputPeerLike, InputStickerSetItem, + MtArgumentError, MtInvalidPeerTypeError, + Sticker, StickerSet, } from '../../types' import { normalizeToInputUser } from '../../utils/peer-utils' @@ -53,14 +55,18 @@ export async function createStickerSet( shortName: string /** - * Whether this is a set of masks + * Type of the stickers in this set. + * Defaults to `sticker`, i.e. regular stickers. + * + * Creating `emoji` stickers via API is not supported yet */ - masks?: boolean + type?: Sticker.Type /** - * Whether this is a set of animated stickers + * File source type for the stickers in this set. + * Defaults to `static`, i.e. regular WEBP stickers. */ - animated?: boolean + sourceType?: Sticker.SourceType /** * List of stickers to be immediately added into the pack. @@ -94,6 +100,12 @@ export async function createStickerSet( ) => void } ): Promise { + if (params.type === 'emoji') { + throw new MtArgumentError( + 'Creating emoji stickers is not supported yet by the API' + ) + } + const owner = normalizeToInputUser(await this.resolvePeer(params.owner)) if (!owner) throw new MtInvalidPeerTypeError(params.owner, 'user') @@ -125,8 +137,11 @@ export async function createStickerSet( const res = await this.call({ _: 'stickers.createStickerSet', - animated: params.animated, - masks: params.masks, + animated: params.sourceType === 'animated', + videos: params.sourceType === 'video', + masks: params.type === 'mask', + // currently not supported + // emojis: params.type === 'emoji', userId: owner, title: params.title, shortName: params.shortName, diff --git a/packages/client/src/methods/stickers/get-custom-emojis.ts b/packages/client/src/methods/stickers/get-custom-emojis.ts new file mode 100644 index 00000000..7718437d --- /dev/null +++ b/packages/client/src/methods/stickers/get-custom-emojis.ts @@ -0,0 +1,37 @@ +import { tl } from '@mtcute/tl' + +import { TelegramClient } from '../../client' +import { MtTypeAssertionError, Sticker } from '../../types' +import { parseDocument } from '../../types/media/document-utils' +import { assertTypeIs } from '../../utils/type-assertion' + +/** + * Get custom emoji stickers by their IDs + * + * @param ids IDs of the stickers (as defined in {@link MessageEntity.emojiId}) + * @internal + */ +export async function getCustomEmojis( + this: TelegramClient, + ids: tl.Long[] +): Promise { + const res = await this.call({ + _: 'messages.getCustomEmojiDocuments', + documentId: ids, + }) + + return res.map((it) => { + assertTypeIs('getCustomEmojis', it, 'document') + + const doc = parseDocument(this, it) + if ((doc as Sticker).type !== 'sticker') { + throw new MtTypeAssertionError( + 'getCustomEmojis', + 'sticker', + (doc as any).type + ) + } + + return doc as Sticker + }) +} diff --git a/packages/client/src/types/media/document-utils.ts b/packages/client/src/types/media/document-utils.ts index 6239e732..93d39ed1 100644 --- a/packages/client/src/types/media/document-utils.ts +++ b/packages/client/src/types/media/document-utils.ts @@ -20,7 +20,8 @@ export function parseDocument( } else { return new Audio(client, doc, attr) } - case 'documentAttributeSticker': { + case 'documentAttributeSticker': + case 'documentAttributeCustomEmoji': { const sz = doc.attributes.find( (it) => it._ === 'documentAttributeImageSize' || @@ -28,6 +29,7 @@ export function parseDocument( )! as | tl.RawDocumentAttributeImageSize | tl.RawDocumentAttributeVideo + return new Sticker(client, doc, attr, sz) } case 'documentAttributeVideo': diff --git a/packages/client/src/types/media/sticker.ts b/packages/client/src/types/media/sticker.ts index 39298850..d008d260 100644 --- a/packages/client/src/types/media/sticker.ts +++ b/packages/client/src/types/media/sticker.ts @@ -33,6 +33,22 @@ export namespace Sticker { */ scale: number } + + /** + * Type of the sticker + * - `sticker`: regular sticker + * - `mask`: mask sticker + * - `emoji`: custom emoji + */ + export type Type = 'sticker' | 'mask' | 'emoji' + + /** + * Sticker source file type + * - `static`: static sticker (webp) + * - `animated`: animated sticker (gzipped lottie json) + * - `video`: video sticker (webm) + */ + export type SourceType = 'static' | 'animated' | 'video' } const MASK_POS = ['forehead', 'eyes', 'mouth', 'chin'] as const @@ -50,7 +66,9 @@ export class Sticker extends RawDocument { constructor( client: TelegramClient, doc: tl.RawDocument, - readonly attr: tl.RawDocumentAttributeSticker, + readonly attr: + | tl.RawDocumentAttributeSticker + | tl.RawDocumentAttributeCustomEmoji, readonly attr2?: | tl.RawDocumentAttributeImageSize | tl.RawDocumentAttributeVideo @@ -73,14 +91,8 @@ export class Sticker extends RawDocument { } /** - * Whether this sticker is a video (WEBM) sticker - */ - get isVideoSticker(): boolean { - return this.attr2?._ === 'documentAttributeVideo' - } - - /** - * Whether this sticker is a video (WEBM) sticker + * Whether this sticker is a premium sticker + * (has premium fullscreen animation) */ get isPremiumSticker(): boolean { return !!this.raw.videoThumbs?.some((s) => s.type === 'f') @@ -109,27 +121,50 @@ export class Sticker extends RawDocument { * Some stickers have multiple associated emojis, * but only one is returned here. This is Telegram's * limitation! Use {@link getAllEmojis} instead. + * + * For custom emojis, this alt should be used as a fallback + * text that will be "behind" the custom emoji entity. */ get emoji(): string { return this.attr.alt } /** - * Whether the sticker is animated. + * Whether this custom emoji can be used by non-premium users. + * `false` if this is not a custom emoji. * - * Animated stickers are represented as gzipped - * lottie json files, and have MIME `application/x-tgsticker`, - * while normal stickers are WEBP images and have MIME `image/webp` + * > Not sure if there are any such stickers currently. */ - get isAnimated(): boolean { - return this.mimeType === 'application/x-tgsticker' + get customEmojiFree(): boolean { + return this.attr._ === 'documentAttributeCustomEmoji' + ? this.attr?.free ?? false + : false } /** - * Whether this is a mask + * Type of the sticker */ - get isMask(): boolean { - return this.attr.mask! + get stickerType(): Sticker.Type { + if (this.attr._ === 'documentAttributeSticker') { + return this.attr.mask ? 'mask' : 'sticker' + } else if (this.attr._ === 'documentAttributeCustomEmoji') { + return 'emoji' + } else { + return 'sticker' + } + } + + /** + * Type of the file representing the sticker + */ + get sourceType(): Sticker.SourceType { + if (this.attr2?._ === 'documentAttributeVideo') { + return 'video' + } else { + return this.mimeType === 'application/x-tgsticker' + ? 'animated' + : 'static' + } } /** @@ -153,7 +188,8 @@ export class Sticker extends RawDocument { * Position where this mask should be placed */ get maskPosition(): Sticker.MaskPosition | null { - if (!this.attr.maskCoords) return null + if (this.attr._ !== 'documentAttributeSticker' || !this.attr.maskCoords) + return null const raw = this.attr.maskCoords if (!this._maskPosition) { diff --git a/packages/client/src/types/messages/message-entity.ts b/packages/client/src/types/messages/message-entity.ts index b345da90..4981b381 100644 --- a/packages/client/src/types/messages/message-entity.ts +++ b/packages/client/src/types/messages/message-entity.ts @@ -22,6 +22,7 @@ const entityToType: Partial< messageEntityTextUrl: 'text_link', messageEntityUnderline: 'underline', messageEntityUrl: 'url', + messageEntityCustomEmoji: 'emoji', } export namespace MessageEntity { @@ -43,6 +44,7 @@ export namespace MessageEntity { * - 'text_link': for clickable text URLs. * - 'text_mention': for users without usernames (see {@link MessageEntity.user} below). * - 'blockquote': A blockquote + * - 'emoji': A custom emoji */ export type Type = | 'mention' @@ -62,6 +64,7 @@ export namespace MessageEntity { | 'text_link' | 'text_mention' | 'blockquote' + | 'emoji' } /** @@ -107,6 +110,13 @@ export class MessageEntity { */ readonly language?: string + /** + * When `type=emoji`, ID of the custom emoji. + * The emoji itself must be loaded separately (and presumably cached) + * using {@link TelegramClient#getCustomEmojis} + */ + readonly emojiId?: tl.Long + static _parse(obj: tl.TypeMessageEntity): MessageEntity | null { const type = entityToType[obj._] if (!type) return null @@ -120,6 +130,7 @@ export class MessageEntity { userId: obj._ === 'messageEntityMentionName' ? obj.userId : undefined, language: obj._ === 'messageEntityPre' ? obj.language : undefined, + emojiId: obj._ === 'messageEntityCustomEmoji' ? obj.documentId : undefined, } } } diff --git a/packages/client/src/types/messages/message.ts b/packages/client/src/types/messages/message.ts index 57ad61a3..4a8c46ab 100644 --- a/packages/client/src/types/messages/message.ts +++ b/packages/client/src/types/messages/message.ts @@ -7,7 +7,7 @@ import { MtArgumentError, MtTypeAssertionError } from '../errors' import { TelegramClient } from '../../client' import { MessageEntity } from './message-entity' import { makeInspectable } from '../utils' -import { InputMediaLike, WebPage } from '../media' +import { InputMediaLike, Sticker, WebPage } from '../media' import { _messageActionFromTl, MessageAction } from './message-action' import { _messageMediaFromTl, MessageMedia } from './message-media' import { FormattedString } from '../parser' @@ -947,6 +947,16 @@ export class Message { big ) } + + async getCustomEmojis(): Promise { + if (this.raw._ === 'messageService' || !this.raw.entities) return [] + + return this.client.getCustomEmojis( + this.raw.entities + .filter((it) => it._ === 'messageEntityCustomEmoji') + .map((it) => (it as tl.RawMessageEntityCustomEmoji).documentId) + ) + } } makeInspectable(Message, ['isScheduled'], ['link']) diff --git a/packages/client/src/types/misc/sticker-set.ts b/packages/client/src/types/misc/sticker-set.ts index 2512773e..105cdd85 100644 --- a/packages/client/src/types/misc/sticker-set.ts +++ b/packages/client/src/types/misc/sticker-set.ts @@ -30,7 +30,7 @@ export namespace StickerSet { } /** - * A stickerset (aka sticker pack) + * A sticker set (aka sticker pack) */ export class StickerSet { readonly brief: tl.RawStickerSet @@ -62,7 +62,8 @@ export class StickerSet { } /** - * Whether this stickerset was archived (due to too many saved stickers in the current account) + * Whether this sticker set was archived + * (due to too many saved stickers in the current account) */ get isArchived(): boolean { return this.brief.archived! @@ -76,28 +77,37 @@ export class StickerSet { } /** - * Whether this stickerset is a set of masks + * Type of the stickers in this set */ - get isMasks(): boolean { - return this.brief.masks! + get type(): Sticker.Type { + if (this.brief.masks) { + return 'mask' + } + + if (this.brief.emojis) { + return 'emoji' + } + + return 'sticker' } /** - * Whether this stickerset is animated + * Source file type of the stickers in this set */ - get isAnimated(): boolean { - return this.brief.animated! + get sourceType(): Sticker.SourceType { + if (this.brief.animated) { + return 'animated' + } + + if (this.brief.videos) { + return 'video' + } + + return 'static' } /** - * Whether this stickerset is video (WEBM) - */ - get isVideo(): boolean { - return this.brief.videos! - } - - /** - * Date when this stickerset was installed + * Date when this sticker set was installed */ get installedDate(): Date | null { return this.brief.installedDate @@ -106,7 +116,7 @@ export class StickerSet { } /** - * Number of stickers in this stickerset + * Number of stickers in this sticker set */ get count(): number { return this.brief.count @@ -124,14 +134,14 @@ export class StickerSet { } /** - * Title of the stickerset + * Title of the sticker set */ get title(): string { return this.brief.title } /** - * Short name of stickerset to use in `tg://addstickers?set=short_name` + * Short name of sticker set to use in `tg://addstickers?set=short_name` * or `https://t.me/addstickers/short_name` */ get shortName(): string { @@ -140,7 +150,7 @@ export class StickerSet { private _stickers?: StickerSet.StickerInfo[] /** - * List of stickers inside this stickerset + * List of stickers inside this sticker set * * @throws MtEmptyError * In case this object does not contain info about stickers (i.e. {@link isFull} = false) @@ -189,7 +199,7 @@ export class StickerSet { private _thumbnails?: Thumbnail[] /** - * Available stickerset thumbnails. + * Available sticker set thumbnails. * * Returns empty array if not available * (i.e. first sticker should be used as thumbnail) @@ -206,7 +216,7 @@ export class StickerSet { } /** - * Get a stickerset thumbnail by its type. + * Get a sticker set thumbnail by its type. * * Thumbnail types are described in the * [Telegram docs](https://core.telegram.org/api/files#image-thumbnail-types), @@ -232,7 +242,7 @@ export class StickerSet { } /** - * Get full stickerset object. + * Get full sticker set object. * * If this object is already full, this method will just * return `this` diff --git a/packages/dispatcher/src/filters.ts b/packages/dispatcher/src/filters.ts index 4534f917..8973516c 100644 --- a/packages/dispatcher/src/filters.ts +++ b/packages/dispatcher/src/filters.ts @@ -702,28 +702,20 @@ export namespace filters { msg.media?.type === 'sticker' /** - * Filter messages containing a regular sticker (not animated/webm) + * Filter messages containing a sticker by its type */ - export const regularSticker: UpdateFilter = ( - msg - ) => - msg.media?.type === 'sticker' && - !msg.media.isAnimated && - !msg.media.isVideoSticker + export const stickerByType = + (type: Sticker.Type): UpdateFilter => + (msg) => + msg.media?.type === 'sticker' && msg.media.stickerType === type /** - * Filter messages containing an animated sticker + * Filter messages containing a sticker by its source file type */ - export const animatedSticker: UpdateFilter = ( - msg - ) => msg.media?.type === 'sticker' && msg.media.isAnimated - - /** - * Filter messages containing a video (webm) sticker - */ - export const videoSticker: UpdateFilter = ( - msg - ) => msg.media?.type === 'sticker' && msg.media.isVideoSticker + export const stickerBySourceType = + (type: Sticker.SourceType): UpdateFilter => + (msg) => + msg.media?.type === 'sticker' && msg.media.sourceType === type /** * Filter messages containing a video.