diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 06f85e60..5fafd58e 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -59,6 +59,7 @@ import { _normalizeInputFile } from './methods/files/normalize-input-file' import { _normalizeInputMedia } from './methods/files/normalize-input-media' import { uploadFile } from './methods/files/upload-file' import { deleteMessages } from './methods/messages/delete-messages' +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' @@ -1136,6 +1137,67 @@ export interface TelegramClient extends BaseTelegramClient { ids: MaybeArray, revoke?: boolean ): Promise + /** + * Edit sent inline message text, media and reply markup. + * + * @param id + * Inline message ID, either as a TL object, or as a + * TDLib and Bot API compatible string + * @param params + */ + editInlineMessage( + id: tl.TypeInputBotInlineMessageID | string, + params: { + /** + * New message text + * + * When `media` is passed, `media.caption` is used instead + */ + text?: string + + /** + * Parse mode to use to parse entities before sending + * the message. Defaults to current default parse mode (if any). + * + * Passing `null` will explicitly disable formatting. + */ + parseMode?: string | null + + /** + * List of formatting entities to use instead of parsing via a + * parse mode. + * + * **Note:** Passing this makes the method ignore {@link parseMode} + * + * When `media` is passed, `media.entities` is used instead + */ + entities?: tl.TypeMessageEntity[] + + /** + * New message media + */ + media?: InputMediaLike + + /** + * Whether to disable links preview in this message + */ + disableWebPreview?: boolean + + /** + * For bots: new reply markup. + * If omitted, existing markup will be removed. + */ + replyMarkup?: ReplyMarkup + + /** + * For media, upload progress callback. + * + * @param uploaded Number of bytes uploaded + * @param total Total file size in bytes + */ + progressCallback?: (uploaded: number, total: number) => void + } + ): Promise /** * Edit message text, media, reply markup and schedule date. * @@ -1959,6 +2021,7 @@ export class TelegramClient extends BaseTelegramClient { protected _userId: number | null protected _isBot: boolean protected _downloadConnections: Record + protected _connectionsForInline: Record protected _parseModes: Record protected _defaultParseMode: string | null protected _updLock: Lock @@ -1970,6 +2033,7 @@ export class TelegramClient extends BaseTelegramClient { this._userId = null this._isBot = false this._downloadConnections = {} + this._connectionsForInline = {} this._parseModes = {} this._defaultParseMode = null this._updLock = new Lock() @@ -2041,6 +2105,7 @@ export class TelegramClient extends BaseTelegramClient { protected _normalizeInputMedia = _normalizeInputMedia uploadFile = uploadFile deleteMessages = deleteMessages + editInlineMessage = editInlineMessage editMessage = editMessage protected _findMessageInUpdate = _findMessageInUpdate forwardMessages = forwardMessages diff --git a/packages/client/src/methods/messages/edit-inline-message.ts b/packages/client/src/methods/messages/edit-inline-message.ts new file mode 100644 index 00000000..678fdf62 --- /dev/null +++ b/packages/client/src/methods/messages/edit-inline-message.ts @@ -0,0 +1,125 @@ +import { TelegramClient } from '../../client' +import { BotKeyboard, InputMediaLike, ReplyMarkup } from '../../types' +import { tl } from '@mtcute/tl' +import { parseInlineMessageId } from '../../utils/inline-utils' +import { TelegramConnection } from '@mtcute/core' + +// @extension +interface EditInlineExtension { + _connectionsForInline: Record +} + +// @initialize +function _initializeEditInline(this: TelegramClient) { + this._connectionsForInline = {} +} + +/** + * Edit sent inline message text, media and reply markup. + * + * @param id + * Inline message ID, either as a TL object, or as a + * TDLib and Bot API compatible string + * @param params + * @internal + */ +export async function editInlineMessage( + this: TelegramClient, + id: tl.TypeInputBotInlineMessageID | string, + params: { + /** + * New message text + * + * When `media` is passed, `media.caption` is used instead + */ + text?: string + + /** + * Parse mode to use to parse entities before sending + * the message. Defaults to current default parse mode (if any). + * + * Passing `null` will explicitly disable formatting. + */ + parseMode?: string | null + + /** + * List of formatting entities to use instead of parsing via a + * parse mode. + * + * **Note:** Passing this makes the method ignore {@link parseMode} + * + * When `media` is passed, `media.entities` is used instead + */ + entities?: tl.TypeMessageEntity[] + + /** + * New message media + */ + media?: InputMediaLike + + /** + * Whether to disable links preview in this message + */ + disableWebPreview?: boolean + + /** + * For bots: new reply markup. + * If omitted, existing markup will be removed. + */ + replyMarkup?: ReplyMarkup + + /** + * For media, upload progress callback. + * + * @param uploaded Number of bytes uploaded + * @param total Total file size in bytes + */ + progressCallback?: (uploaded: number, total: number) => void + } +): Promise { + let content: string | undefined + let entities: tl.TypeMessageEntity[] | undefined + let media: tl.TypeInputMedia | undefined = undefined + + if (params.media) { + media = await this._normalizeInputMedia(params.media, params) + ;[content, entities] = await this._parseEntities( + params.media.caption, + params.parseMode, + params.media.entities + ) + } else { + ;[content, entities] = await this._parseEntities( + params.text, + params.parseMode, + params.entities + ) + } + + if (typeof id === 'string') { + id = parseInlineMessageId(id) + } + + let connection = this.primaryConnection + if (id.dcId !== connection.params.dc.id) { + if (!(id.dcId in this._connectionsForInline)) { + this._connectionsForInline[ + id.dcId + ] = await this.createAdditionalConnection(id.dcId) + } + connection = this._connectionsForInline[id.dcId] + } + + await this.call( + { + _: 'messages.editInlineBotMessage', + id, + noWebpage: params.disableWebPreview, + replyMarkup: BotKeyboard._convertToTl(params.replyMarkup), + message: content, + entities, + media, + }, + { connection } + ) +} diff --git a/packages/client/src/types/bots/input/input-inline-message.ts b/packages/client/src/types/bots/input/input-inline-message.ts index 0e1f1f05..2070ba1f 100644 --- a/packages/client/src/types/bots/input/input-inline-message.ts +++ b/packages/client/src/types/bots/input/input-inline-message.ts @@ -185,12 +185,10 @@ export namespace BotInlineMessage { } export function media ( - text?: string, - params?: Omit, + params?: Omit, ): InputInlineMessageMedia { return { type: 'media', - text, ...( params || {} ), diff --git a/packages/client/src/types/bots/input/input-inline-result.ts b/packages/client/src/types/bots/input/input-inline-result.ts index ed12b61c..62595642 100644 --- a/packages/client/src/types/bots/input/input-inline-result.ts +++ b/packages/client/src/types/bots/input/input-inline-result.ts @@ -546,13 +546,13 @@ export namespace BotInline { export function photo( id: string, media: string | tl.RawInputWebDocument | tl.RawInputPhoto, - params: Omit + params?: Omit ): InputInlineResultPhoto { return { id, type: 'photo', media, - ...params, + ...(params || {}), } } diff --git a/packages/client/src/utils/inline-utils.ts b/packages/client/src/utils/inline-utils.ts new file mode 100644 index 00000000..ddbe0608 --- /dev/null +++ b/packages/client/src/utils/inline-utils.ts @@ -0,0 +1,25 @@ +import { tl } from '@mtcute/tl' +import { encodeUrlSafeBase64, parseUrlSafeBase64 } from '@mtcute/file-id/src/utils' +import { BinaryReader, BinaryWriter } from '@mtcute/core' + +export function parseInlineMessageId(id: string): tl.RawInputBotInlineMessageID { + const buf = parseUrlSafeBase64(id) + const reader = new BinaryReader(buf) + + return { + _: 'inputBotInlineMessageID', + dcId: reader.int32(), + id: reader.long(), + accessHash: reader.long() + } +} + +export function encodeInlineMessageId(id: tl.RawInputBotInlineMessageID): string { + const writer = BinaryWriter.alloc(20) // int32, int64, int64 + + writer.int32(id.dcId) + writer.long(id.id) + writer.long(id.accessHash) + + return encodeUrlSafeBase64(writer.result()) +} diff --git a/packages/core/src/base-client.ts b/packages/core/src/base-client.ts index c01eb38b..e283b6fa 100644 --- a/packages/core/src/base-client.ts +++ b/packages/core/src/base-client.ts @@ -456,7 +456,8 @@ export class BaseTelegramClient { async call( message: T, params?: { - throwFlood: boolean + throwFlood?: boolean + connection?: TelegramConnection } ): Promise { if (!this._connected) { @@ -491,7 +492,7 @@ export class BaseTelegramClient { for (let i = 0; i < this._rpcRetryCount; i++) { try { - const res = await this.primaryConnection.sendForResult( + const res = await (params?.connection ?? this.primaryConnection).sendForResult( message, stack ) diff --git a/packages/dispatcher/src/builders.ts b/packages/dispatcher/src/builders.ts index e43b059d..9b9d410f 100644 --- a/packages/dispatcher/src/builders.ts +++ b/packages/dispatcher/src/builders.ts @@ -1,5 +1,6 @@ import { ChatMemberUpdateHandler, + ChosenInlineResultHandler, InlineQueryHandler, NewMessageHandler, RawUpdateHandler, @@ -8,6 +9,7 @@ import { import { filters, UpdateFilter } from './filters' import { InlineQuery, Message } from '@mtcute/client' import { ChatMemberUpdate } from './updates' +import { ChosenInlineResult } from './updates/chosen-inline-result' function _create( type: T['type'], @@ -18,18 +20,17 @@ function _create( return { type, check: filter, - callback: handler + callback: handler, } as any } return { type, - callback: filter + callback: filter, } as any } export namespace handlers { - /** * Create a {@link RawUpdateHandler} * @@ -74,10 +75,7 @@ export namespace handlers { handler: NewMessageHandler>['callback'] ): NewMessageHandler - export function newMessage( - filter: any, - handler?: any - ): NewMessageHandler { + export function newMessage(filter: any, handler?: any): NewMessageHandler { return _create('new_message', filter, handler) } @@ -98,7 +96,9 @@ export namespace handlers { */ export function chatMemberUpdate( filter: UpdateFilter, - handler: ChatMemberUpdateHandler>['callback'] + handler: ChatMemberUpdateHandler< + filters.Modify + >['callback'] ): ChatMemberUpdateHandler export function chatMemberUpdate( @@ -125,7 +125,9 @@ export namespace handlers { */ export function inlineQuery( filter: UpdateFilter, - handler: InlineQueryHandler>['callback'] + handler: InlineQueryHandler< + filters.Modify + >['callback'] ): InlineQueryHandler export function inlineQuery( @@ -134,4 +136,33 @@ export namespace handlers { ): InlineQueryHandler { return _create('inline_query', filter, handler) } + + /** + * Create a chosen inline result handler + * + * @param handler Chosen inline result handler + */ + export function chosenInlineResult( + handler: ChosenInlineResultHandler['callback'] + ): ChosenInlineResultHandler + + /** + * Create a chosen inline result handler with a filter + * + * @param filter Chosen inline result filter + * @param handler Chosen inline result handler + */ + export function chosenInlineResult( + filter: UpdateFilter, + handler: ChosenInlineResultHandler< + filters.Modify + >['callback'] + ): ChosenInlineResultHandler + + export function chosenInlineResult( + filter: any, + handler?: any + ): ChosenInlineResultHandler { + return _create('chosen_inline_result', filter, handler) + } } diff --git a/packages/dispatcher/src/dispatcher.ts b/packages/dispatcher/src/dispatcher.ts index 20af3ffb..7a1a4efe 100644 --- a/packages/dispatcher/src/dispatcher.ts +++ b/packages/dispatcher/src/dispatcher.ts @@ -12,7 +12,9 @@ import { StopPropagation, } from './propagation' import { - ChatMemberUpdateHandler, InlineQueryHandler, + ChatMemberUpdateHandler, + ChosenInlineResultHandler, + InlineQueryHandler, NewMessageHandler, RawUpdateHandler, UpdateHandler, @@ -20,6 +22,7 @@ import { import { filters, UpdateFilter } from './filters' import { handlers } from './builders' import { ChatMemberUpdate } from './updates' +import { ChosenInlineResult } from './updates/chosen-inline-result' const noop = () => {} @@ -69,6 +72,11 @@ const PARSERS: Partial< 'inline_query', (client, upd, users) => new InlineQuery(client, upd as any, users), ], + updateBotInlineSend: [ + 'chosen_inline_result', + (client, upd, users) => + new ChosenInlineResult(client, upd as any, users), + ], } /** @@ -465,10 +473,7 @@ export class Dispatcher { * @param group Handler group index * @internal */ - onInlineQuery( - handler: InlineQueryHandler['callback'], - group?: number - ): void + onInlineQuery(handler: InlineQueryHandler['callback'], group?: number): void /** * Register an inline query handler with a given filter @@ -489,4 +494,36 @@ export class Dispatcher { onInlineQuery(filter: any, handler?: any, group?: number): void { this._addKnownHandler('inlineQuery', filter, handler, group) } + + /** + * Register a chosen inline result handler without any filters. + * + * @param handler Update handler + * @param group Handler group index + * @internal + */ + onChosenInlineResult( + handler: ChosenInlineResultHandler['callback'], + group?: number + ): void + + /** + * Register an inline query handler with a given filter + * + * @param filter Update filter + * @param handler Update handler + * @param group Handler group index + */ + onChosenInlineResult( + filter: UpdateFilter, + handler: ChosenInlineResultHandler< + filters.Modify + >['callback'], + group?: number + ): void + + /** @internal */ + onChosenInlineResult(filter: any, handler?: any, group?: number): void { + this._addKnownHandler('chosenInlineResult', filter, handler, group) + } } diff --git a/packages/dispatcher/src/handler.ts b/packages/dispatcher/src/handler.ts index 8f11dadc..bca38eba 100644 --- a/packages/dispatcher/src/handler.ts +++ b/packages/dispatcher/src/handler.ts @@ -1,7 +1,13 @@ -import { MaybeAsync, Message, TelegramClient, InlineQuery } from '@mtcute/client' +import { + MaybeAsync, + Message, + TelegramClient, + InlineQuery, +} from '@mtcute/client' import { tl } from '@mtcute/tl' import { PropagationSymbol } from './propagation' import { ChatMemberUpdate } from './updates' +import { ChosenInlineResult } from './updates/chosen-inline-result' interface BaseUpdateHandler { type: Type @@ -47,7 +53,13 @@ export type ChatMemberUpdateHandler = ParsedUpdateHandler< 'chat_member', T > -export type InlineQueryHandler = ParsedUpdateHandler<'inline_query', T> +export type InlineQueryHandler = ParsedUpdateHandler< + 'inline_query', + T +> +export type ChosenInlineResultHandler< + T = ChosenInlineResult +> = ParsedUpdateHandler<'chosen_inline_result', T> export type UpdateHandler = | RawUpdateHandler @@ -55,3 +67,4 @@ export type UpdateHandler = | EditMessageHandler | ChatMemberUpdateHandler | InlineQueryHandler + | ChosenInlineResultHandler diff --git a/packages/dispatcher/src/updates/chosen-inline-result.ts b/packages/dispatcher/src/updates/chosen-inline-result.ts new file mode 100644 index 00000000..69892d4b --- /dev/null +++ b/packages/dispatcher/src/updates/chosen-inline-result.ts @@ -0,0 +1,113 @@ +import { makeInspectable } from '@mtcute/client/src/types/utils' +import { tl } from '@mtcute/tl' +import { + TelegramClient, + User, + Location, + MtCuteArgumentError, +} from '@mtcute/client' +import { encodeInlineMessageId } from '@mtcute/client/src/utils/inline-utils' + +/** + * An inline result was chosen by the user and sent to some chat + * + * > **Note**: To receive these updates, you must enable + * > Inline feedback in [@BotFather](//t.me/botfather) + */ +export class ChosenInlineResult { + readonly client: TelegramClient + readonly raw: tl.RawUpdateBotInlineSend + + readonly _users: Record + + constructor( + client: TelegramClient, + raw: tl.RawUpdateBotInlineSend, + users: Record + ) { + this.client = client + this.raw = raw + this._users = users + } + + /** + * Unique identifier of the chosen result, + * as set in `InputInlineResult.id` + */ + get id(): string { + return this.raw.id + } + + private _user?: User + /** + * User who has chosen the query + */ + get user(): User { + if (!this._user) { + this._user = new User(this.client, this._users[this.raw.userId]) + } + + return this._user + } + + /** + * The query that was previously sent by the user, + * which was used to obtain this result + */ + get query(): string { + return this.raw.query + } + + private _location?: Location + /** + * Sender location, only applicable to bots that requested user location + */ + get location(): Location | null { + if (this.raw.geo?._ !== 'geoPoint') return null + + if (!this._location) { + this._location = new Location(this.raw.geo) + } + + return this._location + } + + /** + * Identifier of the sent inline message, + * which can be used in `TelegramClient.editInlineMessage` + * + * > **Note**: this is only available in case the `InputInlineMessage` + * > contained a reply keyboard markup. + */ + get messageId(): tl.TypeInputBotInlineMessageID | null { + return this.raw.msgId ?? null + } + + /** + * Identifier of the sent inline message + * as a TDLib and Bot API compatible string. + * Can be used instead of {@link messageId} in + * case you want to store it in some storage. + * + * > **Note**: this is only available in case the `InputInlineMessage` + * > contained a reply keyboard markup. + */ + get messageIdStr(): string | null { + if (!this.raw.msgId) return null + + return encodeInlineMessageId(this.raw.msgId) + } + + async editMessage( + params: Parameters[1] + ): Promise { + if (!this.raw.msgId) + throw new MtCuteArgumentError( + 'No message ID, make sure you have included reply markup!' + ) + + return this.client.editInlineMessage(this.raw.msgId, params) + } +} + +makeInspectable(ChosenInlineResult)