feat(client): support custom emojis
This commit is contained in:
parent
e39057bda5
commit
eaa517a5c3
11 changed files with 204 additions and 77 deletions
|
@ -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
|
||||
|
|
|
@ -57,6 +57,7 @@ import {
|
|||
ChatJoinRequestUpdate,
|
||||
PeerReaction,
|
||||
MessageReactions,
|
||||
Sticker
|
||||
} from '../types'
|
||||
|
||||
// @copy
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
37
packages/client/src/methods/stickers/get-custom-emojis.ts
Normal file
37
packages/client/src/methods/stickers/get-custom-emojis.ts
Normal 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
|
||||
})
|
||||
}
|
|
@ -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':
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue