feat: support business connections

This commit is contained in:
alina 🌸 2024-05-06 03:12:33 +03:00
parent c021f64ed0
commit 2a399532a3
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
36 changed files with 857 additions and 49 deletions

View file

@ -21,3 +21,8 @@ story: StoryUpdate = StoryUpdate
delete_story = DeleteStoryUpdate
bot_reaction: BotReactionUpdate = BotReactionUpdate
bot_reaction_count: BotReactionCountUpdate = BotReactionCountUpdate
business_connection: BusinessConnectionUpdate = BusinessConnection
new_business_message = BusinessMessage + State in BusinessMessageContext
edit_business_message = BusinessMessage + State in BusinessMessageContext
business_message_group = BusinessMessage[] + State in BusinessMessageContext
delete_business_message = DeleteBusinessMessageUpdate

View file

@ -183,6 +183,7 @@ import { deleteBusinessChatLink, editBusinessChatLink } from './methods/premium/
import { getBoostStats } from './methods/premium/get-boost-stats.js'
import { getBoosts } from './methods/premium/get-boosts.js'
import { getBusinessChatLinks } from './methods/premium/get-business-chat-links.js'
import { getBusinessConnection } from './methods/premium/get-business-connection.js'
import { getMyBoostSlots } from './methods/premium/get-my-boost-slots.js'
import { iterBoosters } from './methods/premium/iter-boosters.js'
import { setBusinessIntro } from './methods/premium/set-business-intro.js'
@ -255,6 +256,8 @@ import {
BotReactionUpdate,
BotStoppedUpdate,
BusinessChatLink,
BusinessConnection,
BusinessMessage,
BusinessWorkHoursDay,
CallbackQuery,
Chat,
@ -267,6 +270,7 @@ import {
ChatPreview,
ChosenInlineResult,
CollectibleInfo,
DeleteBusinessMessageUpdate,
DeleteMessageUpdate,
DeleteStoryUpdate,
Dialog,
@ -524,6 +528,41 @@ export interface TelegramClient extends ITelegramClient {
* @param handler Bot reaction count update handler
*/
on(name: 'bot_reaction_count', handler: (upd: BotReactionCountUpdate) => void): this
/**
* Register a business connection update handler
*
* @param name Event name
* @param handler Business connection update handler
*/
on(name: 'business_connection', handler: (upd: BusinessConnection) => void): this
/**
* Register a new business message handler
*
* @param name Event name
* @param handler New business message handler
*/
on(name: 'new_business_message', handler: (upd: BusinessMessage) => void): this
/**
* Register an edit business message handler
*
* @param name Event name
* @param handler Edit business message handler
*/
on(name: 'edit_business_message', handler: (upd: BusinessMessage) => void): this
/**
* Register a business message group handler
*
* @param name Event name
* @param handler Business message group handler
*/
on(name: 'business_message_group', handler: (upd: BusinessMessage[]) => void): this
/**
* Register a delete business message handler
*
* @param name Event name
* @param handler Delete business message handler
*/
on(name: 'delete_business_message', handler: (upd: DeleteBusinessMessageUpdate) => void): this
// eslint-disable-next-line @typescript-eslint/no-explicit-any
on(name: string, handler: (...args: any[]) => void): this
@ -2338,6 +2377,7 @@ export interface TelegramClient extends ITelegramClient {
params?: {
progressCallback?: (uploaded: number, total: number) => void
uploadPeer?: tl.TypeInputPeer
businessConnectionId?: string
},
uploadMedia?: boolean,
): Promise<tl.TypeInputMedia>
@ -3123,6 +3163,8 @@ export interface TelegramClient extends ITelegramClient {
* client render the preview above the caption and not below.
*/
invertMedia?: boolean
businessConnectionId?: string
},
): Promise<Message>
/**
@ -4009,6 +4051,11 @@ export interface TelegramClient extends ITelegramClient {
*/
progress?: number
/**
* Unique identifier of the business connection on behalf of which the action will be sent
*/
businessConnectionId?: string
/**
* For comment threads, ID of the thread (i.e. top message)
*/
@ -4263,6 +4310,15 @@ export interface TelegramClient extends ITelegramClient {
*
*/
getBusinessChatLinks(): Promise<BusinessChatLink[]>
/**
* Get information about the connection of the bot with a business account
*
* **Available**: 🤖 bots only
*
* @param connectionId ID of the business connection
*/
getBusinessConnection(connectionId: string): Promise<BusinessConnection>
/**
* Get boost slots information of the current user.
*
@ -5935,6 +5991,9 @@ TelegramClient.prototype.getBoosts = function (...args) {
TelegramClient.prototype.getBusinessChatLinks = function (...args) {
return getBusinessChatLinks(this._client, ...args)
}
TelegramClient.prototype.getBusinessConnection = function (...args) {
return getBusinessConnection(this._client, ...args)
}
TelegramClient.prototype.getMyBoostSlots = function (...args) {
return getMyBoostSlots(this._client, ...args)
}

View file

@ -199,6 +199,7 @@ export { deleteBusinessChatLink } from './methods/premium/edit-business-chat-lin
export { getBoostStats } from './methods/premium/get-boost-stats.js'
export { getBoosts } from './methods/premium/get-boosts.js'
export { getBusinessChatLinks } from './methods/premium/get-business-chat-links.js'
export { getBusinessConnection } from './methods/premium/get-business-connection.js'
export { getMyBoostSlots } from './methods/premium/get-my-boost-slots.js'
export { iterBoosters } from './methods/premium/iter-boosters.js'
export { setBusinessIntro } from './methods/premium/set-business-intro.js'

View file

@ -27,6 +27,8 @@ import {
BotReactionUpdate,
BotStoppedUpdate,
BusinessChatLink,
BusinessConnection,
BusinessMessage,
BusinessWorkHoursDay,
CallbackQuery,
Chat,
@ -39,6 +41,7 @@ import {
ChatPreview,
ChosenInlineResult,
CollectibleInfo,
DeleteBusinessMessageUpdate,
DeleteMessageUpdate,
DeleteStoryUpdate,
Dialog,

View file

@ -28,6 +28,7 @@ export async function _normalizeInputMedia(
params: {
progressCallback?: (uploaded: number, total: number) => void
uploadPeer?: tl.TypeInputPeer
businessConnectionId?: string
} = {},
uploadMedia = false,
): Promise<tl.TypeInputMedia> {
@ -254,6 +255,7 @@ export async function _normalizeInputMedia(
_: 'messages.uploadMedia',
peer: uploadPeer,
media: inputMedia,
businessConnectionId: params.businessConnectionId,
})
if (photo) {

View file

@ -0,0 +1,59 @@
import { tl } from '@mtcute/tl'
import { RpcCallOptions } from '../../../network/network-manager.js'
import { LruMap } from '../../../utils/lru-map.js'
import { ITelegramClient } from '../../client.types.js'
import { getBusinessConnection } from '../premium/get-business-connection.js'
// temporary solution
// todo rework once we have either a more generic caching solution
const DC_MAP_SYMBOL = Symbol('dcMap')
const getDcMap = (client: ITelegramClient): LruMap<string, number> => {
const client_ = client as typeof client & { [DC_MAP_SYMBOL]?: LruMap<string, number> }
if (!client_[DC_MAP_SYMBOL]) {
client_[DC_MAP_SYMBOL] = new LruMap(50)
}
return client_[DC_MAP_SYMBOL]
}
// @available=both
/**
* @internal
*/
export async function _maybeInvokeWithBusinessConnection<T extends tl.RpcMethod>(
client: ITelegramClient,
businessConnectionId: string | undefined,
request: T,
params?: RpcCallOptions,
): Promise<tl.RpcCallReturn[T['_']]> {
if (!businessConnectionId) {
return client.call(request, params)
}
const dcMap = getDcMap(client)
if (!dcMap.has(businessConnectionId)) {
const res = await getBusinessConnection(client, businessConnectionId)
dcMap.set(businessConnectionId, res.dcId)
}
const dcId = dcMap.get(businessConnectionId)!
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return client.call(
{
_: 'invokeWithBusinessConnection',
connectionId: businessConnectionId,
query: request,
},
{
...params,
localMigrate: true, // just in case
dcId,
},
)
}

View file

@ -13,6 +13,7 @@ import {
import { _normalizeInputMedia } from '../files/normalize-input-media.js'
import { _normalizeInputText } from '../misc/normalize-text.js'
import { resolvePeer } from '../users/resolve-peer.js'
import { _maybeInvokeWithBusinessConnection } from './_business-connection.js'
import { _findMessageInUpdate } from './find-in-update.js'
/**
@ -75,6 +76,8 @@ export async function editMessage(
* client render the preview above the caption and not below.
*/
invertMedia?: boolean
businessConnectionId?: string
},
): Promise<Message> {
const { chatId, message } = normalizeInputMessageId(params)
@ -96,7 +99,7 @@ export async function editMessage(
[content, entities] = await _normalizeInputText(client, params.text)
}
const res = await client.call({
const res = await _maybeInvokeWithBusinessConnection(client, params.businessConnectionId, {
_: 'messages.editMessage',
id: message,
peer: await resolvePeer(client, chatId),

View file

@ -57,14 +57,23 @@ export function _findMessageInUpdate(
}
if (isEdit) {
if (!(u._ === 'updateEditMessage' || u._ === 'updateEditChannelMessage')) continue
if (
!(
u._ === 'updateEditMessage' ||
u._ === 'updateEditChannelMessage' ||
u._ === 'updateBotEditBusinessMessage'
)
) {
continue
}
} else {
if (
!(
u._ === 'updateNewMessage' ||
u._ === 'updateNewChannelMessage' ||
u._ === 'updateNewScheduledMessage' ||
u._ === 'updateQuickReplyMessage'
u._ === 'updateQuickReplyMessage' ||
u._ === 'updateBotNewBusinessMessage'
)
) {
continue

View file

@ -111,6 +111,12 @@ export interface CommonSendParams {
* to the client's update handler.
*/
shouldDispatch?: true
/**
* Unique identifier of the business connection on behalf of which
* the message will be sent
*/
businessConnectionId?: string
}
/**

View file

@ -9,6 +9,7 @@ import { assertIsUpdatesGroup } from '../../updates/utils.js'
import { _normalizeInputMedia } from '../files/normalize-input-media.js'
import { _normalizeInputText } from '../misc/normalize-text.js'
import { resolvePeer } from '../users/resolve-peer.js'
import { _maybeInvokeWithBusinessConnection } from './_business-connection.js'
import { _getDiscussionMessage } from './get-discussion-message.js'
import { _processCommonSendParameters, CommonSendParams } from './send-common.js'
@ -77,6 +78,7 @@ export async function sendMediaGroup(
// but otherwise Telegram throws MEDIA_INVALID
// fuck my life
uploadPeer: peer,
businessConnectionId: params.businessConnectionId,
},
true,
)
@ -97,7 +99,9 @@ export async function sendMediaGroup(
})
}
const res = await client.call(
const res = await _maybeInvokeWithBusinessConnection(
client,
params.businessConnectionId,
{
_: 'messages.sendMultiMedia',
peer,

View file

@ -8,6 +8,7 @@ import { InputPeerLike } from '../../types/peers/index.js'
import { _normalizeInputMedia } from '../files/normalize-input-media.js'
import { _normalizeInputText } from '../misc/normalize-text.js'
import { resolvePeer } from '../users/resolve-peer.js'
import { _maybeInvokeWithBusinessConnection } from './_business-connection.js'
import { _findMessageInUpdate } from './find-in-update.js'
import { _getDiscussionMessage } from './get-discussion-message.js'
import { _processCommonSendParameters, CommonSendParams } from './send-common.js'
@ -87,7 +88,9 @@ export async function sendMedia(
)
const randomId = randomLong()
const res = await client.call(
const res = await _maybeInvokeWithBusinessConnection(
client,
params.businessConnectionId,
{
_: 'messages.sendMedia',
peer,

View file

@ -13,6 +13,7 @@ import { inputPeerToPeer } from '../../utils/peer-utils.js'
import { _getRawPeerBatched } from '../chats/batched-queries.js'
import { _normalizeInputText } from '../misc/normalize-text.js'
import { resolvePeer } from '../users/resolve-peer.js'
import { _maybeInvokeWithBusinessConnection } from './_business-connection.js'
import { _findMessageInUpdate } from './find-in-update.js'
import { _getDiscussionMessage } from './get-discussion-message.js'
import { _processCommonSendParameters, CommonSendParams } from './send-common.js'
@ -61,7 +62,9 @@ export async function sendText(
)
const randomId = randomLong()
const res = await client.call(
const res = await _maybeInvokeWithBusinessConnection(
client,
params.businessConnectionId,
{
_: 'messages.sendMessage',
peer,

View file

@ -5,6 +5,7 @@ import { assertTrue } from '../../../utils/type-assertions.js'
import { ITelegramClient } from '../../client.types.js'
import { InputPeerLike, TypingStatus } from '../../types/index.js'
import { resolvePeer } from '../users/resolve-peer.js'
import { _maybeInvokeWithBusinessConnection } from './_business-connection.js'
/**
* Sends a current user/bot typing event
@ -28,6 +29,11 @@ export async function sendTyping(
*/
progress?: number
/**
* Unique identifier of the business connection on behalf of which the action will be sent
*/
businessConnectionId?: string
/**
* For comment threads, ID of the thread (i.e. top message)
*/
@ -91,7 +97,7 @@ export async function sendTyping(
}
}
const r = await client.call({
const r = await _maybeInvokeWithBusinessConnection(client, params?.businessConnectionId, {
_: 'messages.setTyping',
peer: await resolvePeer(client, chatId),
action: status,

View file

@ -0,0 +1,28 @@
import { assertTypeIs } from '../../../utils/type-assertions.js'
import { ITelegramClient } from '../../client.types.js'
import { PeersIndex } from '../../types/peers/peers-index.js'
import { BusinessConnection } from '../../types/premium/business-connection.js'
import { assertIsUpdatesGroup } from '../../updates/utils.js'
// @available=bot
/**
* Get information about the connection of the bot with a business account
*
* @param connectionId ID of the business connection
*/
export async function getBusinessConnection(
client: ITelegramClient,
connectionId: string,
): Promise<BusinessConnection> {
const res = await client.call({
_: 'account.getBotBusinessConnection',
connectionId,
})
assertIsUpdatesGroup('account.getBotBusinessConnection', res)
assertTypeIs('account.getBotBusinessConnection', res.updates[0], 'updateBotBusinessConnect')
const peers = PeersIndex.from(res)
return new BusinessConnection(res.updates[0].connection, peers)
}

View file

@ -194,21 +194,7 @@ export class Dialog {
* Chat that this dialog represents
*/
get chat(): Chat {
const peer = this.raw.peer
let chat
switch (peer._) {
case 'peerChannel':
case 'peerChat':
chat = this._peers.chat(peer._ === 'peerChannel' ? peer.channelId : peer.chatId)
break
default:
chat = this._peers.user(peer.userId)
break
}
return new Chat(chat)
return Chat._parseFromPeer(this.raw.peer, this._peers)
}
/**

View file

@ -253,6 +253,20 @@ export class Message {
return new User(this._peers.user(this.raw.viaBotId))
}
/**
* If this message was sent by a business bot on behalf of {@link sender},
* information about the business bot.
*
* **Note**: only available to the business account and the bot itself.
*/
get viaBusinessBot(): User | null {
if (this.raw._ === 'messageService' || !this.raw.viaBusinessBotId) {
return null
}
return new User(this._peers.user(this.raw.viaBusinessBotId))
}
/**
* Message text or media caption.
*

View file

@ -0,0 +1,49 @@
import { tl } from '@mtcute/tl'
import { makeInspectable } from '../../utils/inspectable.js'
import { memoizeGetters } from '../../utils/memoize.js'
import { PeersIndex } from '../peers/peers-index.js'
import { User } from '../peers/user.js'
/**
* Describes the connection of the bot with a business account.
*/
export class BusinessConnection {
constructor(
readonly raw: tl.TypeBotBusinessConnection,
readonly _peers: PeersIndex,
) {}
/** Whether the connection was removed by the user */
get isRemoved(): boolean {
return this.raw.disabled!
}
/** ID of the connection */
get id(): string {
return this.raw.connectionId
}
/** Datacenter ID of the connected user */
get dcId(): number {
return this.raw.dcId
}
/** Date when the connection was created */
get date(): Date {
return new Date(this.raw.date * 1000)
}
/** Whether the bot can reply on behalf of the user */
get canReply(): boolean {
return this.raw.canReply!
}
/** Business account user that created the business connection */
get user(): User {
return new User(this._peers.user(this.raw.userId))
}
}
makeInspectable(BusinessConnection)
memoizeGetters(BusinessConnection, ['user'])

View file

@ -3,5 +3,6 @@ export * from './boost-slot.js'
export * from './boost-stats.js'
export * from './business-account.js'
export * from './business-chat-link.js'
export * from './business-connection.js'
export * from './business-intro.js'
export * from './business-work-hours.js'

View file

@ -0,0 +1,32 @@
import { tl } from '@mtcute/tl'
import { Message } from '../messages/message.js'
import { PeersIndex } from '../peers/peers-index.js'
/**
* Update about a new or edited business message.
*/
export class BusinessMessage extends Message {
constructor(
readonly update: tl.RawUpdateBotNewBusinessMessage | tl.RawUpdateBotEditBusinessMessage,
readonly _peers: PeersIndex,
) {
super(update.message, _peers)
}
/**
* Unique identifier of the business connection from which the message was received.
*/
get connectionId(): string {
return this.update.connectionId
}
get groupedIdUnique(): string {
return `${super.groupedIdUnique}|${this.update.connectionId}`
}
/** The replied message (if any) */
get replyTo(): Message | null {
return this.update.replyToMessage ? new Message(this.update.replyToMessage, this._peers) : null
}
}

View file

@ -0,0 +1,38 @@
import { tl } from '@mtcute/tl'
import { makeInspectable } from '../../utils/index.js'
import { memoizeGetters } from '../../utils/memoize.js'
import { Chat } from '../peers/chat.js'
import { PeersIndex } from '../peers/peers-index.js'
/**
* One or more messages were deleted from a connected business account
*/
export class DeleteBusinessMessageUpdate {
constructor(
readonly raw: tl.RawUpdateBotDeleteBusinessMessage,
readonly _peers: PeersIndex,
) {}
/** Unique identifier of the business connection */
get connectionId(): string {
return this.raw.connectionId
}
/**
* IDs of the messages which were deleted
*/
get messageIds(): number[] {
return this.raw.messages
}
/**
* Chat where the messages were deleted
*/
get chat(): Chat {
return Chat._parseFromPeer(this.raw.peer, this._peers)
}
}
makeInspectable(DeleteBusinessMessageUpdate)
memoizeGetters(DeleteBusinessMessageUpdate, ['chat'])

View file

@ -1,4 +1,4 @@
import type { Message } from '../../types/index.js'
import type { BusinessConnection, Message } from '../../types/index.js'
import { BotChatJoinRequestUpdate } from './bot-chat-join-request.js'
import { BotStoppedUpdate } from './bot-stopped.js'
import { CallbackQuery, InlineCallbackQuery } from './callback-query.js'
@ -7,7 +7,9 @@ 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 { BusinessMessage } from './business-message.js'
import { ChosenInlineResult } from './chosen-inline-result.js'
import { DeleteBusinessMessageUpdate } from './delete-business-message-update.js'
import { DeleteMessageUpdate } from './delete-message-update.js'
import { DeleteStoryUpdate } from './delete-story-update.js'
import { HistoryReadUpdate } from './history-read-update.js'
@ -23,10 +25,12 @@ export {
BotReactionCountUpdate,
BotReactionUpdate,
BotStoppedUpdate,
BusinessMessage,
CallbackQuery,
ChatJoinRequestUpdate,
ChatMemberUpdate,
ChosenInlineResult,
DeleteBusinessMessageUpdate,
DeleteMessageUpdate,
DeleteStoryUpdate,
HistoryReadUpdate,
@ -64,5 +68,10 @@ export type ParsedUpdate =
| { name: 'delete_story'; data: DeleteStoryUpdate }
| { name: 'bot_reaction'; data: BotReactionUpdate }
| { name: 'bot_reaction_count'; data: BotReactionCountUpdate }
| { name: 'business_connection'; data: BusinessConnection }
| { name: 'new_business_message'; data: BusinessMessage }
| { name: 'edit_business_message'; data: BusinessMessage }
| { name: 'business_message_group'; data: BusinessMessage[] }
| { name: 'delete_business_message'; data: DeleteBusinessMessageUpdate }
// end-codegen

View file

@ -6,10 +6,13 @@ import {
BotReactionCountUpdate,
BotReactionUpdate,
BotStoppedUpdate,
BusinessConnection,
BusinessMessage,
CallbackQuery,
ChatJoinRequestUpdate,
ChatMemberUpdate,
ChosenInlineResult,
DeleteBusinessMessageUpdate,
DeleteMessageUpdate,
DeleteStoryUpdate,
HistoryReadUpdate,
@ -94,6 +97,14 @@ export function _parseUpdate(update: tl.TypeUpdate, peers: PeersIndex): ParsedUp
return { name: 'bot_reaction', data: new BotReactionUpdate(update, peers) }
case 'updateBotMessageReactions':
return { name: 'bot_reaction_count', data: new BotReactionCountUpdate(update, peers) }
case 'updateBotBusinessConnect':
return { name: 'business_connection', data: new BusinessConnection(update.connection, peers) }
case 'updateBotNewBusinessMessage':
return { name: 'new_business_message', data: new BusinessMessage(update, peers) }
case 'updateBotEditBusinessMessage':
return { name: 'edit_business_message', data: new BusinessMessage(update, peers) }
case 'updateBotDeleteBusinessMessage':
return { name: 'delete_business_message', data: new DeleteBusinessMessageUpdate(update, peers) }
default:
return null
}

View file

@ -523,7 +523,8 @@ export class UpdatesManager {
switch (upd._) {
case 'updateNewMessage':
case 'updateNewChannelMessage': {
case 'updateNewChannelMessage':
case 'updateBotNewBusinessMessage': {
const channelId = upd.message.peerId?._ === 'peerChannel' ? upd.message.peerId.channelId : 0
const set = noDispatchMsg.get(channelId)
@ -1260,7 +1261,11 @@ export class UpdatesManager {
if (this.noDispatchEnabled) {
const channelId = pending.channelId ?? 0
const msgId =
upd._ === 'updateNewMessage' || upd._ === 'updateNewChannelMessage' ? upd.message.id : undefined
upd._ === 'updateNewMessage' ||
upd._ === 'updateNewChannelMessage' ||
upd._ === 'updateBotNewBusinessMessage' ?
upd.message.id :
undefined
// we first need to remove it from each index, and then check if it was there
const foundByMsgId = msgId && this.noDispatchMsg.get(channelId)?.delete(msgId)

View file

@ -1,5 +1,5 @@
import { Message } from '../types/messages/index.js'
import { ParsedUpdate } from '../types/updates/index.js'
import { BusinessMessage, ParsedUpdate } from '../types/updates/index.js'
import { _parseUpdate } from '../types/updates/parse-update.js'
import { RawUpdateHandler } from './types.js'
@ -54,10 +54,11 @@ export function makeParsedUpdateHandler(params: ParsedUpdateHandlerParams): RawU
onRawUpdate(update, peers)
if (parsed) {
if (parsed.name === 'new_message') {
if (parsed.name === 'new_message' || parsed.name === 'new_business_message') {
const group = parsed.data.groupedIdUnique
if (group) {
const isBusiness = parsed.name === 'new_business_message'
const pendingGroup = pending.get(group)
if (pendingGroup) {
@ -66,7 +67,12 @@ export function makeParsedUpdateHandler(params: ParsedUpdateHandlerParams): RawU
const messages = [parsed.data]
const timeout = setTimeout(() => {
pending.delete(group)
onUpdate({ name: 'message_group', data: messages })
if (isBusiness) {
onUpdate({ name: 'business_message_group', data: messages as BusinessMessage[] })
} else {
onUpdate({ name: 'message_group', data: messages })
}
}, messageGroupingInterval)
pending.set(group, [messages, timeout])

View file

@ -173,6 +173,15 @@ export interface RpcCallOptions {
*/
throw503?: boolean
/**
* Whether the `X_MIGRATE_%d` errors should be handled locally on request level
* instead of changing the default datacenter for the entire client.
*
* Useful for `invokeWithBusinessConnection`, as it returns a `USER_MIGRATE_%d` error
* that is in fact not related to the user, but to the specific request.
*/
localMigrate?: boolean
/**
* Some requests should be processed consecutively, and not in parallel.
* Using the same `chainId` for multiple requests will ensure that they are processed in the order
@ -807,10 +816,15 @@ export class NetworkManager {
if (manager === this._primaryDc) {
if (e.is('PHONE_MIGRATE_%d') || e.is('NETWORK_MIGRATE_%d') || e.is('USER_MIGRATE_%d')) {
this._log.info('Migrate error, new dc = %d', e.newDc)
if (params?.localMigrate) {
manager = await this._getOtherDc(e.newDc)
} else {
this._log.info('Migrate error, new dc = %d', e.newDc)
await this.changePrimaryDc(e.newDc)
manager = this._primaryDc!
}
await this.changePrimaryDc(e.newDc)
manager = this._primaryDc!
multi = manager[kind]
continue

View file

@ -1,4 +1,4 @@
const { types, toSentence, replaceSections, formatFile } = require('../../mtcute/scripts/generate-updates.cjs')
const { types, toSentence, replaceSections, formatFile } = require('../../core/scripts/generate-updates.cjs')
function generateHandler() {
const lines = []

View file

@ -0,0 +1,187 @@
import { BusinessMessage, OmitInputMessageId, ParametersSkip1 } from '@mtcute/core'
import { TelegramClient } from '@mtcute/core/client.js'
import {
DeleteMessagesParams,
ForwardMessageOptions,
SendCopyGroupParams,
SendCopyParams,
} from '@mtcute/core/methods.js'
import { UpdateContext } from './base.js'
/**
* Context of a business message related update.
*
* This is a subclass of {@link BusinessMessage}, so all fields
* of the message are available.
*
* For message groups, own fields are related to the last message
* in the group. To access all messages, use {@link BusinessMessageContext#messages}.
*/
export class BusinessMessageContext extends BusinessMessage implements UpdateContext<BusinessMessage> {
// this is primarily for proper types in filters, so don't bother much with actual value
readonly _name = 'new_business_message'
/**
* List of messages in the message group.
*
* For other updates, this is a list with a single element (`this`).
*/
readonly messages: BusinessMessageContext[]
/** Whether this update is about a message group */
readonly isMessageGroup: boolean
constructor(
readonly client: TelegramClient,
message: BusinessMessage | BusinessMessage[],
) {
const msg = Array.isArray(message) ? message[message.length - 1] : message
super(msg.update, msg._peers)
this.messages = Array.isArray(message) ? message.map((it) => new BusinessMessageContext(client, it)) : [this]
this.isMessageGroup = Array.isArray(message)
}
/** Get all custom emojis contained in this message (message group), if any */
getCustomEmojis() {
return this.client.getCustomEmojisFromMessages(this.messages)
}
/** Send a text message to the same chat (and topic, if applicable) as a given message */
answerText(...params: ParametersSkip1<TelegramClient['answerText']>) {
const [send, params_ = {}] = params
params_.businessConnectionId = this.update.connectionId
return this.client.answerText(this, send, params_)
}
/** Send a media to the same chat (and topic, if applicable) as a given message */
answerMedia(...params: ParametersSkip1<TelegramClient['answerMedia']>) {
const [send, params_ = {}] = params
params_.businessConnectionId = this.update.connectionId
return this.client.answerMedia(this, send, params_)
}
/** Send a media group to the same chat (and topic, if applicable) as a given message */
answerMediaGroup(...params: ParametersSkip1<TelegramClient['answerMediaGroup']>) {
const [send, params_ = {}] = params
params_.businessConnectionId = this.update.connectionId
return this.client.answerMediaGroup(this, send, params_)
}
/** Send a text message in reply to this message */
replyText(...params: ParametersSkip1<TelegramClient['replyText']>) {
const [send, params_ = {}] = params
params_.businessConnectionId = this.update.connectionId
return this.client.replyText(this, send, params_)
}
/** Send a media in reply to this message */
replyMedia(...params: ParametersSkip1<TelegramClient['replyMedia']>) {
const [send, params_ = {}] = params
params_.businessConnectionId = this.update.connectionId
return this.client.replyMedia(this, send, params_)
}
/** Send a media group in reply to this message */
replyMediaGroup(...params: ParametersSkip1<TelegramClient['replyMediaGroup']>) {
const [send, params_ = {}] = params
params_.businessConnectionId = this.update.connectionId
return this.client.replyMediaGroup(this, send, params_)
}
/** Send a text message in reply to this message */
quoteWithText(params: Parameters<TelegramClient['quoteWithText']>[1]) {
params.businessConnectionId = this.update.connectionId
return this.client.quoteWithText(this, params)
}
/** Send a media in reply to this message */
quoteWithMedia(params: Parameters<TelegramClient['quoteWithMedia']>[1]) {
params.businessConnectionId = this.update.connectionId
return this.client.quoteWithMedia(this, params)
}
/** Send a media group in reply to this message */
quoteWithMediaGroup(params: Parameters<TelegramClient['quoteWithMediaGroup']>[1]) {
params.businessConnectionId = this.update.connectionId
return this.client.quoteWithMediaGroup(this, params)
}
/** Delete this message (message group) */
delete(params?: DeleteMessagesParams) {
return this.client.deleteMessagesById(
this.chat.inputPeer,
this.messages.map((it) => it.id),
params,
)
}
/** Pin this message */
pin(params?: OmitInputMessageId<Parameters<TelegramClient['pinMessage']>[0]>) {
return this.client.pinMessage({
chatId: this.chat.inputPeer,
message: this.id,
...params,
})
}
/** Unpin this message */
unpin() {
return this.client.unpinMessage({
chatId: this.chat.inputPeer,
message: this.id,
})
}
/** Edit this message */
edit(params: OmitInputMessageId<Parameters<TelegramClient['editMessage']>[0]>) {
return this.client.editMessage({
chatId: this.chat.inputPeer,
message: this.id,
...params,
})
}
/** Forward this message (message group) */
forwardTo(params: ForwardMessageOptions) {
return this.client.forwardMessagesById({
fromChatId: this.chat.inputPeer,
messages: this.messages.map((it) => it.id),
...params,
})
}
/** Send a copy of this message (message group) */
copy(params: SendCopyParams & SendCopyGroupParams) {
if (this.isMessageGroup) {
return this.client.sendCopyGroup({
messages: this.messages,
...params,
})
}
return this.client.sendCopy({
message: this,
...params,
})
}
/** React to this message */
react(params: OmitInputMessageId<Parameters<TelegramClient['sendReaction']>[0]>) {
return this.client.sendReaction({
chatId: this.chat.inputPeer,
message: this.id,
...params,
})
}
}

View file

@ -2,6 +2,7 @@ import { ParsedUpdate } from '@mtcute/core'
import { TelegramClient } from '@mtcute/core/client.js'
import { UpdateContextDistributed } from './base.js'
import { BusinessMessageContext } from './business-message.js'
import { CallbackQueryContext } from './callback-query.js'
import { ChatJoinRequestUpdateContext } from './chat-join-request.js'
import { ChosenInlineResultContext } from './chosen-inline-result.js'
@ -26,6 +27,10 @@ export function _parsedUpdateToContext(client: TelegramClient, update: ParsedUpd
return new ChatJoinRequestUpdateContext(client, update.data)
case 'pre_checkout_query':
return new PreCheckoutQueryContext(client, update.data)
case 'new_business_message':
case 'edit_business_message':
case 'business_message_group':
return new BusinessMessageContext(client, update.data)
}
const _update = update.data as UpdateContextDistributed<typeof update.data>

View file

@ -7,8 +7,10 @@ import {
BotReactionCountUpdate,
BotReactionUpdate,
BotStoppedUpdate,
BusinessConnection,
ChatJoinRequestUpdate,
ChatMemberUpdate,
DeleteBusinessMessageUpdate,
DeleteMessageUpdate,
DeleteStoryUpdate,
HistoryReadUpdate,
@ -26,6 +28,7 @@ import {
import { TelegramClient } from '@mtcute/core/client.js'
import { UpdateContext } from './context/base.js'
import { BusinessMessageContext } from './context/business-message.js'
import {
CallbackQueryContext,
ChatJoinRequestUpdateContext,
@ -43,17 +46,22 @@ import {
BotReactionCountUpdateHandler,
BotReactionUpdateHandler,
BotStoppedHandler,
BusinessConnectionUpdateHandler,
BusinessMessageGroupHandler,
CallbackQueryHandler,
ChatJoinRequestHandler,
ChatMemberUpdateHandler,
ChosenInlineResultHandler,
DeleteBusinessMessageHandler,
DeleteMessageHandler,
DeleteStoryHandler,
EditBusinessMessageHandler,
EditMessageHandler,
HistoryReadHandler,
InlineCallbackQueryHandler,
InlineQueryHandler,
MessageGroupHandler,
NewBusinessMessageHandler,
NewMessageHandler,
PollUpdateHandler,
PollVoteHandler,
@ -1014,9 +1022,9 @@ export class Dispatcher<State extends object = never> {
if (typeof handler === 'number' || typeof handler === 'undefined') {
this.addUpdateHandler(
{
name,
name: name,
callback: filter,
},
} as UpdateHandler,
handler,
)
} else {
@ -1025,7 +1033,7 @@ export class Dispatcher<State extends object = never> {
name,
callback: handler,
check: filter,
},
} as UpdateHandler,
group,
)
}
@ -1743,5 +1751,212 @@ export class Dispatcher<State extends object = never> {
this._addKnownHandler('bot_reaction_count', filter, handler, group)
}
/**
* Register a business connection update handler without any filters
*
* @param handler Business connection update handler
* @param group Handler group index
*/
onBusinessConnectionUpdate(handler: BusinessConnectionUpdateHandler['callback'], group?: number): void
/**
* Register a business connection update handler with a filter
*
* @param filter Update filter
* @param handler Business connection update handler
* @param group Handler group index
*/
onBusinessConnectionUpdate<Mod>(
filter: UpdateFilter<UpdateContext<BusinessConnection>, Mod>,
handler: BusinessConnectionUpdateHandler<filters.Modify<UpdateContext<BusinessConnection>, Mod>>['callback'],
group?: number,
): void
/** @internal */
onBusinessConnectionUpdate(filter: any, handler?: any, group?: number): void {
this._addKnownHandler('business_connection', filter, handler, group)
}
/**
* Register a new business message handler without any filters
*
* @param handler New business message handler
* @param group Handler group index
*/
onNewBusinessMessage(
handler: NewBusinessMessageHandler<
BusinessMessageContext,
State extends never ? never : UpdateState<State>
>['callback'],
group?: number,
): void
/**
* Register a new business message handler with a filter
*
* @param filter Update filter
* @param handler New business message handler
* @param group Handler group index
*/
onNewBusinessMessage<Mod>(
filter: UpdateFilter<BusinessMessageContext, Mod, State>,
handler: NewBusinessMessageHandler<
filters.Modify<BusinessMessageContext, Mod>,
State extends never ? never : UpdateState<State>
>['callback'],
group?: number,
): void
/**
* Register a new business message handler with a filter
*
* @param filter Update filter
* @param handler New business message handler
* @param group Handler group index
*/
onNewBusinessMessage<Mod>(
filter: UpdateFilter<BusinessMessageContext, Mod>,
handler: NewBusinessMessageHandler<
filters.Modify<BusinessMessageContext, Mod>,
State extends never ? never : UpdateState<State>
>['callback'],
group?: number,
): void
/** @internal */
onNewBusinessMessage(filter: any, handler?: any, group?: number): void {
this._addKnownHandler('new_business_message', filter, handler, group)
}
/**
* Register an edit business message handler without any filters
*
* @param handler Edit business message handler
* @param group Handler group index
*/
onEditBusinessMessage(
handler: EditBusinessMessageHandler<
BusinessMessageContext,
State extends never ? never : UpdateState<State>
>['callback'],
group?: number,
): void
/**
* Register an edit business message handler with a filter
*
* @param filter Update filter
* @param handler Edit business message handler
* @param group Handler group index
*/
onEditBusinessMessage<Mod>(
filter: UpdateFilter<BusinessMessageContext, Mod, State>,
handler: EditBusinessMessageHandler<
filters.Modify<BusinessMessageContext, Mod>,
State extends never ? never : UpdateState<State>
>['callback'],
group?: number,
): void
/**
* Register an edit business message handler with a filter
*
* @param filter Update filter
* @param handler Edit business message handler
* @param group Handler group index
*/
onEditBusinessMessage<Mod>(
filter: UpdateFilter<BusinessMessageContext, Mod>,
handler: EditBusinessMessageHandler<
filters.Modify<BusinessMessageContext, Mod>,
State extends never ? never : UpdateState<State>
>['callback'],
group?: number,
): void
/** @internal */
onEditBusinessMessage(filter: any, handler?: any, group?: number): void {
this._addKnownHandler('edit_business_message', filter, handler, group)
}
/**
* Register a business message group handler without any filters
*
* @param handler Business message group handler
* @param group Handler group index
*/
onBusinessMessageGroup(
handler: BusinessMessageGroupHandler<
BusinessMessageContext,
State extends never ? never : UpdateState<State>
>['callback'],
group?: number,
): void
/**
* Register a business message group handler with a filter
*
* @param filter Update filter
* @param handler Business message group handler
* @param group Handler group index
*/
onBusinessMessageGroup<Mod>(
filter: UpdateFilter<BusinessMessageContext, Mod, State>,
handler: BusinessMessageGroupHandler<
filters.Modify<BusinessMessageContext, Mod>,
State extends never ? never : UpdateState<State>
>['callback'],
group?: number,
): void
/**
* Register a business message group handler with a filter
*
* @param filter Update filter
* @param handler Business message group handler
* @param group Handler group index
*/
onBusinessMessageGroup<Mod>(
filter: UpdateFilter<BusinessMessageContext, Mod>,
handler: BusinessMessageGroupHandler<
filters.Modify<BusinessMessageContext, Mod>,
State extends never ? never : UpdateState<State>
>['callback'],
group?: number,
): void
/** @internal */
onBusinessMessageGroup(filter: any, handler?: any, group?: number): void {
this._addKnownHandler('business_message_group', filter, handler, group)
}
/**
* Register a delete business message handler without any filters
*
* @param handler Delete business message handler
* @param group Handler group index
*/
onDeleteBusinessMessage(handler: DeleteBusinessMessageHandler['callback'], group?: number): void
/**
* Register a delete business message handler with a filter
*
* @param filter Update filter
* @param handler Delete business message handler
* @param group Handler group index
*/
onDeleteBusinessMessage<Mod>(
filter: UpdateFilter<UpdateContext<DeleteBusinessMessageUpdate>, Mod>,
handler: DeleteBusinessMessageHandler<
filters.Modify<UpdateContext<DeleteBusinessMessageUpdate>, Mod>
>['callback'],
group?: number,
): void
/** @internal */
onDeleteBusinessMessage(filter: any, handler?: any, group?: number): void {
this._addKnownHandler('delete_business_message', filter, handler, group)
}
// end-codegen
}

View file

@ -1,5 +1,6 @@
import { MaybeArray, MaybePromise, Message } from '@mtcute/core'
import { BusinessMessageContext } from '../context/business-message.js'
import { MessageContext } from '../context/message.js'
import { chat } from './chat.js'
import { and, or } from './logic.js'
@ -30,7 +31,7 @@ export const command = (
prefixes?: MaybeArray<string> | null
caseSensitive?: boolean
} = {},
): UpdateFilter<MessageContext, { command: string[] }> => {
): UpdateFilter<MessageContext | BusinessMessageContext, { command: string[] }> => {
if (!Array.isArray(commands)) commands = [commands]
if (!caseSensitive) {
@ -51,7 +52,7 @@ export const command = (
const _prefixes = prefixes
const check = (msg: MessageContext): MaybePromise<boolean> => {
const check = (msg: MessageContext | BusinessMessageContext): MaybePromise<boolean> => {
if (msg.isMessageGroup) return check(msg.messages[0])
for (const pref of _prefixes) {
@ -107,8 +108,10 @@ export const start = and(chat('private'), command('start'))
export const startGroup = and(or(chat('supergroup'), chat('group')), command('start'))
const deeplinkBase =
(base: UpdateFilter<MessageContext, { command: string[] }>) =>
(params: MaybeArray<string | RegExp>): UpdateFilter<MessageContext, { command: string[] }> => {
(base: UpdateFilter<MessageContext | BusinessMessageContext, { command: string[] }>) =>
(
params: MaybeArray<string | RegExp>,
): UpdateFilter<MessageContext | BusinessMessageContext, { command: string[] }> => {
if (!Array.isArray(params)) {
return and(start, (_msg: Message) => {
const msg = _msg as Message & { command: string[] }

View file

@ -3,6 +3,7 @@ import {
Chat,
ChatMemberUpdate,
ChatType,
DeleteBusinessMessageUpdate,
HistoryReadUpdate,
MaybeArray,
Message,
@ -58,6 +59,7 @@ export const chatId: {
| HistoryReadUpdate
| PollVoteUpdate
| BotChatJoinRequestUpdate
| DeleteBusinessMessageUpdate
>>
} = (id) => {
const indexId = new Set<number>()

View file

@ -1,5 +1,6 @@
import { MaybePromise, Message } from '@mtcute/core'
import { BusinessMessageContext } from '../context/business-message.js'
import { MessageContext } from '../context/message.js'
import { Modify, UpdateFilter } from './types.js'
@ -15,9 +16,9 @@ import { Modify, UpdateFilter } from './types.js'
export function every<Mod, State extends object>(
filter: UpdateFilter<Message, Mod, State>,
): UpdateFilter<
MessageContext,
MessageContext | BusinessMessageContext,
Mod & {
messages: Modify<MessageContext, Mod>[]
messages: Modify<MessageContext | BusinessMessageContext, Mod>[]
},
State
> {
@ -61,7 +62,7 @@ export function some<State extends object>(
// eslint-disable-next-line
filter: UpdateFilter<Message, any, State>,
// eslint-disable-next-line
): UpdateFilter<MessageContext, {}, State> {
): UpdateFilter<MessageContext | BusinessMessageContext, {}, State> {
return (ctx, state) => {
let i = 0
const upds = ctx.messages

View file

@ -18,6 +18,7 @@ import {
Video,
} from '@mtcute/core'
import { BusinessMessageContext } from '../context/business-message.js'
import { MessageContext } from '../index.js'
import { Modify, UpdateFilter } from './types.js'
@ -237,14 +238,16 @@ export const sender =
export const replyTo =
<Mod, State extends object>(
filter?: UpdateFilter<Message, Mod, State>,
): UpdateFilter<MessageContext, { getReplyTo: () => Promise<Message & Mod> }, State> =>
): UpdateFilter<MessageContext | BusinessMessageContext, { getReplyTo: () => Promise<Message & Mod> }, State> =>
async (msg, state) => {
if (!msg.replyToMessage?.id) return false
const reply = await msg.getReplyTo()
const reply = msg._name === 'new_message' ? await msg.getReplyTo() : msg.replyTo
if (!reply) return false
msg.getReplyTo = () => Promise.resolve(reply)
if (msg._name === 'new_message') {
msg.getReplyTo = () => Promise.resolve(reply)
}
if (!filter) return true

View file

@ -83,7 +83,9 @@ export const userId: {
return (upd) => {
switch (upd._name) {
case 'new_message':
case 'edit_message': {
case 'edit_message':
case 'new_business_message':
case 'edit_business_message': {
const sender = upd.sender
return (matchSelf && sender.isSelf) ||

View file

@ -2,8 +2,10 @@ import {
BotReactionCountUpdate,
BotReactionUpdate,
BotStoppedUpdate,
BusinessConnection,
ChatJoinRequestUpdate,
ChatMemberUpdate,
DeleteBusinessMessageUpdate,
DeleteMessageUpdate,
DeleteStoryUpdate,
HistoryReadUpdate,
@ -19,6 +21,7 @@ import {
import { TelegramClient } from '@mtcute/core/client.js'
import { UpdateContext } from './context/base.js'
import { BusinessMessageContext } from './context/business-message.js'
import {
CallbackQueryContext,
ChatJoinRequestUpdateContext,
@ -89,6 +92,29 @@ export type BotReactionCountUpdateHandler<T = UpdateContext<BotReactionCountUpda
'bot_reaction_count',
T
>
export type BusinessConnectionUpdateHandler<T = UpdateContext<BusinessConnection>> = ParsedUpdateHandler<
'business_connection',
T
>
export type NewBusinessMessageHandler<T = BusinessMessageContext, S = never> = ParsedUpdateHandler<
'new_business_message',
T,
S
>
export type EditBusinessMessageHandler<T = BusinessMessageContext, S = never> = ParsedUpdateHandler<
'edit_business_message',
T,
S
>
export type BusinessMessageGroupHandler<T = BusinessMessageContext, S = never> = ParsedUpdateHandler<
'business_message_group',
T,
S
>
export type DeleteBusinessMessageHandler<T = UpdateContext<DeleteBusinessMessageUpdate>> = ParsedUpdateHandler<
'delete_business_message',
T
>
export type UpdateHandler =
| RawUpdateHandler
@ -114,5 +140,10 @@ export type UpdateHandler =
| DeleteStoryHandler
| BotReactionUpdateHandler
| BotReactionCountUpdateHandler
| BusinessConnectionUpdateHandler
| NewBusinessMessageHandler
| EditBusinessMessageHandler
| BusinessMessageGroupHandler
| DeleteBusinessMessageHandler
// end-codegen

View file

@ -1,5 +1,6 @@
import { assertNever, MaybePromise, Peer } from '@mtcute/core'
import { BusinessMessageContext } from '../context/business-message.js'
import { CallbackQueryContext, MessageContext } from '../context/index.js'
/**
@ -10,7 +11,9 @@ import { CallbackQueryContext, MessageContext } from '../context/index.js'
* @param msg Message or callback from which to derive the key
* @param scene Current scene UID, or `null` if none
*/
export type StateKeyDelegate = (upd: MessageContext | CallbackQueryContext | Peer) => MaybePromise<string | null>
export type StateKeyDelegate = (
upd: MessageContext | BusinessMessageContext | CallbackQueryContext | Peer,
) => MaybePromise<string | null>
/**
* Default state key delegate.
@ -29,7 +32,7 @@ export const defaultStateKeyDelegate: StateKeyDelegate = (upd): string | null =>
return String(upd.id)
}
if (upd._name === 'new_message') {
if (upd._name === 'new_message' || upd._name === 'new_business_message') {
switch (upd.chat.chatType) {
case 'private':
case 'bot':