diff --git a/packages/client/scripts/update-types.txt b/packages/client/scripts/update-types.txt index 64fc4fdf..4e25ca85 100644 --- a/packages/client/scripts/update-types.txt +++ b/packages/client/scripts/update-types.txt @@ -18,4 +18,6 @@ bot_chat_join_request = BotChatJoinRequestUpdate in ChatJoinRequestUpdateContext chat_join_request = ChatJoinRequestUpdate pre_checkout_query = PreCheckoutQuery in PreCheckoutQueryContext story: StoryUpdate = StoryUpdate -delete_story = DeleteStoryUpdate \ No newline at end of file +delete_story = DeleteStoryUpdate +bot_reaction: BotReactionUpdate = BotReactionUpdate +bot_reaction_count: BotReactionCountUpdate = BotReactionCountUpdate \ No newline at end of file diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 2ee5a73e..989e9bcc 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -260,6 +260,8 @@ import { BoostStats, BotChatJoinRequestUpdate, BotCommands, + BotReactionCountUpdate, + BotReactionUpdate, BotStoppedUpdate, CallbackQuery, Chat, @@ -532,6 +534,20 @@ export interface TelegramClient extends BaseTelegramClient { * @param handler Delete story handler */ 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 on(name: string, handler: (...args: any[]) => void): this @@ -3907,12 +3923,14 @@ export interface TelegramClient extends BaseTelegramClient { * * **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( params: InputMessageId & { /** Reaction emoji (or `null` to remove reaction) */ - emoji?: InputReaction | null + emoji?: MaybeArray | null /** Whether to use a big reaction */ big?: boolean @@ -3922,7 +3940,7 @@ export interface TelegramClient extends BaseTelegramClient { */ shouldDispatch?: true }, - ): Promise + ): Promise /** Send a text in reply to a given message */ replyText(message: Message, ...params: ParametersSkip2): ReturnType /** Send a media in reply to a given message */ diff --git a/packages/client/src/methods/_imports.ts b/packages/client/src/methods/_imports.ts index ab45367d..3ee4cd99 100644 --- a/packages/client/src/methods/_imports.ts +++ b/packages/client/src/methods/_imports.ts @@ -24,6 +24,8 @@ import { BoostStats, BotChatJoinRequestUpdate, BotCommands, + BotReactionCountUpdate, + BotReactionUpdate, BotStoppedUpdate, CallbackQuery, Chat, diff --git a/packages/client/src/methods/chats/ban-chat-member.ts b/packages/client/src/methods/chats/ban-chat-member.ts index a112f821..526be26e 100644 --- a/packages/client/src/methods/chats/ban-chat-member.ts +++ b/packages/client/src/methods/chats/ban-chat-member.ts @@ -1,4 +1,4 @@ -import { BaseTelegramClient, MtTypeAssertionError } from '@mtcute/core' +import { BaseTelegramClient } from '@mtcute/core' import { InputPeerLike, Message, MtInvalidPeerTypeError } from '../../types/index.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') - try { - return _findMessageInUpdate(client, res, false, !shouldDispatch) - } catch (e) { - if (e instanceof MtTypeAssertionError && e.context === '_findInUpdate (@ .updates[*])') { - // no service message - return null - } - - throw e - } + return _findMessageInUpdate(client, res, false, !shouldDispatch, true) } diff --git a/packages/client/src/methods/messages/find-in-update.ts b/packages/client/src/methods/messages/find-in-update.ts index 829f4d31..3018f178 100644 --- a/packages/client/src/methods/messages/find-in-update.ts +++ b/packages/client/src/methods/messages/find-in-update.ts @@ -1,16 +1,44 @@ +/* eslint-disable max-params */ import { BaseTelegramClient, MtTypeAssertionError, tl } from '@mtcute/core' import { Message } from '../../types/messages/index.js' import { PeersIndex } from '../../types/peers/index.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( client: BaseTelegramClient, res: tl.TypeUpdates, isEdit = false, noDispatch = true, -): Message { + allowNull = false, +): Message | null { assertIsUpdatesGroup('_findMessageInUpdate', res) client.network.handleUpdate(res, noDispatch) @@ -29,6 +57,8 @@ export function _findMessageInUpdate( } } + if (allowNull) return null + throw new MtTypeAssertionError( '_findInUpdate (@ .updates[*])', 'updateNewMessage | updateNewChannelMessage | updateNewScheduledMessage', diff --git a/packages/client/src/methods/messages/pin-message.ts b/packages/client/src/methods/messages/pin-message.ts index ee95eb90..2e92840b 100644 --- a/packages/client/src/methods/messages/pin-message.ts +++ b/packages/client/src/methods/messages/pin-message.ts @@ -1,4 +1,4 @@ -import { BaseTelegramClient, MtTypeAssertionError } from '@mtcute/core' +import { BaseTelegramClient } from '@mtcute/core' import { InputMessageId, Message, normalizeInputMessageId } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -38,14 +38,5 @@ export async function pinMessage( pmOneside: !bothSides, }) - try { - return _findMessageInUpdate(client, res, false, !shouldDispatch) - } catch (e) { - if (e instanceof MtTypeAssertionError && e.context === '_findInUpdate (@ .updates[*])') { - // no service message - return null - } - - throw e - } + return _findMessageInUpdate(client, res, false, !shouldDispatch, true) } diff --git a/packages/client/src/methods/messages/send-reaction.ts b/packages/client/src/methods/messages/send-reaction.ts index 347e43af..0af47a5b 100644 --- a/packages/client/src/methods/messages/send-reaction.ts +++ b/packages/client/src/methods/messages/send-reaction.ts @@ -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 { resolvePeer } from '../users/resolve-peer.js' import { _findMessageInUpdate } from './find-in-update.js' @@ -8,13 +14,15 @@ import { _findMessageInUpdate } from './find-in-update.js' /** * 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( client: BaseTelegramClient, params: InputMessageId & { /** Reaction emoji (or `null` to remove reaction) */ - emoji?: InputReaction | null + emoji?: MaybeArray | null /** Whether to use a big reaction */ big?: boolean @@ -24,17 +32,18 @@ export async function sendReaction( */ shouldDispatch?: true }, -): Promise { +): Promise { const { emoji, big } = 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({ _: 'messages.sendReaction', peer: await resolvePeer(client, chatId), msgId: message, - reaction: [reaction], + reaction: reactions, big, }) @@ -45,6 +54,9 @@ export async function sendReaction( // updateMessageReactions // idk why, they contain literally the same data // 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) } diff --git a/packages/client/src/types/updates/bot-reaction.ts b/packages/client/src/types/updates/bot-reaction.ts new file mode 100644 index 00000000..dc76bfd0 --- /dev/null +++ b/packages/client/src/types/updates/bot-reaction.ts @@ -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) diff --git a/packages/client/src/types/updates/index.ts b/packages/client/src/types/updates/index.ts index 430d32af..8af3c1e6 100644 --- a/packages/client/src/types/updates/index.ts +++ b/packages/client/src/types/updates/index.ts @@ -6,6 +6,7 @@ import { ChatJoinRequestUpdate } from './chat-join-request.js' import { ChatMemberUpdate } from './chat-member-update.js' import { InlineQuery } from './inline-query.js' export type { ChatMemberUpdateType } from './chat-member-update.js' +import { BotReactionCountUpdate, BotReactionUpdate } from './bot-reaction.js' import { ChosenInlineResult } from './chosen-inline-result.js' import { DeleteMessageUpdate } from './delete-message-update.js' import { DeleteStoryUpdate } from './delete-story-update.js' @@ -19,6 +20,8 @@ import { UserTypingUpdate } from './user-typing-update.js' export { BotChatJoinRequestUpdate, + BotReactionCountUpdate, + BotReactionUpdate, BotStoppedUpdate, CallbackQuery, ChatJoinRequestUpdate, @@ -59,5 +62,7 @@ export type ParsedUpdate = | { name: 'pre_checkout_query'; data: PreCheckoutQuery } | { name: 'story'; data: StoryUpdate } | { name: 'delete_story'; data: DeleteStoryUpdate } + | { name: 'bot_reaction'; data: BotReactionUpdate } + | { name: 'bot_reaction_count'; data: BotReactionCountUpdate } // end-codegen diff --git a/packages/client/src/types/updates/parse-update.ts b/packages/client/src/types/updates/parse-update.ts index 0fd7a1c9..77be37e3 100644 --- a/packages/client/src/types/updates/parse-update.ts +++ b/packages/client/src/types/updates/parse-update.ts @@ -3,6 +3,8 @@ import { tl } from '@mtcute/core' import { BotChatJoinRequestUpdate, + BotReactionCountUpdate, + BotReactionUpdate, BotStoppedUpdate, CallbackQuery, ChatJoinRequestUpdate, @@ -88,6 +90,10 @@ export function _parseUpdate(update: tl.TypeUpdate, peers: PeersIndex): ParsedUp 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: return null } diff --git a/packages/dispatcher/src/dispatcher.ts b/packages/dispatcher/src/dispatcher.ts index 66e3703a..6ac13bf7 100644 --- a/packages/dispatcher/src/dispatcher.ts +++ b/packages/dispatcher/src/dispatcher.ts @@ -4,6 +4,8 @@ // ^^ will be looked into in MTQ-29 import { + BotReactionCountUpdate, + BotReactionUpdate, BotStoppedUpdate, ChatJoinRequestUpdate, ChatMemberUpdate, @@ -38,6 +40,8 @@ import { filters, UpdateFilter } from './filters/index.js' // begin-codegen-imports import { BotChatJoinRequestHandler, + BotReactionCountUpdateHandler, + BotReactionUpdateHandler, BotStoppedHandler, CallbackQueryHandler, ChatJoinRequestHandler, @@ -1698,5 +1702,57 @@ export class Dispatcher { 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( + filter: UpdateFilter, Mod>, + handler: BotReactionUpdateHandler, 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( + filter: UpdateFilter, Mod>, + handler: BotReactionCountUpdateHandler, Mod>>['callback'], + group?: number, + ): void + + /** @internal */ + onBotReactionCountUpdate(filter: any, handler?: any, group?: number): void { + this._addKnownHandler('bot_reaction_count', filter, handler, group) + } + // end-codegen } diff --git a/packages/dispatcher/src/handler.ts b/packages/dispatcher/src/handler.ts index 068869c0..6f71ef64 100644 --- a/packages/dispatcher/src/handler.ts +++ b/packages/dispatcher/src/handler.ts @@ -1,4 +1,6 @@ import { + BotReactionCountUpdate, + BotReactionUpdate, BotStoppedUpdate, ChatJoinRequestUpdate, ChatMemberUpdate, @@ -82,6 +84,11 @@ export type ChatJoinRequestHandler> = P export type PreCheckoutQueryHandler = ParsedUpdateHandler<'pre_checkout_query', T> export type StoryUpdateHandler> = ParsedUpdateHandler<'story', T> export type DeleteStoryHandler> = ParsedUpdateHandler<'delete_story', T> +export type BotReactionUpdateHandler> = ParsedUpdateHandler<'bot_reaction', T> +export type BotReactionCountUpdateHandler> = ParsedUpdateHandler< + 'bot_reaction_count', + T +> export type UpdateHandler = | RawUpdateHandler @@ -105,5 +112,7 @@ export type UpdateHandler = | PreCheckoutQueryHandler | StoryUpdateHandler | DeleteStoryHandler + | BotReactionUpdateHandler + | BotReactionCountUpdateHandler // end-codegen