feat(client): support comments and reply threads
This commit is contained in:
parent
a0294b9a64
commit
d50a25eab9
8 changed files with 292 additions and 37 deletions
|
@ -89,6 +89,7 @@ import { editInlineMessage } from './methods/messages/edit-inline-message'
|
|||
import { editMessage } from './methods/messages/edit-message'
|
||||
import { _findMessageInUpdate } from './methods/messages/find-in-update'
|
||||
import { forwardMessages } from './methods/messages/forward-messages'
|
||||
import { _getDiscussionMessage } from './methods/messages/get-discussion-message'
|
||||
import { getHistory } from './methods/messages/get-history'
|
||||
import { getMessageGroup } from './methods/messages/get-message-group'
|
||||
import { getMessages } from './methods/messages/get-messages'
|
||||
|
@ -2275,6 +2276,13 @@ export interface TelegramClient extends BaseTelegramClient {
|
|||
*/
|
||||
replyTo?: number | Message
|
||||
|
||||
/**
|
||||
* Message to comment to. Either a message object or message ID.
|
||||
*
|
||||
* This overwrites `replyTo` if it was passed
|
||||
*/
|
||||
commentTo?: number | Message
|
||||
|
||||
/**
|
||||
* Parse mode to use to parse entities before sending
|
||||
* the message. Defaults to current default parse mode (if any).
|
||||
|
@ -2346,6 +2354,13 @@ export interface TelegramClient extends BaseTelegramClient {
|
|||
*/
|
||||
replyTo?: number | Message
|
||||
|
||||
/**
|
||||
* Message to comment to. Either a message object or message ID.
|
||||
*
|
||||
* This overwrites `replyTo` if it was passed
|
||||
*/
|
||||
commentTo?: number | Message
|
||||
|
||||
/**
|
||||
* Parse mode to use to parse entities before sending
|
||||
* the message. Defaults to current default parse mode (if any).
|
||||
|
@ -2408,6 +2423,13 @@ export interface TelegramClient extends BaseTelegramClient {
|
|||
*/
|
||||
replyTo?: number | Message
|
||||
|
||||
/**
|
||||
* Message to comment to. Either a message object or message ID.
|
||||
*
|
||||
* This overwrites `replyTo` if it was passed
|
||||
*/
|
||||
commentTo?: number | Message
|
||||
|
||||
/**
|
||||
* Parse mode to use to parse entities before sending
|
||||
* the message. Defaults to current default parse mode (if any).
|
||||
|
@ -2467,12 +2489,22 @@ export interface TelegramClient extends BaseTelegramClient {
|
|||
*
|
||||
* @param chatId Chat ID
|
||||
* @param status (default: `'typing'`) Typing status
|
||||
* @param progress (default: `0`) For `upload_*` and history import actions, progress of the upload
|
||||
* @param params
|
||||
*/
|
||||
sendTyping(
|
||||
chatId: InputPeerLike,
|
||||
status?: TypingStatus | tl.TypeSendMessageAction,
|
||||
progress?: number
|
||||
params?: {
|
||||
/**
|
||||
* For `upload_*` and history import actions, progress of the upload
|
||||
*/
|
||||
progress?: number
|
||||
|
||||
/**
|
||||
* For comment threads, ID of the thread (i.e. top message)
|
||||
*/
|
||||
threadId?: number
|
||||
}
|
||||
): Promise<void>
|
||||
/**
|
||||
* Send or retract a vote in a poll.
|
||||
|
@ -3118,6 +3150,7 @@ export class TelegramClient extends BaseTelegramClient {
|
|||
editMessage = editMessage
|
||||
protected _findMessageInUpdate = _findMessageInUpdate
|
||||
forwardMessages = forwardMessages
|
||||
protected _getDiscussionMessage = _getDiscussionMessage
|
||||
getHistory = getHistory
|
||||
getMessageGroup = getMessageGroup
|
||||
getMessages = getMessages
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { TelegramClient } from '../../client'
|
||||
import { InputPeerLike } from '../../types'
|
||||
import { tl } from '@mtcute/tl'
|
||||
|
||||
/** @internal */
|
||||
export async function _getDiscussionMessage(
|
||||
this: TelegramClient,
|
||||
peer: InputPeerLike,
|
||||
message: number
|
||||
): Promise<[tl.TypeInputPeer, number]> {
|
||||
const inputPeer = await this.resolvePeer(peer)
|
||||
|
||||
const res = await this.call({
|
||||
_: 'messages.getDiscussionMessage',
|
||||
peer: inputPeer,
|
||||
msgId: message,
|
||||
})
|
||||
|
||||
if (!res.messages.length || res.messages[0]._ === 'messageEmpty')
|
||||
// no discussion message (i guess?), return the same msg
|
||||
return [inputPeer, message]
|
||||
|
||||
const msg = res.messages[0]
|
||||
const chat = res.chats.find(
|
||||
(it) => it.id === (msg.peerId as tl.RawPeerChannel).channelId
|
||||
)! as tl.RawChannel
|
||||
|
||||
return [
|
||||
{
|
||||
_: 'inputPeerChannel',
|
||||
channelId: chat.id,
|
||||
accessHash: chat.accessHash!
|
||||
},
|
||||
msg.id
|
||||
]
|
||||
}
|
|
@ -6,7 +6,7 @@ import {
|
|||
Message,
|
||||
ReplyMarkup,
|
||||
} from '../../types'
|
||||
import { normalizeDate, randomUlong } from '../../utils/misc-utils'
|
||||
import { normalizeDate, normalizeMessageId, randomUlong } from '../../utils/misc-utils'
|
||||
import { tl } from '@mtcute/tl'
|
||||
import { assertIsUpdatesGroup } from '../../utils/updates-utils'
|
||||
import { createUsersChatsIndex } from '../../utils/peer-utils'
|
||||
|
@ -30,6 +30,13 @@ export async function sendMediaGroup(
|
|||
*/
|
||||
replyTo?: number | Message
|
||||
|
||||
/**
|
||||
* Message to comment to. Either a message object or message ID.
|
||||
*
|
||||
* This overwrites `replyTo` if it was passed
|
||||
*/
|
||||
commentTo?: number | Message
|
||||
|
||||
/**
|
||||
* Parse mode to use to parse entities before sending
|
||||
* the message. Defaults to current default parse mode (if any).
|
||||
|
@ -83,9 +90,14 @@ export async function sendMediaGroup(
|
|||
): Promise<Message[]> {
|
||||
if (!params) params = {}
|
||||
|
||||
const peer = await this.resolvePeer(chatId)
|
||||
let peer = await this.resolvePeer(chatId)
|
||||
const replyMarkup = BotKeyboard._convertToTl(params.replyMarkup)
|
||||
|
||||
let replyTo = normalizeMessageId(params.replyTo)
|
||||
if (params.commentTo) {
|
||||
;[peer, replyTo] = await this._getDiscussionMessage(peer, normalizeMessageId(params.commentTo)!)
|
||||
}
|
||||
|
||||
const multiMedia: tl.RawInputSingleMedia[] = []
|
||||
|
||||
for (let i = 0; i < medias.length; i++) {
|
||||
|
@ -117,11 +129,7 @@ export async function sendMediaGroup(
|
|||
peer,
|
||||
multiMedia,
|
||||
silent: params.silent,
|
||||
replyToMsgId: params.replyTo
|
||||
? typeof params.replyTo === 'number'
|
||||
? params.replyTo
|
||||
: params.replyTo.id
|
||||
: undefined,
|
||||
replyToMsgId: replyTo,
|
||||
randomId: randomUlong(),
|
||||
scheduleDate: normalizeDate(params.schedule),
|
||||
replyMarkup,
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
Message,
|
||||
ReplyMarkup,
|
||||
} from '../../types'
|
||||
import { normalizeDate, randomUlong } from '../../utils/misc-utils'
|
||||
import { normalizeDate, normalizeMessageId, randomUlong } from '../../utils/misc-utils'
|
||||
|
||||
/**
|
||||
* Send a single media (a photo or a document-based media)
|
||||
|
@ -30,6 +30,13 @@ export async function sendMedia(
|
|||
*/
|
||||
replyTo?: number | Message
|
||||
|
||||
/**
|
||||
* Message to comment to. Either a message object or message ID.
|
||||
*
|
||||
* This overwrites `replyTo` if it was passed
|
||||
*/
|
||||
commentTo?: number | Message
|
||||
|
||||
/**
|
||||
* Parse mode to use to parse entities before sending
|
||||
* the message. Defaults to current default parse mode (if any).
|
||||
|
@ -96,19 +103,20 @@ export async function sendMedia(
|
|||
(media as any).entities
|
||||
)
|
||||
|
||||
const peer = await this.resolvePeer(chatId)
|
||||
let peer = await this.resolvePeer(chatId)
|
||||
const replyMarkup = BotKeyboard._convertToTl(params.replyMarkup)
|
||||
|
||||
let replyTo = normalizeMessageId(params.replyTo)
|
||||
if (params.commentTo) {
|
||||
;[peer, replyTo] = await this._getDiscussionMessage(peer, normalizeMessageId(params.commentTo)!)
|
||||
}
|
||||
|
||||
const res = await this.call({
|
||||
_: 'messages.sendMedia',
|
||||
peer,
|
||||
media: inputMedia,
|
||||
silent: params.silent,
|
||||
replyToMsgId: params.replyTo
|
||||
? typeof params.replyTo === 'number'
|
||||
? params.replyTo
|
||||
: params.replyTo.id
|
||||
: undefined,
|
||||
replyToMsgId: replyTo,
|
||||
randomId: randomUlong(),
|
||||
scheduleDate: normalizeDate(params.schedule),
|
||||
replyMarkup,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { TelegramClient } from '../../client'
|
||||
import { tl } from '@mtcute/tl'
|
||||
import { inputPeerToPeer } from '../../utils/peer-utils'
|
||||
import { normalizeDate, randomUlong } from '../../utils/misc-utils'
|
||||
import { normalizeDate, normalizeMessageId, randomUlong } from '../../utils/misc-utils'
|
||||
import { InputPeerLike, Message, BotKeyboard, ReplyMarkup } from '../../types'
|
||||
|
||||
/**
|
||||
|
@ -22,6 +22,13 @@ export async function sendText(
|
|||
*/
|
||||
replyTo?: number | Message
|
||||
|
||||
/**
|
||||
* Message to comment to. Either a message object or message ID.
|
||||
*
|
||||
* This overwrites `replyTo` if it was passed
|
||||
*/
|
||||
commentTo?: number | Message
|
||||
|
||||
/**
|
||||
* Parse mode to use to parse entities before sending
|
||||
* the message. Defaults to current default parse mode (if any).
|
||||
|
@ -79,19 +86,20 @@ export async function sendText(
|
|||
params.entities
|
||||
)
|
||||
|
||||
const peer = await this.resolvePeer(chatId)
|
||||
let peer = await this.resolvePeer(chatId)
|
||||
const replyMarkup = BotKeyboard._convertToTl(params.replyMarkup)
|
||||
|
||||
let replyTo = normalizeMessageId(params.replyTo)
|
||||
if (params.commentTo) {
|
||||
;[peer, replyTo] = await this._getDiscussionMessage(peer, normalizeMessageId(params.commentTo)!)
|
||||
}
|
||||
|
||||
const res = await this.call({
|
||||
_: 'messages.sendMessage',
|
||||
peer,
|
||||
noWebpage: params.disableWebPreview,
|
||||
silent: params.silent,
|
||||
replyToMsgId: params.replyTo
|
||||
? typeof params.replyTo === 'number'
|
||||
? params.replyTo
|
||||
: params.replyTo.id
|
||||
: undefined,
|
||||
replyToMsgId: replyTo,
|
||||
randomId: randomUlong(),
|
||||
scheduleDate: normalizeDate(params.schedule),
|
||||
replyMarkup,
|
||||
|
|
|
@ -12,16 +12,27 @@ import { InputPeerLike, TypingStatus } from '../../types'
|
|||
*
|
||||
* @param chatId Chat ID
|
||||
* @param status Typing status
|
||||
* @param progress For `upload_*` and history import actions, progress of the upload
|
||||
* @param params
|
||||
* @internal
|
||||
*/
|
||||
export async function sendTyping(
|
||||
this: TelegramClient,
|
||||
chatId: InputPeerLike,
|
||||
status: TypingStatus | tl.TypeSendMessageAction = 'typing',
|
||||
progress = 0
|
||||
params?: {
|
||||
/**
|
||||
* For `upload_*` and history import actions, progress of the upload
|
||||
*/
|
||||
progress?: number
|
||||
|
||||
/**
|
||||
* For comment threads, ID of the thread (i.e. top message)
|
||||
*/
|
||||
threadId?: number
|
||||
}
|
||||
): Promise<void> {
|
||||
if (typeof status === 'string') {
|
||||
const progress = params?.progress ?? 0
|
||||
switch (status) {
|
||||
case 'typing':
|
||||
status = { _: 'sendMessageTypingAction' }
|
||||
|
@ -74,6 +85,7 @@ export async function sendTyping(
|
|||
await this.call({
|
||||
_: 'messages.setTyping',
|
||||
peer: await this.resolvePeer(chatId),
|
||||
action: status
|
||||
action: status,
|
||||
topMsgId: params?.threadId
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { User, Chat, InputPeerLike, UsersIndex, ChatsIndex } from '../peers'
|
||||
import { tl } from '@mtcute/tl'
|
||||
import { BotKeyboard, ReplyMarkup } from '../bots'
|
||||
import { MAX_CHANNEL_ID } from '@mtcute/core'
|
||||
import { getMarkedPeerId, MAX_CHANNEL_ID } from '@mtcute/core'
|
||||
import {
|
||||
MtCuteArgumentError,
|
||||
MtCuteEmptyError,
|
||||
|
@ -43,6 +43,53 @@ export namespace Message {
|
|||
*/
|
||||
signature?: string
|
||||
}
|
||||
|
||||
/** Information about replies to a message */
|
||||
export interface MessageRepliesInfo {
|
||||
/**
|
||||
* Whether this is a comments thread under a channel post
|
||||
*/
|
||||
isComments: false
|
||||
|
||||
/**
|
||||
* Total number of replies
|
||||
*/
|
||||
count: number
|
||||
|
||||
/**
|
||||
* Whether this reply thread has unread messages
|
||||
*/
|
||||
hasUnread: boolean
|
||||
|
||||
/**
|
||||
* ID of the last message in the thread (if any)
|
||||
*/
|
||||
lastMessageId?: number
|
||||
|
||||
/**
|
||||
* ID of the last read message in the thread (if any)
|
||||
*/
|
||||
lastReadMessageId?: number
|
||||
}
|
||||
|
||||
/** Information about comments to a channel post */
|
||||
export interface MessageCommentsInfo
|
||||
extends Omit<MessageRepliesInfo, 'isComments'> {
|
||||
/**
|
||||
* Whether this is a comments thread under a channel post
|
||||
*/
|
||||
isComments: true
|
||||
|
||||
/**
|
||||
* ID of the discussion group for the post
|
||||
*/
|
||||
discussion: number
|
||||
|
||||
/**
|
||||
* IDs of the last few commenters to the post
|
||||
*/
|
||||
repliers: number[]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -294,6 +341,39 @@ export class Message {
|
|||
return this._forward
|
||||
}
|
||||
|
||||
private _replies?: Message.MessageRepliesInfo | Message.MessageCommentsInfo
|
||||
/**
|
||||
* Information about comments (for channels) or replies (for groups)
|
||||
*/
|
||||
get replies():
|
||||
| Message.MessageRepliesInfo
|
||||
| Message.MessageCommentsInfo
|
||||
| null {
|
||||
if (this.raw._ !== 'message' || !this.raw.replies) return null
|
||||
|
||||
if (!this._replies) {
|
||||
const r = this.raw.replies
|
||||
const obj: Message.MessageRepliesInfo = {
|
||||
isComments: r.comments as false,
|
||||
count: r.replies,
|
||||
hasUnread: r.readMaxId !== undefined && r.readMaxId !== r.maxId,
|
||||
lastMessageId: r.maxId,
|
||||
lastReadMessageId: r.readMaxId,
|
||||
}
|
||||
|
||||
if (r.comments) {
|
||||
const o = (obj as unknown) as Message.MessageCommentsInfo
|
||||
o.discussion = r.channelId!
|
||||
o.repliers =
|
||||
r.recentRepliers?.map((it) => getMarkedPeerId(it)) ?? []
|
||||
}
|
||||
|
||||
this._replies = obj
|
||||
}
|
||||
|
||||
return this._replies
|
||||
}
|
||||
|
||||
/**
|
||||
* For replies, the ID of the message that current message
|
||||
* replies to.
|
||||
|
@ -555,6 +635,69 @@ export class Message {
|
|||
return this.client.sendMedia(this.chat.inputPeer, media, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a text-only comment to this message.
|
||||
*
|
||||
* If this is a normal message (not a channel post),
|
||||
* a simple reply will be sent.
|
||||
*
|
||||
* If this post does not have comments section,
|
||||
* {@link MtCuteArgumentError} is thrown. To check
|
||||
* if a message has comments, use {@link replies}
|
||||
*
|
||||
* @param text Text of the message
|
||||
* @param params
|
||||
*/
|
||||
commentText(
|
||||
text: string,
|
||||
params?: Parameters<TelegramClient['sendText']>[2]
|
||||
): ReturnType<TelegramClient['sendText']> {
|
||||
if (this.chat.type !== 'channel') {
|
||||
return this.replyText(text, true, params)
|
||||
}
|
||||
|
||||
if (!this.replies || !this.replies.isComments) {
|
||||
throw new MtCuteArgumentError(
|
||||
'This message does not have comments section'
|
||||
)
|
||||
}
|
||||
|
||||
if (!params) params = {}
|
||||
params.commentTo = this.id
|
||||
return this.client.sendText(this.chat.inputPeer, text, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a media comment to this message
|
||||
* .
|
||||
* If this is a normal message (not a channel post),
|
||||
* a simple reply will be sent.
|
||||
*
|
||||
* If this post does not have comments section,
|
||||
* {@link MtCuteArgumentError} is thrown. To check
|
||||
* if a message has comments, use {@link replies}
|
||||
*
|
||||
* @param media Media to send
|
||||
* @param params
|
||||
*/
|
||||
commentMedia(
|
||||
media: InputMediaLike,
|
||||
params?: Parameters<TelegramClient['sendMedia']>[2]
|
||||
): ReturnType<TelegramClient['sendMedia']> {
|
||||
if (this.chat.type !== 'channel') {
|
||||
return this.replyMedia(media, true, params)
|
||||
}
|
||||
|
||||
if (!this.replies || !this.replies.isComments) {
|
||||
throw new MtCuteArgumentError(
|
||||
'This message does not have comments section'
|
||||
)
|
||||
}
|
||||
if (!params) params = {}
|
||||
params.commentTo = this.id
|
||||
return this.client.sendMedia(this.chat.inputPeer, media, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this message.
|
||||
*
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { MaybeDynamic, MtCuteError } from '../types'
|
||||
import { MaybeDynamic, Message, MtCuteError } from '../types'
|
||||
import { BigInteger } from 'big-integer'
|
||||
import { randomBytes } from '@mtcute/core/src/utils/buffer-utils'
|
||||
import { bufferToBigInt } from '@mtcute/core/src/utils/bigint-utils'
|
||||
|
@ -25,15 +25,16 @@ export function extractChannelIdFromUpdate(
|
|||
upd: tl.TypeUpdate
|
||||
): number | undefined {
|
||||
// holy shit
|
||||
const res = 'channelId' in upd
|
||||
? upd.channelId
|
||||
: 'message' in upd &&
|
||||
typeof upd.message !== 'string' &&
|
||||
'peerId' in upd.message &&
|
||||
upd.message.peerId &&
|
||||
'channelId' in upd.message.peerId
|
||||
? upd.message.peerId.channelId
|
||||
: undefined
|
||||
const res =
|
||||
'channelId' in upd
|
||||
? upd.channelId
|
||||
: 'message' in upd &&
|
||||
typeof upd.message !== 'string' &&
|
||||
'peerId' in upd.message &&
|
||||
upd.message.peerId &&
|
||||
'channelId' in upd.message.peerId
|
||||
? upd.message.peerId.channelId
|
||||
: undefined
|
||||
if (res === 0) return undefined
|
||||
return res
|
||||
}
|
||||
|
@ -45,3 +46,9 @@ export function normalizeDate(
|
|||
? ~~((typeof date === 'number' ? date : date.getTime()) / 1000)
|
||||
: undefined
|
||||
}
|
||||
|
||||
export function normalizeMessageId(
|
||||
msg: Message | number | undefined
|
||||
): number | undefined {
|
||||
return msg ? (typeof msg === 'number' ? msg : msg.id) : undefined
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue