feat(client): support comments and reply threads

This commit is contained in:
teidesu 2021-05-27 15:57:05 +03:00
parent a0294b9a64
commit d50a25eab9
8 changed files with 292 additions and 37 deletions

View file

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

View file

@ -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
]
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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