diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 8b2a9138..7cb47924 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -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 /** * 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 diff --git a/packages/client/src/methods/messages/get-discussion-message.ts b/packages/client/src/methods/messages/get-discussion-message.ts new file mode 100644 index 00000000..0bba7393 --- /dev/null +++ b/packages/client/src/methods/messages/get-discussion-message.ts @@ -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 + ] +} diff --git a/packages/client/src/methods/messages/send-media-group.ts b/packages/client/src/methods/messages/send-media-group.ts index fa5ca4d6..26dc1b8b 100644 --- a/packages/client/src/methods/messages/send-media-group.ts +++ b/packages/client/src/methods/messages/send-media-group.ts @@ -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 { 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, diff --git a/packages/client/src/methods/messages/send-media.ts b/packages/client/src/methods/messages/send-media.ts index 1f71823b..3595ac6a 100644 --- a/packages/client/src/methods/messages/send-media.ts +++ b/packages/client/src/methods/messages/send-media.ts @@ -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, diff --git a/packages/client/src/methods/messages/send-text.ts b/packages/client/src/methods/messages/send-text.ts index 65d3fa04..4d1fbe63 100644 --- a/packages/client/src/methods/messages/send-text.ts +++ b/packages/client/src/methods/messages/send-text.ts @@ -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, diff --git a/packages/client/src/methods/messages/send-typing.ts b/packages/client/src/methods/messages/send-typing.ts index 6a45085e..ee717481 100644 --- a/packages/client/src/methods/messages/send-typing.ts +++ b/packages/client/src/methods/messages/send-typing.ts @@ -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 { 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 }) } diff --git a/packages/client/src/types/messages/message.ts b/packages/client/src/types/messages/message.ts index 2c73e7d3..1e34fdba 100644 --- a/packages/client/src/types/messages/message.ts +++ b/packages/client/src/types/messages/message.ts @@ -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 { + /** + * 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[2] + ): ReturnType { + 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[2] + ): ReturnType { + 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. * diff --git a/packages/client/src/utils/misc-utils.ts b/packages/client/src/utils/misc-utils.ts index 0a00a134..9279b67c 100644 --- a/packages/client/src/utils/misc-utils.ts +++ b/packages/client/src/utils/misc-utils.ts @@ -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 +}