feat(client): reactions for bots

This commit is contained in:
alina 🌸 2023-12-29 15:04:18 +03:00
parent 9459748d0d
commit 0474ab918a
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
12 changed files with 273 additions and 36 deletions

View file

@ -19,3 +19,5 @@ chat_join_request = ChatJoinRequestUpdate
pre_checkout_query = PreCheckoutQuery in PreCheckoutQueryContext pre_checkout_query = PreCheckoutQuery in PreCheckoutQueryContext
story: StoryUpdate = StoryUpdate story: StoryUpdate = StoryUpdate
delete_story = DeleteStoryUpdate delete_story = DeleteStoryUpdate
bot_reaction: BotReactionUpdate = BotReactionUpdate
bot_reaction_count: BotReactionCountUpdate = BotReactionCountUpdate

View file

@ -260,6 +260,8 @@ import {
BoostStats, BoostStats,
BotChatJoinRequestUpdate, BotChatJoinRequestUpdate,
BotCommands, BotCommands,
BotReactionCountUpdate,
BotReactionUpdate,
BotStoppedUpdate, BotStoppedUpdate,
CallbackQuery, CallbackQuery,
Chat, Chat,
@ -532,6 +534,20 @@ export interface TelegramClient extends BaseTelegramClient {
* @param handler Delete story handler * @param handler Delete story handler
*/ */
on(name: 'delete_story', handler: (upd: DeleteStoryUpdate) => void): this on(name: 'delete_story', handler: (upd: DeleteStoryUpdate) => void): this
/**
* Register a bot reaction update handler
*
* @param name Event name
* @param handler Bot reaction update handler
*/
on(name: 'bot_reaction', handler: (upd: BotReactionUpdate) => void): this
/**
* Register a bot reaction count update handler
*
* @param name Event name
* @param handler Bot reaction count update handler
*/
on(name: 'bot_reaction_count', handler: (upd: BotReactionCountUpdate) => void): this
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
on(name: string, handler: (...args: any[]) => void): this on(name: string, handler: (...args: any[]) => void): this
@ -3907,12 +3923,14 @@ export interface TelegramClient extends BaseTelegramClient {
* *
* **Available**: 👤 users only * **Available**: 👤 users only
* *
* @returns Message to which the reaction was sent * @returns
* Message to which the reaction was sent, if available.
* The message is normally available for users, but may not be available for bots in PMs.
*/ */
sendReaction( sendReaction(
params: InputMessageId & { params: InputMessageId & {
/** Reaction emoji (or `null` to remove reaction) */ /** Reaction emoji (or `null` to remove reaction) */
emoji?: InputReaction | null emoji?: MaybeArray<InputReaction> | null
/** Whether to use a big reaction */ /** Whether to use a big reaction */
big?: boolean big?: boolean
@ -3922,7 +3940,7 @@ export interface TelegramClient extends BaseTelegramClient {
*/ */
shouldDispatch?: true shouldDispatch?: true
}, },
): Promise<Message> ): Promise<Message | null>
/** Send a text in reply to a given message */ /** Send a text in reply to a given message */
replyText(message: Message, ...params: ParametersSkip2<typeof sendText>): ReturnType<typeof sendText> replyText(message: Message, ...params: ParametersSkip2<typeof sendText>): ReturnType<typeof sendText>
/** Send a media in reply to a given message */ /** Send a media in reply to a given message */

View file

@ -24,6 +24,8 @@ import {
BoostStats, BoostStats,
BotChatJoinRequestUpdate, BotChatJoinRequestUpdate,
BotCommands, BotCommands,
BotReactionCountUpdate,
BotReactionUpdate,
BotStoppedUpdate, BotStoppedUpdate,
CallbackQuery, CallbackQuery,
Chat, Chat,

View file

@ -1,4 +1,4 @@
import { BaseTelegramClient, MtTypeAssertionError } from '@mtcute/core' import { BaseTelegramClient } from '@mtcute/core'
import { InputPeerLike, Message, MtInvalidPeerTypeError } from '../../types/index.js' import { InputPeerLike, Message, MtInvalidPeerTypeError } from '../../types/index.js'
import { isInputPeerChannel, isInputPeerChat, toInputChannel, toInputUser } from '../../utils/peer-utils.js' import { isInputPeerChannel, isInputPeerChat, toInputChannel, toInputUser } from '../../utils/peer-utils.js'
@ -56,14 +56,5 @@ export async function banChatMember(
}) })
} else throw new MtInvalidPeerTypeError(chatId, 'chat or channel') } else throw new MtInvalidPeerTypeError(chatId, 'chat or channel')
try { return _findMessageInUpdate(client, res, false, !shouldDispatch, true)
return _findMessageInUpdate(client, res, false, !shouldDispatch)
} catch (e) {
if (e instanceof MtTypeAssertionError && e.context === '_findInUpdate (@ .updates[*])') {
// no service message
return null
}
throw e
}
} }

