feat(client): support reactions

This commit is contained in:
teidesu 2022-05-12 10:09:37 +03:00
parent 96300a795e
commit 9039830572
9 changed files with 512 additions and 6 deletions

View file

@ -33,9 +33,11 @@ import {
MaybeDynamic,
Message,
MessageMedia,
MessageReactions,
ParsedUpdate,
PartialExcept,
PartialOnly,
PeerReaction,
PeersIndex,
Photo,
Poll,
@ -172,19 +174,23 @@ import {
} from './methods/messages/get-discussion-message'
import { getHistory } from './methods/messages/get-history'
import { getMessageGroup } from './methods/messages/get-message-group'
import { getMessageReactions } from './methods/messages/get-message-reactions'
import { getMessagesUnsafe } from './methods/messages/get-messages-unsafe'
import { getMessages } from './methods/messages/get-messages'
import { getReactionUsers } from './methods/messages/get-reaction-users'
import { getScheduledMessages } from './methods/messages/get-scheduled-messages'
import { iterHistory } from './methods/messages/iter-history'
import { _normalizeInline } from './methods/messages/normalize-inline'
import { _parseEntities } from './methods/messages/parse-entities'
import { pinMessage } from './methods/messages/pin-message'
import { readHistory } from './methods/messages/read-history'
import { readReactions } from './methods/messages/read-reactions'
import { searchGlobal } from './methods/messages/search-global'
import { searchMessages } from './methods/messages/search-messages'
import { sendCopy } from './methods/messages/send-copy'
import { sendMediaGroup } from './methods/messages/send-media-group'
import { sendMedia } from './methods/messages/send-media'
import { sendReaction } from './methods/messages/send-reaction'
import { sendScheduled } from './methods/messages/send-scheduled'
import { sendText } from './methods/messages/send-text'
import { sendTyping } from './methods/messages/send-typing'
@ -2430,6 +2436,36 @@ export interface TelegramClient extends BaseTelegramClient {
* @param message ID of one of the messages in the group
*/
getMessageGroup(chatId: InputPeerLike, message: number): Promise<Message[]>
/**
* Get reactions to a message.
*
* > Apps should short-poll reactions for visible messages
* > (that weren't sent by the user) once every 15-30 seconds,
* > but only if `message.reactions` is set
*
* @param chatId ID of the chat with the message
* @param messages Message ID
* @returns Reactions to the corresponding message, or `null` if there are none
*/
getMessageReactions(
chatId: InputPeerLike,
messages: number
): Promise<MessageReactions | null>
/**
* Get reactions to messages.
*
* > Apps should short-poll reactions for visible messages
* > (that weren't sent by the user) once every 15-30 seconds,
* > but only if `message.reactions` is set
*
* @param chatId ID of the chat with messages
* @param messages Message IDs
* @returns Reactions to corresponding messages, or `null` if there are none
*/
getMessageReactions(
chatId: InputPeerLike,
messages: number[]
): Promise<(MessageReactions | null)[]>
/**
* Get a single message from PM or legacy group by its ID.
* For channels, use {@link getMessages}.
@ -2496,6 +2532,37 @@ export interface TelegramClient extends BaseTelegramClient {
messageIds: number[],
fromReply?: boolean
): Promise<(Message | null)[]>
/**
* Get users who have reacted to the message.
*
* @param chatId Chat ID
* @param messageId Message ID
* @param params
*/
getReactionUsers(
chatId: InputPeerLike,
messageId: number,
params?: {
/**
* Get only reactions with the specified emoji
*/
emoji?: string
/**
* Limit the number of events returned.
*
* Defaults to `Infinity`, i.e. all events are returned
*/
limit?: number
/**
* Chunk size, usually not needed.
*
* Defaults to `100`
*/
chunkSize?: number
}
): AsyncIterableIterator<PeerReaction>
/**
* Get a single scheduled message in chat by its ID
*
@ -2603,6 +2670,12 @@ export interface TelegramClient extends BaseTelegramClient {
message?: number,
clearMentions?: boolean
): Promise<void>
/**
* Mark all reactions in chat as read.
*
* @param chatId Chat ID
*/
readReactions(chatId: InputPeerLike): Promise<void>
/**
* Search for messages globally from all of your chats
*
@ -2979,6 +3052,21 @@ export interface TelegramClient extends BaseTelegramClient {
clearDraft?: boolean
}
): Promise<Message>
/**
* Send or remove a reaction.
*
* @param chatId Chat ID with the message to react to
* @param message Message ID to react to
* @param emoji Reaction emoji (or `null` to remove)
* @param big (default: `false`) Whether to use a big reaction
* @returns Message to which the reaction was sent
*/
sendReaction(
chatId: InputPeerLike,
message: number,
emoji: string | null,
big?: boolean
): Promise<Message>
/**
* Send s previously scheduled message.
*
@ -3891,19 +3979,23 @@ export class TelegramClient extends BaseTelegramClient {
getDiscussionMessage = getDiscussionMessage
getHistory = getHistory
getMessageGroup = getMessageGroup
getMessageReactions = getMessageReactions
getMessagesUnsafe = getMessagesUnsafe
getMessages = getMessages
getReactionUsers = getReactionUsers
getScheduledMessages = getScheduledMessages
iterHistory = iterHistory
protected _normalizeInline = _normalizeInline
protected _parseEntities = _parseEntities
pinMessage = pinMessage
readHistory = readHistory
readReactions = readReactions
searchGlobal = searchGlobal
searchMessages = searchMessages
sendCopy = sendCopy
sendMediaGroup = sendMediaGroup
sendMedia = sendMedia
sendReaction = sendReaction
sendScheduled = sendScheduled
sendText = sendText
sendTyping = sendTyping

View file

@ -55,6 +55,8 @@ import {
BotStoppedUpdate,
BotChatJoinRequestUpdate,
ChatJoinRequestUpdate,
PeerReaction,
MessageReactions,
} from '../types'
// @copy

View file

@ -0,0 +1,95 @@
import { TelegramClient } from '../../client'
import { InputPeerLike, PeersIndex, MessageReactions } from '../../types'
import { assertIsUpdatesGroup } from '../../utils/updates-utils'
import { getMarkedPeerId, MaybeArray } from '@mtcute/core'
import { assertTypeIs } from '../../utils/type-assertion'
/**
* Get reactions to a message.
*
* > Apps should short-poll reactions for visible messages
* > (that weren't sent by the user) once every 15-30 seconds,
* > but only if `message.reactions` is set
*
* @param chatId ID of the chat with the message
* @param messages Message ID
* @returns Reactions to the corresponding message, or `null` if there are none
* @internal
*/
export async function getMessageReactions(
this: TelegramClient,
chatId: InputPeerLike,
messages: number
): Promise<MessageReactions | null>
/**
* Get reactions to messages.
*
* > Apps should short-poll reactions for visible messages
* > (that weren't sent by the user) once every 15-30 seconds,
* > but only if `message.reactions` is set
*
* @param chatId ID of the chat with messages
* @param messages Message IDs
* @returns Reactions to corresponding messages, or `null` if there are none
* @internal
*/
export async function getMessageReactions(
this: TelegramClient,
chatId: InputPeerLike,
messages: number[]
): Promise<(MessageReactions | null)[]>
/**
* @internal
*/
export async function getMessageReactions(
this: TelegramClient,
chatId: InputPeerLike,
messages: MaybeArray<number>
): Promise<MaybeArray<MessageReactions | null>> {
const single = !Array.isArray(messages)
if (!Array.isArray(messages)) {
messages = [messages]
}
const res = await this.call({
_: 'messages.getMessagesReactions',
peer: await this.resolvePeer(chatId),
id: messages,
})
assertIsUpdatesGroup('messages.getMessagesReactions', res)
// normally the group contains updateMessageReactions
// for each message requested that has reactions
//
// these updates are not ordered in any way, so
// we don't need to pass them to updates engine
const index: Record<number, MessageReactions> = {}
const peers = PeersIndex.from(res)
for (const update of res.updates) {
assertTypeIs(
'messages.getMessagesReactions',
update,
'updateMessageReactions'
)
index[update.msgId] = new MessageReactions(
this,
update.msgId,
getMarkedPeerId(update.peer),
update.reactions,
peers
)
}
if (single) {
return index[messages[0]] ?? null
}
return messages.map((messageId) => index[messageId] ?? null)
}

View file

@ -0,0 +1,77 @@
import { TelegramClient } from '../../client'
import {
InputPeerLike,
PeerReaction,
PeersIndex,
} from '../../types'
import { tl } from '@mtcute/tl'
/**
* Get users who have reacted to the message.
*
* @param chatId Chat ID
* @param messageId Message ID
* @param params
* @internal
*/
export async function* getReactionUsers(
this: TelegramClient,
chatId: InputPeerLike,
messageId: number,
params?: {
/**
* Get only reactions with the specified emoji
*/
emoji?: string
/**
* Limit the number of events returned.
*
* Defaults to `Infinity`, i.e. all events are returned
*/
limit?: number
/**
* Chunk size, usually not needed.
*
* Defaults to `100`
*/
chunkSize?: number
}
): AsyncIterableIterator<PeerReaction> {
if (!params) params = {}
const peer = await this.resolvePeer(chatId)
let current = 0
let offset: string | undefined = undefined
const total = params.limit || Infinity
const chunkSize = Math.min(params.chunkSize ?? 100, total)
for (;;) {
const res: tl.RpcCallReturn['messages.getMessageReactionsList'] =
await this.call({
_: 'messages.getMessageReactionsList',
peer,
id: messageId,
reaction: params.emoji,
limit: Math.min(chunkSize, total - current),
offset,
})
if (!res.reactions.length) break
offset = res.nextOffset
const peers = PeersIndex.from(res)
for (const reaction of res.reactions) {
const parsed = new PeerReaction(this, reaction, peers)
current += 1
yield parsed
if (current >= total) break
}
}
}

View file

@ -0,0 +1,20 @@
import { TelegramClient } from '../../client'
import { InputPeerLike } from '../../types'
import { createDummyUpdate } from '../../utils/updates-utils'
/**
* Mark all reactions in chat as read.
*
* @param chatId Chat ID
* @internal
*/
export async function readReactions(
this: TelegramClient,
chatId: InputPeerLike,
): Promise<void> {
const res = await this.call({
_: 'messages.readReactions',
peer: await this.resolvePeer(chatId)
})
this._handleUpdate(createDummyUpdate(res.pts, res.ptsCount))
}

View file

@ -0,0 +1,60 @@
import { TelegramClient } from '../../client'
import {
InputPeerLike,
Message,
MtTypeAssertionError,
PeersIndex,
} from '../../types'
import { assertIsUpdatesGroup } from '../../utils/updates-utils'
import { tl } from '@mtcute/tl'
/**
* Send or remove a reaction.
*
* @param chatId Chat ID with the message to react to
* @param message Message ID to react to
* @param emoji Reaction emoji (or `null` to remove)
* @param big Whether to use a big reaction
* @returns Message to which the reaction was sent
* @internal
*/
export async function sendReaction(
this: TelegramClient,
chatId: InputPeerLike,
message: number,
emoji: string | null,
big = false
): Promise<Message> {
const res = await this.call({
_: 'messages.sendReaction',
peer: await this.resolvePeer(chatId),
msgId: message,
reaction: emoji ?? undefined,
big,
})
assertIsUpdatesGroup('messages.sendReaction', res)
// normally the group contains 2 updates:
// updateEditChannelMessage
// updateMessageReactions
// idk why, they contain literally the same data
// so we can just return the message from the first one
this._handleUpdate(res, true)
const upd = res.updates.find(
(it) => it._ === 'updateEditChannelMessage'
) as tl.RawUpdateEditChannelMessage | undefined
if (!upd) {
throw new MtTypeAssertionError(
'messages.sendReaction (@ .updates[*])',
'updateEditChannelMessage',
'undefined'
)
}
const peers = PeersIndex.from(res)
return new Message(this, upd.message, peers)
}

View file

@ -5,3 +5,4 @@ export * from './message'
export * from './search-filters'
export * from './draft-message'
export * from './dialog'
export * from './reactions'

View file

@ -1,11 +1,8 @@
import { User, Chat, InputPeerLike, PeersIndex } from '../peers'
import { tl } from '@mtcute/tl'
import { BotKeyboard, ReplyMarkup } from '../bots'
import { assertNever, getMarkedPeerId, toggleChannelIdMark } from "@mtcute/core";
import {
MtArgumentError,
MtTypeAssertionError,
} from '../errors'
import { assertNever, getMarkedPeerId, toggleChannelIdMark } from '@mtcute/core'
import { MtArgumentError, MtTypeAssertionError } from '../errors'
import { TelegramClient } from '../../client'
import { MessageEntity } from './message-entity'
import { makeInspectable } from '../utils'
@ -13,6 +10,7 @@ import { InputMediaLike, WebPage } from '../media'
import { _messageActionFromTl, MessageAction } from './message-action'
import { _messageMediaFromTl, MessageMedia } from './message-media'
import { FormattedString } from '../parser'
import { MessageReactions } from './reactions'
/**
* A message or a service message
@ -314,7 +312,7 @@ export class Message {
}
if (r.comments) {
const o = (obj as unknown) as Message.MessageCommentsInfo
const o = obj as unknown as Message.MessageCommentsInfo
o.discussion = getMarkedPeerId(r.channelId!, 'channel')
o.repliers =
r.recentRepliers?.map((it) => getMarkedPeerId(it)) ?? []
@ -490,6 +488,34 @@ export class Message {
return this._markup
}
/**
* Whether this message can be forwarded
*
* `false` for service mesasges and private restricted chats/chanenls
*/
get canBeForwarded(): boolean {
return this.raw._ === 'message' && !this.raw.noforwards
}
private _reactions?: MessageReactions | null
get reactions(): MessageReactions | null {
if (this._reactions === undefined) {
if (this.raw._ === 'messageService' || !this.raw.reactions) {
this._reactions = null
} else {
this._reactions = new MessageReactions(
this.client,
this.raw.id,
getMarkedPeerId(this.raw.peerId),
this.raw.reactions,
this._peers
)
}
}
return this._reactions
}
/**
* Generated permalink to this message, only for groups and channels
*
@ -892,6 +918,21 @@ export class Message {
clearMentions
)
}
/**
* React to this message
*
* @param emoji Reaction emoji
* @param big Whether to use a big reaction
*/
async react(emoji: string | null, big?: boolean): Promise<Message> {
return this.client.sendReaction(
this.chat.inputPeer,
this.raw.id,
emoji,
big
)
}
}
makeInspectable(Message, ['isScheduled'], ['link'])

View file

@ -0,0 +1,118 @@
import { TelegramClient } from '../../client'
import { tl } from '@mtcute/tl'
import { makeInspectable } from '../utils'
import { getMarkedPeerId } from '@mtcute/core'
import { PeersIndex, User } from '../peers'
import { assertTypeIs } from '../../utils/type-assertion'
export class PeerReaction {
constructor(
readonly client: TelegramClient,
readonly raw: tl.RawMessagePeerReaction,
readonly _peers: PeersIndex
) {}
/**
* Emoji representing the reaction
*/
get emoji(): string {
return this.raw.reaction!
}
/**
* Whether this is a big reaction
*/
get big(): boolean {
return this.raw.big!
}
/**
* Whether this reaction is unread by the current user
*/
get unread(): boolean {
return this.raw.unread!
}
/**
* ID of the user who has reacted
*/
get userId(): number {
return getMarkedPeerId(this.raw.peerId)
}
private _user?: User
/**
* User who has reacted
*/
get user(): User {
if (!this._user) {
assertTypeIs('PeerReaction#user', this.raw.peerId, 'peerUser')
this._user = new User(
this.client,
this._peers.user(this.raw.peerId.userId)
)
}
return this._user
}
}
makeInspectable(PeerReaction)
export class MessageReactions {
constructor(
readonly client: TelegramClient,
readonly messageId: number,
readonly chatId: number,
readonly raw: tl.RawMessageReactions,
readonly _peers: PeersIndex
) {}
/**
* Whether you can use {@link getUsers}
* (or {@link TelegramClient.getReactionUsers})
* to get the users who reacted to this message
*/
get usersVisible(): boolean {
return this.raw.canSeeList!
}
/**
* Reactions on the message, along with their counts
*/
get reactions(): tl.TypeReactionCount[] {
return this.raw.results
}
private _recentReactions?: PeerReaction[]
/**
* Recently reacted users.
* To get a full list of users, use {@link getUsers}
*/
get recentReactions(): PeerReaction[] {
if (!this.raw.recentReactions) {
return []
}
if (!this._recentReactions) {
this._recentReactions = this.raw.recentReactions.map(
(reaction) =>
new PeerReaction(this.client, reaction, this._peers)
)
}
return this._recentReactions
}
/**
* Get the users who reacted to this message
*/
getUsers(params?: Parameters<TelegramClient['getReactionUsers']>[2]): AsyncIterableIterator<PeerReaction> {
return this.client.getReactionUsers(this.messageId, this.chatId, params)
}
}
makeInspectable(MessageReactions)