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-reactions.js'
|
||||
export * from './message-replies.js'
|
||||
export * from './replied-message.js'
|
||||
export * from './search-filters.js'
|
||||
|
|
|
@ -5,8 +5,7 @@ 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'
|
||||
import { _messageMediaFromTl } from './message-media.js'
|
||||
|
||||
/**
|
||||
* Information about forwarded message origin
|
||||
|
@ -76,74 +75,3 @@ export class MessageForwardInfo {
|
|||
|
||||
memoizeGetters(MessageForwardInfo, ['sender', 'fromChat'])
|
||||
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,16 +53,21 @@ export class MessageRepliesInfo {
|
|||
|
||||
/**
|
||||
* ID of the discussion group for the post
|
||||
*
|
||||
* `null` if the post is not a channel post
|
||||
*/
|
||||
get discussion(): number {
|
||||
return getMarkedPeerId(this.raw.channelId!, 'channel')
|
||||
get discussion(): number | null {
|
||||
if (!this.raw.channelId) return null
|
||||
|
||||
return getMarkedPeerId(this.raw.channelId, 'channel')
|
||||
}
|
||||
|
||||
/**
|
||||
* Last few commenters to the post (usually 3)
|
||||
*/
|
||||
get repliers(): (User | Chat)[] {
|
||||
return this.raw.recentRepliers!.map((it) => {
|
||||
return (
|
||||
this.raw.recentRepliers?.map((it) => {
|
||||
switch (it._) {
|
||||
case 'peerUser':
|
||||
return new User(this._peers.user(it.userId))
|
||||
|
@ -71,7 +76,8 @@ export class MessageRepliesInfo {
|
|||
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 { _messageActionFromTl, MessageAction } from './message-action.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 { MessageReactions } from './message-reactions.js'
|
||||
import { MessageRepliesInfo } from './message-replies.js'
|
||||
import { RepliedMessageInfo } from './replied-message.js'
|
||||
|
||||
/**
|
||||
* A Telegram message.
|
||||
|
@ -190,10 +191,10 @@ export class Message {
|
|||
*
|
||||
* Mutually exclusive with {@link replyToStory}
|
||||
*/
|
||||
get replyToMessage(): MessageReplyInfo | null {
|
||||
get replyToMessage(): RepliedMessageInfo | 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 */
|
||||
// ^^ will be looked into in MTQ-29
|
||||
import {
|
||||
_RepliedMessageAssertionsByOrigin,
|
||||
Chat,
|
||||
MaybeArray,
|
||||
Message,
|
||||
MessageAction,
|
||||
MessageMediaType,
|
||||
MessageReplyInfo,
|
||||
RawDocument,
|
||||
RawLocation,
|
||||
RepliedMessageInfo,
|
||||
RepliedMessageOrigin,
|
||||
Sticker,
|
||||
StickerSourceType,
|
||||
StickerType,
|
||||
|
@ -36,7 +38,22 @@ export const outgoing: UpdateFilter<Message, { isOutgoing: true }> = (msg) => ms
|
|||
/**
|
||||
* 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
|
||||
|
@ -74,7 +91,7 @@ export const liveLocation = mediaOf('live_location')
|
|||
/** Filter messages containing a game */
|
||||
export const game = mediaOf('game')
|
||||
/** Filter messages containing a web page */
|
||||
export const webpage = mediaOf('web_page')
|
||||
export const webpage = mediaOf('webpage')
|
||||
/** Filter messages containing a venue */
|
||||
export const venue = mediaOf('venue')
|
||||
/** Filter messages containing a poll */
|
||||
|
@ -212,7 +229,8 @@ export const sender =
|
|||
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.
|
||||
*/
|
||||
|
|
Loading…
Reference in a new issue