View file

@ -1,16 +1,44 @@
/* eslint-disable max-params */
import { BaseTelegramClient, MtTypeAssertionError, tl } from '@mtcute/core' import { BaseTelegramClient, MtTypeAssertionError, tl } from '@mtcute/core'
import { Message } from '../../types/messages/index.js' import { Message } from '../../types/messages/index.js'
import { PeersIndex } from '../../types/peers/index.js' import { PeersIndex } from '../../types/peers/index.js'
import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' import { assertIsUpdatesGroup } from '../../utils/updates-utils.js'
/** @internal */ /**
* @internal
* @noemit
*/
export function _findMessageInUpdate(
client: BaseTelegramClient,
res: tl.TypeUpdates,
isEdit?: boolean,
noDispatch?: boolean,
allowNull?: false,
): Message
/**
* @internal
* @noemit
*/
export function _findMessageInUpdate(
client: BaseTelegramClient,
res: tl.TypeUpdates,
isEdit?: boolean,
noDispatch?: boolean,
allowNull?: true,
): Message | null
/**
* @internal
* @noemit
*/
export function _findMessageInUpdate( export function _findMessageInUpdate(
client: BaseTelegramClient, client: BaseTelegramClient,
res: tl.TypeUpdates, res: tl.TypeUpdates,
isEdit = false, isEdit = false,
noDispatch = true, noDispatch = true,
): Message { allowNull = false,
): Message | null {
assertIsUpdatesGroup('_findMessageInUpdate', res) assertIsUpdatesGroup('_findMessageInUpdate', res)
client.network.handleUpdate(res, noDispatch) client.network.handleUpdate(res, noDispatch)
@ -29,6 +57,8 @@ export function _findMessageInUpdate(
} }
} }
if (allowNull) return null
throw new MtTypeAssertionError( throw new MtTypeAssertionError(
'_findInUpdate (@ .updates[*])', '_findInUpdate (@ .updates[*])',
'updateNewMessage | updateNewChannelMessage | updateNewScheduledMessage', 'updateNewMessage | updateNewChannelMessage | updateNewScheduledMessage',

View file

@ -1,4 +1,4 @@
import { BaseTelegramClient, MtTypeAssertionError } from '@mtcute/core' import { BaseTelegramClient } from '@mtcute/core'
import { InputMessageId, Message, normalizeInputMessageId } from '../../types/index.js' import { InputMessageId, Message, normalizeInputMessageId } from '../../types/index.js'
import { resolvePeer } from '../users/resolve-peer.js' import { resolvePeer } from '../users/resolve-peer.js'
@ -38,14 +38,5 @@ export async function pinMessage(
pmOneside: !bothSides, pmOneside: !bothSides,
}) })
try { return _findMessageInUpdate(client, res, false, !shouldDispatch, true)
return _findMessageInUpdate(client, res, false, !shouldDispatch)
} catch (e) {
if (e instanceof MtTypeAssertionError && e.context === '_findInUpdate (@ .updates[*])') {
// no service message
return null
}
throw e
}
} }

View file

