chore(client)!: improved replied-to message handling

breaking:
  - `MessageReplyInfo` renamed to `RepliedMessageInfo`
  - some types were changed
This commit is contained in:
alina 🌸 2023-12-02 07:56:13 +03:00
parent af34f1e5ca
commit 9db9411c27
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
6 changed files with 250 additions and 92 deletions

View file

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

View file

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

View file

@ -53,25 +53,31 @@ 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) => {
switch (it._) {
case 'peerUser':
return new User(this._peers.user(it.userId))
case 'peerChannel':
return new Chat(this._peers.chat(it.channelId))
default:
throw new Error('Unexpected peer type: ' + it._)
}
})
return (
this.raw.recentRepliers?.map((it) => {
switch (it._) {
case 'peerUser':
return new User(this._peers.user(it.userId))
case 'peerChannel':
return new Chat(this._peers.chat(it.channelId))
default:
throw new Error('Unexpected peer type: ' + it._)
}
}) ?? []
)
}
}

View file

@ -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)
}
/**

View 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)

View file

@ -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.
*/