feat(client): support custom emojis

This commit is contained in:
teidesu 2022-08-18 19:52:24 +03:00
parent e39057bda5
commit eaa517a5c3
11 changed files with 204 additions and 77 deletions

View file

@ -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<StickerSet>
/**
* Get custom emoji stickers by their IDs
*
* @param ids IDs of the stickers (as defined in {@link MessageEntity.emojiId})
*/
getCustomEmojis(ids: tl.Long[]): Promise<Sticker[]>
/**
* Get a list of all installed sticker packs
*
@ -3933,7 +3945,7 @@ export interface TelegramClient extends BaseTelegramClient {
*/
updateUsername(username: string | null): Promise<User>
}
/** @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

View file

@ -57,6 +57,7 @@ import {
ChatJoinRequestUpdate,
PeerReaction,
MessageReactions,
Sticker
} from '../types'
// @copy

View file

@ -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
}

View file

@ -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<StickerSet> {
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,

View file

@ -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<Sticker[]> {
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
})
}

View file

@ -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':

View file

@ -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) {

View file

@ -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,
}
}
}

View file

@ -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<Sticker[]> {
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'])

View file

@ -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`

View file

@ -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<Message, { media: Sticker }> = (
msg
) =>
msg.media?.type === 'sticker' &&
!msg.media.isAnimated &&
!msg.media.isVideoSticker
export const stickerByType =
(type: Sticker.Type): UpdateFilter<Message, { media: Sticker }> =>
(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<Message, { media: Sticker }> = (
msg
) => msg.media?.type === 'sticker' && msg.media.isAnimated
/**
* Filter messages containing a video (webm) sticker
*/
export const videoSticker: UpdateFilter<Message, { media: Sticker }> = (
msg
) => msg.media?.type === 'sticker' && msg.media.isVideoSticker
export const stickerBySourceType =
(type: Sticker.SourceType): UpdateFilter<Message, { media: Sticker }> =>
(msg) =>
msg.media?.type === 'sticker' && msg.media.sourceType === type
/**
* Filter messages containing a video.