@ -1,6 +1,12 @@
import { BaseTelegramClient } from '@mtcute/core' import { BaseTelegramClient, MaybeArray } from '@mtcute/core'
import { InputMessageId, InputReaction, Message, normalizeInputMessageId, normalizeInputReaction } from '../../types/index.js' import {
InputMessageId,
InputReaction,
Message,
normalizeInputMessageId,
normalizeInputReaction,
} from '../../types/index.js'
import { assertIsUpdatesGroup } from '../../utils/updates-utils.js' import { assertIsUpdatesGroup } from '../../utils/updates-utils.js'
import { resolvePeer } from '../users/resolve-peer.js' import { resolvePeer } from '../users/resolve-peer.js'
import { _findMessageInUpdate } from './find-in-update.js' import { _findMessageInUpdate } from './find-in-update.js'
@ -8,13 +14,15 @@ import { _findMessageInUpdate } from './find-in-update.js'
/** /**
* Send or remove a reaction. * Send or remove a reaction.
* *
* @returns Message to which the reaction was sent * @returns
* Message to which the reaction was sent, if available.
* The message is normally available for users, but may not be available for bots in PMs.
*/ */
export async function sendReaction( export async function sendReaction(
client: BaseTelegramClient, client: BaseTelegramClient,
params: InputMessageId & { params: InputMessageId & {
/** Reaction emoji (or `null` to remove reaction) */ /** Reaction emoji (or `null` to remove reaction) */
emoji?: InputReaction | null emoji?: MaybeArray<InputReaction> | null
/** Whether to use a big reaction */ /** Whether to use a big reaction */
big?: boolean big?: boolean
@ -24,17 +32,18 @@ export async function sendReaction(
*/ */
shouldDispatch?: true shouldDispatch?: true
}, },
): Promise<Message> { ): Promise<Message | null> {
const { emoji, big } = params const { emoji, big } = params
const { chatId, message } = normalizeInputMessageId(params) const { chatId, message } = normalizeInputMessageId(params)
const reaction = normalizeInputReaction(emoji) const emojis = Array.isArray(emoji) ? emoji : [emoji]
const reactions = emojis.map(normalizeInputReaction)
const res = await client.call({ const res = await client.call({
_: 'messages.sendReaction', _: 'messages.sendReaction',
peer: await resolvePeer(client, chatId), peer: await resolvePeer(client, chatId),
msgId: message, msgId: message,
reaction: [reaction], reaction: reactions,
big, big,
}) })
@ -45,6 +54,9 @@ export async function sendReaction(
// updateMessageReactions // updateMessageReactions
// idk why, they contain literally the same data // idk why, they contain literally the same data
// so we can just return the message from the first one // so we can just return the message from the first one
//
// for whatever reason, sendReaction for bots returns empty updates
// group in pms, so we should handle that too
return _findMessageInUpdate(client, res, true, !params.shouldDispatch) return _findMessageInUpdate(client, res, true, !params.shouldDispatch, true)
} }

View file

@ -0,0 +1,115 @@
import { tl } from '@mtcute/core'
import { makeInspectable } from '../../utils/inspectable.js'
import { memoizeGetters } from '../../utils/memoize.js'
import { Chat } from '../peers/chat.js'
import { parsePeer, Peer } from '../peers/peer.js'
import { PeersIndex } from '../peers/peers-index.js'
import { ReactionCount } from '../reactions/reaction-count.js'
import { InputReaction, toReactionEmoji } from '../reactions/types.js'
/**
* A reaction to a message was changed by a user.
*
* These updates are only received for bots - for PMs and in chats
* where the bot is an administrator.
*
* Reactions sent by other bots are not received.
*/
export class BotReactionUpdate {
constructor(
readonly raw: tl.RawUpdateBotMessageReaction,
readonly _peers: PeersIndex,
) {}
/**
* Chat where the reaction has been changed
*/
get chat(): Chat {
return Chat._parseFromPeer(this.raw.peer, this._peers)
}
/**
* ID of the message where the reaction has been changed
*/
get messageId(): number {
return this.raw.msgId
}
/**
* Date when the reaction has been changed
*/
get date(): Date {
return new Date(this.raw.date * 1000)
}
/**
* ID of the user who has set/removed the reaction
*/
get actor(): Peer {
return parsePeer(this.raw.actor, this._peers)
}
/**
* List of reactions before the change
*/
get before(): InputReaction[] {
return this.raw.oldReactions.map((it) => toReactionEmoji(it))
}
/**
* List of reactions after the change
*/
get after(): InputReaction[] {
return this.raw.newReactions.map((it) => toReactionEmoji(it))
}
}
memoizeGetters(BotReactionUpdate, ['chat', 'actor', 'before', 'after'])
makeInspectable(BotReactionUpdate)
/**
* The count of reactions to a message has been updated.
*
* These updates are only received for bots in chats where
* the bot is an administrator. Unlike {@link BotReactionUpdate},
* this update is used for chats where the list of users who
* reacted to a message is not visible (e.g. channels).
*/
export class BotReactionCountUpdate {
constructor(
readonly raw: tl.RawUpdateBotMessageReactions,
readonly _peers: PeersIndex,
) {}
/**
* Chat where the reaction has been changed
*/
get chat(): Chat {
return Chat._parseFromPeer(this.raw.peer, this._peers)
}
/**
* ID of the message where the reaction has been changed
*/
get messageId(): number {
return this.raw.msgId
}
/**
* Date when the reaction has been changed
*/
get date(): Date {
return new Date(this.raw.date * 1000)
}
/**
* The new list of reactions to the message
*/
get reactions(): ReactionCount[] {
return this.raw.reactions.map((it) => new ReactionCount(it))
}
}
memoizeGetters(BotReactionCountUpdate, ['chat'])
makeInspectable(BotReactionCountUpdate)

View file

@ -6,6 +6,7 @@ import { ChatJoinRequestUpdate } from './chat-join-request.js'
import { ChatMemberUpdate } from './chat-member-update.js' import { ChatMemberUpdate } from './chat-member-update.js'
import { InlineQuery } from './inline-query.js' import { InlineQuery } from './inline-query.js'
export type { ChatMemberUpdateType } from './chat-member-update.js' export type { ChatMemberUpdateType } from './chat-member-update.js'
import { BotReactionCountUpdate, BotReactionUpdate } from './bot-reaction.js'
import { ChosenInlineResult } from './chosen-inline-result.js' import { ChosenInlineResult } from './chosen-inline-result.js'
import { DeleteMessageUpdate } from './delete-message-update.js' import { DeleteMessageUpdate } from './delete-message-update.js'
import { DeleteStoryUpdate } from './delete-story-update.js' import { DeleteStoryUpdate } from './delete-story-update.js'
@ -19,6 +20,8 @@ import { UserTypingUpdate } from './user-typing-update.js'
export { export {
BotChatJoinRequestUpdate, BotChatJoinRequestUpdate,
BotReactionCountUpdate,
BotReactionUpdate,
BotStoppedUpdate, BotStoppedUpdate,
CallbackQuery, CallbackQuery,
ChatJoinRequestUpdate, ChatJoinRequestUpdate,
@ -59,5 +62,7 @@ export type ParsedUpdate =
| { name: 'pre_checkout_query'; data: PreCheckoutQuery } | { name: 'pre_checkout_query'; data: PreCheckoutQuery }
| { name: 'story'; data: StoryUpdate } | { name: 'story'; data: StoryUpdate }
| { name: 'delete_story'; data: DeleteStoryUpdate } | { name: 'delete_story'; data: DeleteStoryUpdate }
| { name: 'bot_reaction'; data: BotReactionUpdate }
| { name: 'bot_reaction_count'; data: BotReactionCountUpdate }
// end-codegen // end-codegen

View file

@ -3,6 +3,8 @@ import { tl } from '@mtcute/core'
import { import {
BotChatJoinRequestUpdate, BotChatJoinRequestUpdate,
BotReactionCountUpdate,
BotReactionUpdate,
BotStoppedUpdate, BotStoppedUpdate,
CallbackQuery, CallbackQuery,
ChatJoinRequestUpdate, ChatJoinRequestUpdate,
@ -88,6 +90,10 @@ export function _parseUpdate(update: tl.TypeUpdate, peers: PeersIndex): ParsedUp
data: new StoryUpdate(update, peers), data: new StoryUpdate(update, peers),
} }
} }
case 'updateBotMessageReaction':
return { name: 'bot_reaction', data: new BotReactionUpdate(update, peers) }
case 'updateBotMessageReactions':
return { name: 'bot_reaction_count', data: new BotReactionCountUpdate(update, peers) }
default: default:
return null return null
} }

