chore(client)!: improved replied-to message handling
breaking: - `MessageReplyInfo` renamed to `RepliedMessageInfo` - some types were changed
This commit is contained in:
parent
af34f1e5ca
commit
9db9411c27
6 changed files with 250 additions and 92 deletions
|
@ -8,4 +8,5 @@ export * from './message-forward.js'
|
||||||
export * from './message-media.js'
|
export * from './message-media.js'
|
||||||
export * from './message-reactions.js'
|
export * from './message-reactions.js'
|
||||||
export * from './message-replies.js'
|
export * from './message-replies.js'
|
||||||
|
export * from './replied-message.js'
|
||||||
export * from './search-filters.js'
|
export * from './search-filters.js'
|
||||||
|
|
|
@ -5,8 +5,7 @@ import { memoizeGetters } from '../../utils/memoize.js'
|
||||||
import { Chat } from '../peers/chat.js'
|
import { Chat } from '../peers/chat.js'
|
||||||
import { PeersIndex } from '../peers/peers-index.js'
|
import { PeersIndex } from '../peers/peers-index.js'
|
||||||
import { User } from '../peers/user.js'
|
import { User } from '../peers/user.js'
|
||||||
import { MessageEntity } from './message-entity.js'
|
import { _messageMediaFromTl } from './message-media.js'
|
||||||
import { _messageMediaFromTl, MessageMedia } from './message-media.js'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Information about forwarded message origin
|
* Information about forwarded message origin
|
||||||
|
@ -76,74 +75,3 @@ export class MessageForwardInfo {
|
||||||
|
|
||||||
memoizeGetters(MessageForwardInfo, ['sender', 'fromChat'])
|
memoizeGetters(MessageForwardInfo, ['sender', 'fromChat'])
|
||||||
makeInspectable(MessageForwardInfo)
|
makeInspectable(MessageForwardInfo)
|
||||||
|
|
||||||
/**
|
|
||||||
* Information about a message that this message is a reply to
|
|
||||||
*/
|
|
||||||
export class MessageReplyInfo {
|
|
||||||
constructor(
|
|
||||||
readonly raw: tl.RawMessageReplyHeader,
|
|
||||||
readonly _peers: PeersIndex,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/** Whether this message is a reply to another scheduled message */
|
|
||||||
get isScheduled(): boolean {
|
|
||||||
return this.raw.replyToScheduled!
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Whether this message was sent to a forum topic */
|
|
||||||
get isForumTopic(): boolean {
|
|
||||||
return this.raw.forumTopic!
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Whether this message is quoting another message */
|
|
||||||
get isQuote(): boolean {
|
|
||||||
return this.raw.quote!
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If replied-to message is available, its ID
|
|
||||||
*/
|
|
||||||
get id(): number | null {
|
|
||||||
return this.raw.replyToMsgId ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ID of the replies thread where this message belongs to */
|
|
||||||
get threadId(): number | null {
|
|
||||||
return this.raw.replyToTopId ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If replied-to message is available, chat where the message was sent.
|
|
||||||
*
|
|
||||||
* If `null`, the message was sent in the same chat.
|
|
||||||
*/
|
|
||||||
get chat(): Chat | null {
|
|
||||||
return this.raw.replyToPeerId ? Chat._parseFromPeer(this.raw.replyToPeerId, this._peers) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
/** If this message is a quote, text of the quote */
|
|
||||||
get quoteText(): string {
|
|
||||||
return this.raw.quoteText ?? ''
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Message entities contained in the quote */
|
|
||||||
get quoteEntities(): MessageEntity[] {
|
|
||||||
return this.raw.quoteEntities?.map((e) => new MessageEntity(e)) ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Media contained in the replied-to message
|
|
||||||
*
|
|
||||||
* Only available in case the replied-to message is in a different chat
|
|
||||||
* (i.e. {@link chat} is not `null`)
|
|
||||||
*/
|
|
||||||
get media(): MessageMedia {
|
|
||||||
if (!this.raw.replyMedia) return null
|
|
||||||
|
|
||||||
return _messageMediaFromTl(this._peers, this.raw.replyMedia)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
memoizeGetters(MessageReplyInfo, ['chat', 'quoteEntities', 'media'])
|
|
||||||
makeInspectable(MessageReplyInfo)
|
|
||||||
|
|
|
@ -53,25 +53,31 @@ export class MessageRepliesInfo {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ID of the discussion group for the post
|
* ID of the discussion group for the post
|
||||||
|
*
|
||||||
|
* `null` if the post is not a channel post
|
||||||
*/
|
*/
|
||||||
get discussion(): number {
|
get discussion(): number | null {
|
||||||
return getMarkedPeerId(this.raw.channelId!, 'channel')
|
if (!this.raw.channelId) return null
|
||||||
|
|
||||||
|
return getMarkedPeerId(this.raw.channelId, 'channel')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Last few commenters to the post (usually 3)
|
* Last few commenters to the post (usually 3)
|
||||||
*/
|
*/
|
||||||
get repliers(): (User | Chat)[] {
|
get repliers(): (User | Chat)[] {
|
||||||
return this.raw.recentRepliers!.map((it) => {
|
return (
|
||||||
switch (it._) {
|
this.raw.recentRepliers?.map((it) => {
|
||||||
case 'peerUser':
|
switch (it._) {
|
||||||
return new User(this._peers.user(it.userId))
|
case 'peerUser':
|
||||||
case 'peerChannel':
|
return new User(this._peers.user(it.userId))
|
||||||
return new Chat(this._peers.chat(it.channelId))
|
case 'peerChannel':
|
||||||
default:
|
return new Chat(this._peers.chat(it.channelId))
|
||||||
throw new Error('Unexpected peer type: ' + it._)
|
default:
|
||||||
}
|
throw new Error('Unexpected peer type: ' + it._)
|
||||||
})
|
}
|
||||||
|
}) ?? []
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,11 @@ import { PeersIndex } from '../peers/peers-index.js'
|
||||||
import { User } from '../peers/user.js'
|
import { User } from '../peers/user.js'
|
||||||
import { _messageActionFromTl, MessageAction } from './message-action.js'
|
import { _messageActionFromTl, MessageAction } from './message-action.js'
|
||||||
import { MessageEntity } from './message-entity.js'
|
import { MessageEntity } from './message-entity.js'
|
||||||
import { MessageForwardInfo, MessageReplyInfo } from './message-forward.js'
|
import { MessageForwardInfo } from './message-forward.js'
|
||||||
import { _messageMediaFromTl, MessageMedia } from './message-media.js'
|
import { _messageMediaFromTl, MessageMedia } from './message-media.js'
|
||||||
import { MessageReactions } from './message-reactions.js'
|
import { MessageReactions } from './message-reactions.js'
|
||||||
import { MessageRepliesInfo } from './message-replies.js'
|
import { MessageRepliesInfo } from './message-replies.js'
|
||||||
|
import { RepliedMessageInfo } from './replied-message.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Telegram message.
|
* A Telegram message.
|
||||||
|
@ -190,10 +191,10 @@ export class Message {
|
||||||
*
|
*
|
||||||
* Mutually exclusive with {@link replyToStory}
|
* Mutually exclusive with {@link replyToStory}
|
||||||
*/
|
*/
|
||||||
get replyToMessage(): MessageReplyInfo | null {
|
get replyToMessage(): RepliedMessageInfo | null {
|
||||||
if (this.raw.replyTo?._ !== 'messageReplyHeader') return null
|
if (this.raw.replyTo?._ !== 'messageReplyHeader') return null
|
||||||
|
|
||||||
return new MessageReplyInfo(this.raw.replyTo, this._peers)
|
return new RepliedMessageInfo(this.raw.replyTo, this._peers)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
204
packages/client/src/types/messages/replied-message.ts
Normal file
204
packages/client/src/types/messages/replied-message.ts
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
import { MtTypeAssertionError, tl } from '@mtcute/core'
|
||||||
|
|
||||||
|
import { makeInspectable } from '../../utils/inspectable.js'
|
||||||
|
import { memoizeGetters } from '../../utils/memoize.js'
|
||||||
|
import { Chat } from '../peers/chat.js'
|
||||||
|
import { PeersIndex } from '../peers/peers-index.js'
|
||||||
|
import { User } from '../peers/user.js'
|
||||||
|
import { MessageEntity } from './message-entity.js'
|
||||||
|
import { _messageMediaFromTl, MessageMedia } from './message-media.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Origin of the replied-to message
|
||||||
|
* - `same_chat` — reply to a message in the same chat as this message
|
||||||
|
* - `other_chat` — reply to a message in a different chat
|
||||||
|
* - `private` — reply to a message in a private chat
|
||||||
|
*/
|
||||||
|
export type RepliedMessageOrigin = 'same_chat' | 'other_chat' | 'private'
|
||||||
|
|
||||||
|
/** @hidden */
|
||||||
|
export interface _RepliedMessageAssertionsByOrigin {
|
||||||
|
same_chat: {
|
||||||
|
id: number
|
||||||
|
chat: null
|
||||||
|
media: null
|
||||||
|
sender: null
|
||||||
|
}
|
||||||
|
other_chat: {
|
||||||
|
id: number
|
||||||
|
chat: Chat
|
||||||
|
sender: User | Chat | string
|
||||||
|
}
|
||||||
|
private: {
|
||||||
|
id: null
|
||||||
|
chat: null
|
||||||
|
sender: User | Chat | string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about a message that this message is a reply to
|
||||||
|
*/
|
||||||
|
export class RepliedMessageInfo {
|
||||||
|
constructor(
|
||||||
|
readonly raw: tl.RawMessageReplyHeader,
|
||||||
|
readonly _peers: PeersIndex,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** Whether this message is a reply to another scheduled message */
|
||||||
|
get isScheduled(): boolean {
|
||||||
|
return this.raw.replyToScheduled!
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether this message was sent to a forum topic */
|
||||||
|
get isForumTopic(): boolean {
|
||||||
|
return this.raw.forumTopic!
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether this message is quoting another message */
|
||||||
|
get isQuote(): boolean {
|
||||||
|
return this.raw.quote!
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Origin of the replied-to message */
|
||||||
|
get origin(): RepliedMessageOrigin {
|
||||||
|
if (!this.raw.replyToMsgId) return 'private'
|
||||||
|
if (!this.raw.replyFrom) return 'same_chat'
|
||||||
|
|
||||||
|
return 'other_chat'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to check {@link origin} that will also
|
||||||
|
* narrow the type of this object respectively
|
||||||
|
*/
|
||||||
|
originIs<T extends RepliedMessageOrigin>(
|
||||||
|
origin: T,
|
||||||
|
): this is RepliedMessageInfo & _RepliedMessageAssertionsByOrigin[T] {
|
||||||
|
if (this.origin === origin) {
|
||||||
|
// do some type assertions just in case
|
||||||
|
switch (origin) {
|
||||||
|
case 'same_chat':
|
||||||
|
// no need, `null` is pretty safe, and `id` is checked in `origin` getter
|
||||||
|
return true
|
||||||
|
case 'other_chat':
|
||||||
|
// replyToMsgId != null, replyFrom != null. checking for replyToPeerId is enough
|
||||||
|
return this.raw.replyToPeerId !== undefined
|
||||||
|
case 'private':
|
||||||
|
// replyFrom.fromId should be available
|
||||||
|
return this.raw.replyFrom?.fromId !== undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For non-`private` origin, ID of the replied-to message in the original chat.
|
||||||
|
*/
|
||||||
|
get id(): number | null {
|
||||||
|
return this.raw.replyToMsgId ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ID of the replies thread where this message belongs to */
|
||||||
|
get threadId(): number | null {
|
||||||
|
return this.raw.replyToTopId ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If replied-to message is available, chat where the message was sent.
|
||||||
|
*
|
||||||
|
* If `null`, the message was sent in the same chat.
|
||||||
|
*/
|
||||||
|
get chat(): Chat | null {
|
||||||
|
if (!this.raw.replyToPeerId || !this.raw.replyFrom) {
|
||||||
|
// same chat or private. even if `replyToPeerId` is available,
|
||||||
|
// without `replyFrom` it would contain the sender, not the chat
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return Chat._parseFromPeer(this.raw.replyToPeerId, this._peers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sender of the replied-to message (either user or a channel)
|
||||||
|
* or their name (for users with private forwards).
|
||||||
|
*
|
||||||
|
* For replies to channel messages, this will be the channel itself.
|
||||||
|
*
|
||||||
|
* `null` if the sender is not available (for `same_chat` origin)
|
||||||
|
*/
|
||||||
|
get sender(): User | Chat | string | null {
|
||||||
|
const { replyFrom, replyToPeerId } = this.raw
|
||||||
|
if (!replyFrom && !replyToPeerId) return null
|
||||||
|
|
||||||
|
if (replyFrom?.fromName) {
|
||||||
|
return replyFrom.fromName
|
||||||
|
}
|
||||||
|
|
||||||
|
const peer = replyFrom?.fromId ?? replyToPeerId
|
||||||
|
|
||||||
|
if (peer) {
|
||||||
|
switch (peer._) {
|
||||||
|
case 'peerChannel':
|
||||||
|
return new Chat(this._peers.chat(peer.channelId))
|
||||||
|
case 'peerUser':
|
||||||
|
return new User(this._peers.user(peer.userId))
|
||||||
|
default:
|
||||||
|
throw new MtTypeAssertionError('fromId ?? replyToPeerId', 'peerUser | peerChannel', peer._)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new MtTypeAssertionError('replyFrom', 'to have fromId, replyToPeerId or fromName', 'neither')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For non-`same_chat` origin, date the original message was sent.
|
||||||
|
*/
|
||||||
|
get date(): Date | null {
|
||||||
|
if (!this.raw.replyFrom?.date) return null
|
||||||
|
|
||||||
|
return new Date(this.raw.replyFrom.date * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this message is a quote, text of the quote.
|
||||||
|
*
|
||||||
|
* For non-`same_chat` origin, this will be the full text of the
|
||||||
|
* replied-to message in case `.isQuote` is `false`
|
||||||
|
*/
|
||||||
|
get quoteText(): string {
|
||||||
|
return this.raw.quoteText ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Message entities contained in the quote */
|
||||||
|
get quoteEntities(): MessageEntity[] {
|
||||||
|
return this.raw.quoteEntities?.map((e) => new MessageEntity(e)) ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offset of the start of the {@link quoteText} in the replied-to message.
|
||||||
|
*
|
||||||
|
* `null` if not available, in which case it should be assumed that the quote
|
||||||
|
* starts at `.indexOf(quoteText)` of the replied-to message text.
|
||||||
|
*/
|
||||||
|
get quoteOffset(): number | null {
|
||||||
|
if (!this.raw.quoteOffset) return null
|
||||||
|
|
||||||
|
return this.raw.quoteOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Media contained in the replied-to message
|
||||||
|
*
|
||||||
|
* Only available for non-`same_chat` origin
|
||||||
|
*/
|
||||||
|
get media(): MessageMedia {
|
||||||
|
if (!this.raw.replyMedia) return null
|
||||||
|
|
||||||
|
return _messageMediaFromTl(this._peers, this.raw.replyMedia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memoizeGetters(RepliedMessageInfo, ['chat', 'sender', 'quoteEntities', 'media'])
|
||||||
|
makeInspectable(RepliedMessageInfo)
|
|
@ -1,14 +1,16 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
// ^^ will be looked into in MTQ-29
|
// ^^ will be looked into in MTQ-29
|
||||||
import {
|
import {
|
||||||
|
_RepliedMessageAssertionsByOrigin,
|
||||||
Chat,
|
Chat,
|
||||||
MaybeArray,
|
MaybeArray,
|
||||||
Message,
|
Message,
|
||||||
MessageAction,
|
MessageAction,
|
||||||
MessageMediaType,
|
MessageMediaType,
|
||||||
MessageReplyInfo,
|
|
||||||
RawDocument,
|
RawDocument,
|
||||||
RawLocation,
|
RawLocation,
|
||||||
|
RepliedMessageInfo,
|
||||||
|
RepliedMessageOrigin,
|
||||||
Sticker,
|
Sticker,
|
||||||
StickerSourceType,
|
StickerSourceType,
|
||||||
StickerType,
|
StickerType,
|
||||||
|
@ -36,7 +38,22 @@ export const outgoing: UpdateFilter<Message, { isOutgoing: true }> = (msg) => ms
|
||||||
/**
|
/**
|
||||||
* Filter messages that are replies to some other message
|
* Filter messages that are replies to some other message
|
||||||
*/
|
*/
|
||||||
export const reply: UpdateFilter<Message, { replyToMessage: MessageReplyInfo }> = (msg) => msg.replyToMessage !== null
|
export const reply: UpdateFilter<Message, { replyToMessage: RepliedMessageInfo }> = (msg) => msg.replyToMessage !== null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter messages that are replies with the given origin type
|
||||||
|
*/
|
||||||
|
export const replyOrigin =
|
||||||
|
<T extends RepliedMessageOrigin>(
|
||||||
|
origin: T,
|
||||||
|
): UpdateFilter<
|
||||||
|
Message,
|
||||||
|
{
|
||||||
|
replyToMessage: Modify<RepliedMessageInfo, _RepliedMessageAssertionsByOrigin[T] & { origin: T }>
|
||||||
|
}
|
||||||
|
> =>
|
||||||
|
(msg) =>
|
||||||
|
msg.replyToMessage?.originIs(origin) ?? false // originIs does additional checks
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter messages containing some media
|
* Filter messages containing some media
|
||||||
|
@ -74,7 +91,7 @@ export const liveLocation = mediaOf('live_location')
|
||||||
/** Filter messages containing a game */
|
/** Filter messages containing a game */
|
||||||
export const game = mediaOf('game')
|
export const game = mediaOf('game')
|
||||||
/** Filter messages containing a web page */
|
/** Filter messages containing a web page */
|
||||||
export const webpage = mediaOf('web_page')
|
export const webpage = mediaOf('webpage')
|
||||||
/** Filter messages containing a venue */
|
/** Filter messages containing a venue */
|
||||||
export const venue = mediaOf('venue')
|
export const venue = mediaOf('venue')
|
||||||
/** Filter messages containing a poll */
|
/** Filter messages containing a poll */
|
||||||
|
@ -212,7 +229,8 @@ export const sender =
|
||||||
msg.sender.type === type
|
msg.sender.type === type
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter that matches messages that are replies to some other message.
|
* Filter that matches messages that are replies to some other message that can be fetched
|
||||||
|
* (i.e. not `private` origin, and has not been deleted)
|
||||||
*
|
*
|
||||||
* Optionally, you can pass a filter that will be applied to the replied message.
|
* Optionally, you can pass a filter that will be applied to the replied message.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in a new issue