View file

@ -4,6 +4,8 @@
// ^^ will be looked into in MTQ-29 // ^^ will be looked into in MTQ-29
import { import {
BotReactionCountUpdate,
BotReactionUpdate,
BotStoppedUpdate, BotStoppedUpdate,
ChatJoinRequestUpdate, ChatJoinRequestUpdate,
ChatMemberUpdate, ChatMemberUpdate,
@ -38,6 +40,8 @@ import { filters, UpdateFilter } from './filters/index.js'
// begin-codegen-imports // begin-codegen-imports
import { import {
BotChatJoinRequestHandler, BotChatJoinRequestHandler,
BotReactionCountUpdateHandler,
BotReactionUpdateHandler,
BotStoppedHandler, BotStoppedHandler,
CallbackQueryHandler, CallbackQueryHandler,
ChatJoinRequestHandler, ChatJoinRequestHandler,
@ -1698,5 +1702,57 @@ export class Dispatcher<State extends object = never> {
this._addKnownHandler('delete_story', filter, handler, group) this._addKnownHandler('delete_story', filter, handler, group)
} }
/**
* Register a bot reaction update handler without any filters
*
* @param handler Bot reaction update handler
* @param group Handler group index
*/
onBotReactionUpdate(handler: BotReactionUpdateHandler['callback'], group?: number): void
/**
* Register a bot reaction update handler with a filter
*
* @param filter Update filter
* @param handler Bot reaction update handler
* @param group Handler group index
*/
onBotReactionUpdate<Mod>(
filter: UpdateFilter<UpdateContext<BotReactionUpdate>, Mod>,
handler: BotReactionUpdateHandler<filters.Modify<UpdateContext<BotReactionUpdate>, Mod>>['callback'],
group?: number,
): void
/** @internal */
onBotReactionUpdate(filter: any, handler?: any, group?: number): void {
this._addKnownHandler('bot_reaction', filter, handler, group)
}
/**
* Register a bot reaction count update handler without any filters
*
* @param handler Bot reaction count update handler
* @param group Handler group index
*/
onBotReactionCountUpdate(handler: BotReactionCountUpdateHandler['callback'], group?: number): void
/**
* Register a bot reaction count update handler with a filter
*
* @param filter Update filter
* @param handler Bot reaction count update handler
* @param group Handler group index
*/
onBotReactionCountUpdate<Mod>(
filter: UpdateFilter<UpdateContext<BotReactionCountUpdate>, Mod>,
handler: BotReactionCountUpdateHandler<filters.Modify<UpdateContext<BotReactionCountUpdate>, Mod>>['callback'],
group?: number,
): void
/** @internal */
onBotReactionCountUpdate(filter: any, handler?: any, group?: number): void {
this._addKnownHandler('bot_reaction_count', filter, handler, group)
}
// end-codegen // end-codegen
} }

View file

@ -1,4 +1,6 @@
import { import {
BotReactionCountUpdate,
BotReactionUpdate,
BotStoppedUpdate, BotStoppedUpdate,
ChatJoinRequestUpdate, ChatJoinRequestUpdate,
ChatMemberUpdate, ChatMemberUpdate,
@ -82,6 +84,11 @@ export type ChatJoinRequestHandler<T = UpdateContext<ChatJoinRequestUpdate>> = P
export type PreCheckoutQueryHandler<T = PreCheckoutQueryContext> = ParsedUpdateHandler<'pre_checkout_query', T> export type PreCheckoutQueryHandler<T = PreCheckoutQueryContext> = ParsedUpdateHandler<'pre_checkout_query', T>
export type StoryUpdateHandler<T = UpdateContext<StoryUpdate>> = ParsedUpdateHandler<'story', T> export type StoryUpdateHandler<T = UpdateContext<StoryUpdate>> = ParsedUpdateHandler<'story', T>
export type DeleteStoryHandler<T = UpdateContext<DeleteStoryUpdate>> = ParsedUpdateHandler<'delete_story', T> export type DeleteStoryHandler<T = UpdateContext<DeleteStoryUpdate>> = ParsedUpdateHandler<'delete_story', T>
export type BotReactionUpdateHandler<T = UpdateContext<BotReactionUpdate>> = ParsedUpdateHandler<'bot_reaction', T>
export type BotReactionCountUpdateHandler<T = UpdateContext<BotReactionCountUpdate>> = ParsedUpdateHandler<
'bot_reaction_count',
T
>
export type UpdateHandler = export type UpdateHandler =
| RawUpdateHandler | RawUpdateHandler
@ -105,5 +112,7 @@ export type UpdateHandler =
| PreCheckoutQueryHandler | PreCheckoutQueryHandler
| StoryUpdateHandler | StoryUpdateHandler
| DeleteStoryHandler | DeleteStoryHandler
| BotReactionUpdateHandler
| BotReactionCountUpdateHandler
// end-codegen // end-codegen