feat: contexts

This commit is contained in:
alina 🌸 2023-10-11 08:24:11 +03:00 committed by Alina Tumanova
parent 6629d91274
commit 337418a34c
51 changed files with 1550 additions and 1078 deletions

View file

@ -343,7 +343,7 @@ async function addSingleMethod(state, fileName) {
state.imports[module] = new Set() state.imports[module] = new Set()
} }
state.imports[module].add(name) if (!isManual) state.imports[module].add(name)
} }
} }
} else if (stmt.kind === ts.SyntaxKind.InterfaceDeclaration) { } else if (stmt.kind === ts.SyntaxKind.InterfaceDeclaration) {

View file

@ -26,7 +26,7 @@ function parseUpdateTypes() {
const ret = [] const ret = []
for (const line of lines) { for (const line of lines) {
const m = line.match(/^([a-z_]+)(?:: ([a-zA-Z]+))? = ([a-zA-Z]+(?:\[\])?)( \+ State)?$/) const m = line.match(/^([a-z_]+)(?:: ([a-zA-Z]+))? = ([a-zA-Z]+(?:\[\])?)( \+ State)?(?: in ([a-zA-Z]+))?$/)
if (!m) throw new Error(`invalid syntax: ${line}`) if (!m) throw new Error(`invalid syntax: ${line}`)
ret.push({ ret.push({
typeName: m[1], typeName: m[1],
@ -34,6 +34,7 @@ function parseUpdateTypes() {
updateType: m[3], updateType: m[3],
funcName: m[2] ? m[2][0].toLowerCase() + m[2].substr(1) : snakeToCamel(m[1]), funcName: m[2] ? m[2][0].toLowerCase() + m[2].substr(1) : snakeToCamel(m[1]),
state: Boolean(m[4]), state: Boolean(m[4]),
context: m[5] ?? `UpdateContext<${m[3]}>`,
}) })
} }

View file

@ -1,20 +1,20 @@
# format: type_name[: handler_type_name] = update_type[ + State] # format: type_name[: handler_type_name] = update_type[ + State][in ContextClassName]
new_message = Message + State new_message = Message + State in MessageContext
edit_message = Message + State edit_message = Message + State in MessageContext
message_group = Message[] + State message_group = Message[] + State in MessageContext
delete_message = DeleteMessageUpdate delete_message = DeleteMessageUpdate
chat_member: ChatMemberUpdate = ChatMemberUpdate chat_member: ChatMemberUpdate = ChatMemberUpdate
inline_query = InlineQuery inline_query = InlineQuery in InlineQueryContext
chosen_inline_result = ChosenInlineResult chosen_inline_result = ChosenInlineResult in ChosenInlineResultContext
callback_query = CallbackQuery + State callback_query = CallbackQuery + State in CallbackQueryContext
poll: PollUpdate = PollUpdate poll: PollUpdate = PollUpdate
poll_vote = PollVoteUpdate poll_vote = PollVoteUpdate
user_status: UserStatusUpdate = UserStatusUpdate user_status: UserStatusUpdate = UserStatusUpdate
user_typing = UserTypingUpdate user_typing = UserTypingUpdate
history_read = HistoryReadUpdate history_read = HistoryReadUpdate
bot_stopped = BotStoppedUpdate bot_stopped = BotStoppedUpdate
bot_chat_join_request = BotChatJoinRequestUpdate bot_chat_join_request = BotChatJoinRequestUpdate in ChatJoinRequestUpdateContext
chat_join_request = ChatJoinRequestUpdate chat_join_request = ChatJoinRequestUpdate
pre_checkout_query = PreCheckoutQuery pre_checkout_query = PreCheckoutQuery in PreCheckoutQueryContext
story: StoryUpdate = StoryUpdate story: StoryUpdate = StoryUpdate
delete_story = DeleteStoryUpdate delete_story = DeleteStoryUpdate

View file

@ -20,7 +20,7 @@ import { getPasswordHint } from './methods/auth/get-password-hint'
import { logOut } from './methods/auth/log-out' import { logOut } from './methods/auth/log-out'
import { recoverPassword } from './methods/auth/recover-password' import { recoverPassword } from './methods/auth/recover-password'
import { resendCode } from './methods/auth/resend-code' import { resendCode } from './methods/auth/resend-code'
import { run } from './methods/auth/run' import {} from './methods/auth/run'
import { sendCode } from './methods/auth/send-code' import { sendCode } from './methods/auth/send-code'
import { sendRecoveryCode } from './methods/auth/send-recovery-code' import { sendRecoveryCode } from './methods/auth/send-recovery-code'
import { signIn } from './methods/auth/sign-in' import { signIn } from './methods/auth/sign-in'
@ -141,6 +141,7 @@ import { getMessageReactions, getMessageReactionsById } from './methods/messages
import { getMessages } from './methods/messages/get-messages' import { getMessages } from './methods/messages/get-messages'
import { getMessagesUnsafe } from './methods/messages/get-messages-unsafe' import { getMessagesUnsafe } from './methods/messages/get-messages-unsafe'
import { getReactionUsers, GetReactionUsersOffset } from './methods/messages/get-reaction-users' import { getReactionUsers, GetReactionUsersOffset } from './methods/messages/get-reaction-users'
import { getReplyTo } from './methods/messages/get-reply-to'
import { getScheduledMessages } from './methods/messages/get-scheduled-messages' import { getScheduledMessages } from './methods/messages/get-scheduled-messages'
import { iterHistory } from './methods/messages/iter-history' import { iterHistory } from './methods/messages/iter-history'
import { iterReactionUsers } from './methods/messages/iter-reaction-users' import { iterReactionUsers } from './methods/messages/iter-reaction-users'
@ -152,10 +153,15 @@ import { readHistory } from './methods/messages/read-history'
import { readReactions } from './methods/messages/read-reactions' import { readReactions } from './methods/messages/read-reactions'
import { searchGlobal, SearchGlobalOffset } from './methods/messages/search-global' import { searchGlobal, SearchGlobalOffset } from './methods/messages/search-global'
import { searchMessages, SearchMessagesOffset } from './methods/messages/search-messages' import { searchMessages, SearchMessagesOffset } from './methods/messages/search-messages'
import { answerMedia, answerMediaGroup, answerText } from './methods/messages/send-answer'
import { commentMedia, commentMediaGroup, commentText } from './methods/messages/send-comment'
import { CommonSendParams } from './methods/messages/send-common'
import { sendCopy, SendCopyParams } from './methods/messages/send-copy' import { sendCopy, SendCopyParams } from './methods/messages/send-copy'
import { sendCopyGroup, SendCopyGroupParams } from './methods/messages/send-copy-group'
import { sendMedia } from './methods/messages/send-media' import { sendMedia } from './methods/messages/send-media'
import { sendMediaGroup } from './methods/messages/send-media-group' import { sendMediaGroup } from './methods/messages/send-media-group'
import { sendReaction } from './methods/messages/send-reaction' import { sendReaction } from './methods/messages/send-reaction'
import { replyMedia, replyMediaGroup, replyText } from './methods/messages/send-reply'
import { sendScheduled } from './methods/messages/send-scheduled' import { sendScheduled } from './methods/messages/send-scheduled'
import { sendText } from './methods/messages/send-text' import { sendText } from './methods/messages/send-text'
import { sendTyping } from './methods/messages/send-typing' import { sendTyping } from './methods/messages/send-typing'
@ -180,7 +186,7 @@ import { removeCloudPassword } from './methods/password/remove-cloud-password'
import { addStickerToSet } from './methods/stickers/add-sticker-to-set' import { addStickerToSet } from './methods/stickers/add-sticker-to-set'
import { createStickerSet } from './methods/stickers/create-sticker-set' import { createStickerSet } from './methods/stickers/create-sticker-set'
import { deleteStickerFromSet } from './methods/stickers/delete-sticker-from-set' import { deleteStickerFromSet } from './methods/stickers/delete-sticker-from-set'
import { getCustomEmojis } from './methods/stickers/get-custom-emojis' import { getCustomEmojis, getCustomEmojisFromMessages } from './methods/stickers/get-custom-emojis'
import { getInstalledStickers } from './methods/stickers/get-installed-stickers' import { getInstalledStickers } from './methods/stickers/get-installed-stickers'
import { getStickerSet } from './methods/stickers/get-sticker-set' import { getStickerSet } from './methods/stickers/get-sticker-set'
import { moveStickerInSet } from './methods/stickers/move-sticker-in-set' import { moveStickerInSet } from './methods/stickers/move-sticker-in-set'
@ -287,6 +293,7 @@ import {
MessageEntity, MessageEntity,
MessageMedia, MessageMedia,
MessageReactions, MessageReactions,
ParametersSkip2,
ParsedUpdate, ParsedUpdate,
PeerReaction, PeerReaction,
PeersIndex, PeersIndex,
@ -547,6 +554,7 @@ export interface TelegramClient extends BaseTelegramClient {
* *
* @param params Parameters to be passed to {@link start} * @param params Parameters to be passed to {@link start}
* @param then Function to be called after {@link start} returns * @param then Function to be called after {@link start} returns
* @manual
*/ */
run(params: Parameters<typeof start>[1], then?: (user: User) => void | Promise<void>): void run(params: Parameters<typeof start>[1], then?: (user: User) => void | Promise<void>): void
/** /**
@ -2713,7 +2721,7 @@ export interface TelegramClient extends BaseTelegramClient {
*/ */
getPrimaryInviteLink(chatId: InputPeerLike): Promise<ChatInviteLink> getPrimaryInviteLink(chatId: InputPeerLike): Promise<ChatInviteLink>
/** /**
* Approve or deny multiple join requests to a chat. * Approve or decline multiple join requests to a chat.
* **Available**: 👤 users only * **Available**: 👤 users only
* *
*/ */
@ -2721,14 +2729,14 @@ export interface TelegramClient extends BaseTelegramClient {
/** Chat/channel ID */ /** Chat/channel ID */
chatId: InputPeerLike chatId: InputPeerLike
/** Whether to approve or deny the join requests */ /** Whether to approve or decline the join requests */
action: 'approve' | 'deny' action: 'approve' | 'decline'
/** Invite link to target */ /** Invite link to target */
link?: string | ChatInviteLink link?: string | ChatInviteLink
}): Promise<void> }): Promise<void>
/** /**
* Approve or deny join request to a chat. * Approve or decline join request to a chat.
* **Available**: both users and bots * **Available**: both users and bots
* *
*/ */
@ -2737,8 +2745,8 @@ export interface TelegramClient extends BaseTelegramClient {
chatId: InputPeerLike chatId: InputPeerLike
/** User ID */ /** User ID */
user: InputPeerLike user: InputPeerLike
/** Whether to approve or deny the join request */ /** Whether to approve or decline the join request */
action: 'approve' | 'deny' action: 'approve' | 'decline'
}): Promise<void> }): Promise<void>
/** /**
* Iterate over users who have joined * Iterate over users who have joined
@ -3215,6 +3223,16 @@ export interface TelegramClient extends BaseTelegramClient {
offset?: GetReactionUsersOffset offset?: GetReactionUsersOffset
}, },
): Promise<ArrayPaginated<PeerReaction, GetReactionUsersOffset>> ): Promise<ArrayPaginated<PeerReaction, GetReactionUsersOffset>>
/**
* For messages containing a reply, fetch the message that is being replied.
*
* Note that even if a message has {@link replyToMessageId},
* the message itself may have been deleted, in which case
* this method will also return `null`.
* **Available**: both users and bots
*
*/
getReplyTo(message: Message): Promise<Message | null>
/** /**
* Get scheduled messages in chat by their IDs * Get scheduled messages in chat by their IDs
* *
@ -3549,6 +3567,76 @@ export interface TelegramClient extends BaseTelegramClient {
*/ */
fromUser?: InputPeerLike fromUser?: InputPeerLike
}): Promise<ArrayPaginated<Message, SearchMessagesOffset>> }): Promise<ArrayPaginated<Message, SearchMessagesOffset>>
/** Send a text to the same chat (and topic, if applicable) as a given message */
answerText(message: Message, ...params: ParametersSkip2<typeof sendText>): ReturnType<typeof sendText>
/** Send a media to the same chat (and topic, if applicable) as a given message */
answerMedia(message: Message, ...params: ParametersSkip2<typeof sendMedia>): ReturnType<typeof sendMedia>
/** Send a media group to the same chat (and topic, if applicable) as a given message */
answerMediaGroup(
message: Message,
...params: ParametersSkip2<typeof sendMediaGroup>
): ReturnType<typeof sendMediaGroup>
/**
* Send a text comment to a given message.
*
* If this is a normal message (not a channel post),
* a simple reply will be sent.
*
* **Available**: both users and bots
*
* @throws MtArgumentError
* If this is a channel post which does not have comments section.
* To check if a post has comments, use {@link Message#replies}.hasComments
*/
commentText(message: Message, ...params: ParametersSkip2<typeof sendText>): ReturnType<typeof sendText>
/**
* Send a text comment to a given message.
*
* If this is a normal message (not a channel post),
* a simple reply will be sent.
*
* **Available**: both users and bots
*
* @throws MtArgumentError
* If this is a channel post which does not have comments section.
* To check if a post has comments, use {@link Message#replies}.hasComments
*/
commentMedia(message: Message, ...params: ParametersSkip2<typeof sendMedia>): ReturnType<typeof sendMedia>
/**
* Send a text comment to a given message.
*
* If this is a normal message (not a channel post),
* a simple reply will be sent.
*
* **Available**: both users and bots
*
* @throws MtArgumentError
* If this is a channel post which does not have comments section.
* To check if a post has comments, use {@link Message#replies}.hasComments
*/
commentMediaGroup(
message: Message,
...params: ParametersSkip2<typeof sendMediaGroup>
): ReturnType<typeof sendMediaGroup>
/**
* Copy a message group (i.e. send the same message group, but do not forward it).
*
* Note that all the provided messages must be in the same message group
* **Available**: both users and bots
*
*/
sendCopyGroup(
params: SendCopyGroupParams &
(
| {
/** Source chat ID */
fromChatId: InputPeerLike
/** Message IDs to forward */
messages: number[]
}
| { messages: Message[] }
),
): Promise<Message[]>
/** /**
* Copy a message (i.e. send the same message, but do not forward it). * Copy a message (i.e. send the same message, but do not forward it).
* *
@ -3556,10 +3644,8 @@ export interface TelegramClient extends BaseTelegramClient {
* it will be copied simply as a text message, * it will be copied simply as a text message,
* and if the message contains an invoice, * and if the message contains an invoice,
* it can't be copied. * it can't be copied.
*
* **Available**: both users and bots * **Available**: both users and bots
* *
* @param params
*/ */
sendCopy( sendCopy(
params: SendCopyParams & params: SendCopyParams &
@ -3589,56 +3675,7 @@ export interface TelegramClient extends BaseTelegramClient {
sendMediaGroup( sendMediaGroup(
chatId: InputPeerLike, chatId: InputPeerLike,
medias: (InputMediaLike | string)[], medias: (InputMediaLike | string)[],
params?: { params?: CommonSendParams & {
/**
* Message to reply to. Either a message object or message ID.
*
* For forums - can also be an ID of the topic (i.e. its top message ID)
*/
replyTo?: number | Message
/**
* Whether to throw an error if {@link replyTo}
* message does not exist.
*
* If that message was not found, `NotFoundError` is thrown,
* with `text` set to `MESSAGE_NOT_FOUND`.
*
* Incurs an additional request, so only use when really needed.
*
* Defaults to `false`
*/
mustReply?: boolean
/**
* Message to comment to. Either a message object or message ID.
*
* This overwrites `replyTo` if it was passed
*/
commentTo?: number | Message
/**
* 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
/**
* Whether to send this message silently.
*/
silent?: boolean
/**
* If set, the message will be scheduled to this date.
* When passing a number, a UNIX time in ms is expected.
*
* You can also pass `0x7FFFFFFE`, this will send the message
* once the peer is online
*/
schedule?: Date | number
/** /**
* Function that will be called after some part has been uploaded. * Function that will be called after some part has been uploaded.
* Only used when a file that requires uploading is passed, * Only used when a file that requires uploading is passed,
@ -3649,26 +3686,6 @@ export interface TelegramClient extends BaseTelegramClient {
* @param total Total file size * @param total Total file size
*/ */
progressCallback?: (index: number, uploaded: number, total: number) => void progressCallback?: (index: number, uploaded: number, total: number) => void
/**
* Whether to clear draft after sending this message.
*
* Defaults to `false`
*/
clearDraft?: boolean
/**
* Whether to disallow further forwards of this message.
*
* Only for bots, works even if the target chat does not
* have content protection.
*/
forbidForwards?: boolean
/**
* Peer to use when sending the message.
*/
sendAs?: InputPeerLike
}, },
): Promise<Message[]> ): Promise<Message[]>
/** /**
@ -3687,7 +3704,13 @@ export interface TelegramClient extends BaseTelegramClient {
sendMedia( sendMedia(
chatId: InputPeerLike, chatId: InputPeerLike,
media: InputMediaLike | string, media: InputMediaLike | string,
params?: { params?: CommonSendParams & {
/**
* For bots: inline or reply markup or an instruction
* to hide a reply keyboard or to force a reply.
*/
replyMarkup?: ReplyMarkup
/** /**
* Override caption for `media`. * Override caption for `media`.
* *
@ -3704,61 +3727,6 @@ export interface TelegramClient extends BaseTelegramClient {
*/ */
entities?: tl.TypeMessageEntity[] entities?: tl.TypeMessageEntity[]
/**
* Message to reply to. Either a message object or message ID.
*
* For forums - can also be an ID of the topic (i.e. its top message ID)
*/
replyTo?: number | Message
/**
* Whether to throw an error if {@link replyTo}
* message does not exist.
*
* If that message was not found, `NotFoundError` is thrown,
* with `text` set to `MESSAGE_NOT_FOUND`.
*
* Incurs an additional request, so only use when really needed.
*
* Defaults to `false`
*/
mustReply?: boolean
/**
* Message to comment to. Either a message object or message ID.
*
* This overwrites `replyTo` if it was passed
*/
commentTo?: number | Message
/**
* 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
/**
* Whether to send this message silently.
*/
silent?: boolean
/**
* If set, the message will be scheduled to this date.
* When passing a number, a UNIX time in ms is expected.
*
* You can also pass `0x7FFFFFFE`, this will send the message
* once the peer is online
*/
schedule?: Date | number
/**
* For bots: inline or reply markup or an instruction
* to hide a reply keyboard or to force a reply.
*/
replyMarkup?: ReplyMarkup
/** /**
* Function that will be called after some part has been uploaded. * Function that will be called after some part has been uploaded.
* Only used when a file that requires uploading is passed, * Only used when a file that requires uploading is passed,
@ -3768,32 +3736,6 @@ export interface TelegramClient extends BaseTelegramClient {
* @param total Total file size * @param total Total file size
*/ */
progressCallback?: (uploaded: number, total: number) => void progressCallback?: (uploaded: number, total: number) => void
/**
* Whether to clear draft after sending this message.
*
* Defaults to `false`
*/
clearDraft?: boolean
/**
* Whether to disallow further forwards of this message.
*
* Only for bots, works even if the target chat does not
* have content protection.
*/
forbidForwards?: boolean
/**
* Peer to use when sending the message.
*/
sendAs?: InputPeerLike
/**
* Whether to dispatch the returned message
* to the client's update handler.
*/
shouldDispatch?: true
}, },
): Promise<Message> ): Promise<Message>
/** /**
@ -3817,6 +3759,15 @@ export interface TelegramClient extends BaseTelegramClient {
shouldDispatch?: true shouldDispatch?: true
}, },
): Promise<Message> ): Promise<Message>
/** Send a text in reply to a given message */
replyText(message: Message, ...params: ParametersSkip2<typeof sendText>): ReturnType<typeof sendText>
/** Send a media in reply to a given message */
replyMedia(message: Message, ...params: ParametersSkip2<typeof sendMedia>): ReturnType<typeof sendMedia>
/** Send a media group in reply to a given message */
replyMediaGroup(
message: Message,
...params: ParametersSkip2<typeof sendMediaGroup>
): ReturnType<typeof sendMediaGroup>
/** /**
* Send previously scheduled message(s) * Send previously scheduled message(s)
* *
@ -3842,41 +3793,12 @@ export interface TelegramClient extends BaseTelegramClient {
sendText( sendText(
chatId: InputPeerLike, chatId: InputPeerLike,
text: string | FormattedString<string>, text: string | FormattedString<string>,
params?: { params?: CommonSendParams & {
/** /**
* Message to reply to. Either a message object or message ID. * For bots: inline or reply markup or an instruction
* * to hide a reply keyboard or to force a reply.
* For forums - can also be an ID of the topic (i.e. its top message ID)
*/ */
replyTo?: number | Message replyMarkup?: ReplyMarkup
/**
* Whether to throw an error if {@link replyTo}
* message does not exist.
*
* If that message was not found, `NotFoundError` is thrown,
* with `text` set to `MESSAGE_NOT_FOUND`.
*
* Incurs an additional request, so only use when really needed.
*
* Defaults to `false`
*/
mustReply?: boolean
/**
* Message to comment to. Either a message object or message ID.
*
* This overwrites `replyTo` if it was passed
*/
commentTo?: number | Message
/**
* 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 * List of formatting entities to use instead of parsing via a
@ -3890,52 +3812,6 @@ export interface TelegramClient extends BaseTelegramClient {
* Whether to disable links preview in this message * Whether to disable links preview in this message
*/ */
disableWebPreview?: boolean disableWebPreview?: boolean
/**
* Whether to send this message silently.
*/
silent?: boolean
/**
* If set, the message will be scheduled to this date.
* When passing a number, a UNIX time in ms is expected.
*
* You can also pass `0x7FFFFFFE`, this will send the message
* once the peer is online
*/
schedule?: Date | number
/**
* For bots: inline or reply markup or an instruction
* to hide a reply keyboard or to force a reply.
*/
replyMarkup?: ReplyMarkup
/**
* Whether to clear draft after sending this message.
*
* Defaults to `false`
*/
clearDraft?: boolean
/**
* Whether to disallow further forwards of this message.
*
* Only for bots, works even if the target chat does not
* have content protection.
*/
forbidForwards?: boolean
/**
* Peer to use when sending the message.
*/
sendAs?: InputPeerLike
/**
* Whether to dispatch the returned message
* to the client's update handler.
*/
shouldDispatch?: true
}, },
): Promise<Message> ): Promise<Message>
/** /**
@ -4279,6 +4155,12 @@ export interface TelegramClient extends BaseTelegramClient {
* @param ids IDs of the stickers (as defined in {@link MessageEntity.emojiId}) * @param ids IDs of the stickers (as defined in {@link MessageEntity.emojiId})
*/ */
getCustomEmojis(ids: tl.Long[]): Promise<Sticker[]> getCustomEmojis(ids: tl.Long[]): Promise<Sticker[]>
/**
* Given one or more messages, extract all unique custom emojis from it and fetch them
* **Available**: both users and bots
*
*/
getCustomEmojisFromMessages(messages: MaybeArray<Message>): Promise<Sticker[]>
/** /**
* Get a list of all installed sticker packs * Get a list of all installed sticker packs
* *
@ -5259,6 +5141,11 @@ export class TelegramClient extends BaseTelegramClient {
} else { } else {
this.start = start.bind(null, this) this.start = start.bind(null, this)
} }
this.run = (params, then) => {
this.start(params)
.then(then)
.catch((err) => this._emitError(err))
}
} }
getAuthState = getAuthState.bind(null, this) getAuthState = getAuthState.bind(null, this)
_onAuthorization = _onAuthorization.bind(null, this) _onAuthorization = _onAuthorization.bind(null, this)
@ -5267,7 +5154,6 @@ export class TelegramClient extends BaseTelegramClient {
logOut = logOut.bind(null, this) logOut = logOut.bind(null, this)
recoverPassword = recoverPassword.bind(null, this) recoverPassword = recoverPassword.bind(null, this)
resendCode = resendCode.bind(null, this) resendCode = resendCode.bind(null, this)
run = run.bind(null, this)
sendCode = sendCode.bind(null, this) sendCode = sendCode.bind(null, this)
sendRecoveryCode = sendRecoveryCode.bind(null, this) sendRecoveryCode = sendRecoveryCode.bind(null, this)
signInBot = signInBot.bind(null, this) signInBot = signInBot.bind(null, this)
@ -5396,6 +5282,7 @@ export class TelegramClient extends BaseTelegramClient {
getMessagesUnsafe = getMessagesUnsafe.bind(null, this) getMessagesUnsafe = getMessagesUnsafe.bind(null, this)
getMessages = getMessages.bind(null, this) getMessages = getMessages.bind(null, this)
getReactionUsers = getReactionUsers.bind(null, this) getReactionUsers = getReactionUsers.bind(null, this)
getReplyTo = getReplyTo.bind(null, this)
getScheduledMessages = getScheduledMessages.bind(null, this) getScheduledMessages = getScheduledMessages.bind(null, this)
iterHistory = iterHistory.bind(null, this) iterHistory = iterHistory.bind(null, this)
iterReactionUsers = iterReactionUsers.bind(null, this) iterReactionUsers = iterReactionUsers.bind(null, this)
@ -5407,10 +5294,20 @@ export class TelegramClient extends BaseTelegramClient {
readReactions = readReactions.bind(null, this) readReactions = readReactions.bind(null, this)
searchGlobal = searchGlobal.bind(null, this) searchGlobal = searchGlobal.bind(null, this)
searchMessages = searchMessages.bind(null, this) searchMessages = searchMessages.bind(null, this)
answerText = answerText.bind(null, this)
answerMedia = answerMedia.bind(null, this)
answerMediaGroup = answerMediaGroup.bind(null, this)
commentText = commentText.bind(null, this)
commentMedia = commentMedia.bind(null, this)
commentMediaGroup = commentMediaGroup.bind(null, this)
sendCopyGroup = sendCopyGroup.bind(null, this)
sendCopy = sendCopy.bind(null, this) sendCopy = sendCopy.bind(null, this)
sendMediaGroup = sendMediaGroup.bind(null, this) sendMediaGroup = sendMediaGroup.bind(null, this)
sendMedia = sendMedia.bind(null, this) sendMedia = sendMedia.bind(null, this)
sendReaction = sendReaction.bind(null, this) sendReaction = sendReaction.bind(null, this)
replyText = replyText.bind(null, this)
replyMedia = replyMedia.bind(null, this)
replyMediaGroup = replyMediaGroup.bind(null, this)
sendScheduled = sendScheduled.bind(null, this) sendScheduled = sendScheduled.bind(null, this)
sendText = sendText.bind(null, this) sendText = sendText.bind(null, this)
sendTyping = sendTyping.bind(null, this) sendTyping = sendTyping.bind(null, this)
@ -5436,6 +5333,7 @@ export class TelegramClient extends BaseTelegramClient {
createStickerSet = createStickerSet.bind(null, this) createStickerSet = createStickerSet.bind(null, this)
deleteStickerFromSet = deleteStickerFromSet.bind(null, this) deleteStickerFromSet = deleteStickerFromSet.bind(null, this)
getCustomEmojis = getCustomEmojis.bind(null, this) getCustomEmojis = getCustomEmojis.bind(null, this)
getCustomEmojisFromMessages = getCustomEmojisFromMessages.bind(null, this)
getInstalledStickers = getInstalledStickers.bind(null, this) getInstalledStickers = getInstalledStickers.bind(null, this)
getStickerSet = getStickerSet.bind(null, this) getStickerSet = getStickerSet.bind(null, this)
moveStickerInSet = moveStickerInSet.bind(null, this) moveStickerInSet = moveStickerInSet.bind(null, this)

View file

@ -53,6 +53,7 @@ import {
MessageEntity, MessageEntity,
MessageMedia, MessageMedia,
MessageReactions, MessageReactions,
ParametersSkip2,
ParsedUpdate, ParsedUpdate,
PeerReaction, PeerReaction,
PeersIndex, PeersIndex,

View file

@ -45,4 +45,9 @@ function _initializeClient(this: TelegramClient, opts: TelegramClientOptions) {
} else { } else {
this.start = start.bind(null, this) this.start = start.bind(null, this)
} }
this.run = (params, then) => {
this.start(params)
.then(then)
.catch((err) => this._emitError(err))
}
} }

View file

@ -13,6 +13,7 @@ import { start } from './start'
* *
* @param params Parameters to be passed to {@link start} * @param params Parameters to be passed to {@link start}
* @param then Function to be called after {@link start} returns * @param then Function to be called after {@link start} returns
* @manual
*/ */
export function run( export function run(
client: BaseTelegramClient, client: BaseTelegramClient,

View file

@ -4,7 +4,7 @@ import type { ChatInviteLink, InputPeerLike } from '../../types'
import { resolvePeer } from '../users/resolve-peer' import { resolvePeer } from '../users/resolve-peer'
/** /**
* Approve or deny multiple join requests to a chat. * Approve or decline multiple join requests to a chat.
*/ */
export async function hideAllJoinRequests( export async function hideAllJoinRequests(
client: BaseTelegramClient, client: BaseTelegramClient,
@ -12,8 +12,8 @@ export async function hideAllJoinRequests(
/** Chat/channel ID */ /** Chat/channel ID */
chatId: InputPeerLike chatId: InputPeerLike
/** Whether to approve or deny the join requests */ /** Whether to approve or decline the join requests */
action: 'approve' | 'deny' action: 'approve' | 'decline'
/** Invite link to target */ /** Invite link to target */
link?: string | ChatInviteLink link?: string | ChatInviteLink

View file

@ -5,7 +5,7 @@ import { normalizeToInputUser } from '../../utils/peer-utils'
import { resolvePeer } from '../users/resolve-peer' import { resolvePeer } from '../users/resolve-peer'
/** /**
* Approve or deny join request to a chat. * Approve or decline join request to a chat.
*/ */
export async function hideJoinRequest( export async function hideJoinRequest(
client: BaseTelegramClient, client: BaseTelegramClient,
@ -14,8 +14,8 @@ export async function hideJoinRequest(
chatId: InputPeerLike chatId: InputPeerLike
/** User ID */ /** User ID */
user: InputPeerLike user: InputPeerLike
/** Whether to approve or deny the join request */ /** Whether to approve or decline the join request */
action: 'approve' | 'deny' action: 'approve' | 'decline'
}, },
): Promise<void> { ): Promise<void> {
const { chatId, user, action } = params const { chatId, user, action } = params

View file

@ -0,0 +1,28 @@
import { BaseTelegramClient } from '@mtcute/core'
import { Message } from '../../types/messages'
import { getMessages } from './get-messages'
import { getMessagesUnsafe } from './get-messages-unsafe'
/**
* For messages containing a reply, fetch the message that is being replied.
*
* Note that even if a message has {@link replyToMessageId},
* the message itself may have been deleted, in which case
* this method will also return `null`.
*/
export async function getReplyTo(client: BaseTelegramClient, message: Message): Promise<Message | null> {
if (!message.replyToMessageId) {
return null
}
if (message.raw.peerId._ === 'peerChannel') {
const [msg] = await getMessages(client, message.chat.inputPeer, message.id, true)
return msg
}
const [msg] = await getMessagesUnsafe(client, message.id, true)
return msg
}

View file

@ -0,0 +1,55 @@
import { BaseTelegramClient } from '@mtcute/core'
import { Message } from '../../types/messages/message'
import { ParametersSkip2 } from '../../types/utils'
import { sendMedia } from './send-media'
import { sendMediaGroup } from './send-media-group'
import { sendText } from './send-text'
/** Send a text to the same chat (and topic, if applicable) as a given message */
export function answerText(
client: BaseTelegramClient,
message: Message,
...params: ParametersSkip2<typeof sendText>
): ReturnType<typeof sendText> {
if (!message.isTopicMessage || !message.replyToThreadId) {
return sendText(client, message.chat.inputPeer, ...params)
}
const [text, params_ = {}] = params
params_.replyTo = message.replyToThreadId
return sendText(client, message.chat.inputPeer, text, params_)
}
/** Send a media to the same chat (and topic, if applicable) as a given message */
export function answerMedia(
client: BaseTelegramClient,
message: Message,
...params: ParametersSkip2<typeof sendMedia>
): ReturnType<typeof sendMedia> {
if (!message.isTopicMessage || !message.replyToThreadId) {
return sendMedia(client, message.chat.inputPeer, ...params)
}
const [media, params_ = {}] = params
params_.replyTo = message.replyToThreadId
return sendMedia(client, message.chat.inputPeer, media, params_)
}
/** Send a media group to the same chat (and topic, if applicable) as a given message */
export function answerMediaGroup(
client: BaseTelegramClient,
message: Message,
...params: ParametersSkip2<typeof sendMediaGroup>
): ReturnType<typeof sendMediaGroup> {
if (!message.isTopicMessage || !message.replyToThreadId) {
return sendMediaGroup(client, message.chat.inputPeer, ...params)
}
const [media, params_ = {}] = params
params_.replyTo = message.replyToThreadId
return sendMediaGroup(client, message.chat.inputPeer, media, params_)
}

View file

@ -0,0 +1,95 @@
import { BaseTelegramClient, MtArgumentError } from '@mtcute/core'
import { Message } from '../../types/messages/message'
import { ParametersSkip2 } from '../../types/utils'
import { sendMedia } from './send-media'
import { sendMediaGroup } from './send-media-group'
import { replyMedia, replyMediaGroup, replyText } from './send-reply'
import { sendText } from './send-text'
/**
* Send a text comment to a given message.
*
* If this is a normal message (not a channel post),
* a simple reply will be sent.
*
* @throws MtArgumentError
* If this is a channel post which does not have comments section.
* To check if a post has comments, use {@link Message#replies}.hasComments
*/
export function commentText(
client: BaseTelegramClient,
message: Message,
...params: ParametersSkip2<typeof sendText>
): ReturnType<typeof sendText> {
if (message.chat.chatType !== 'channel') {
return replyText(client, message, ...params)
}
if (!message.replies || !message.replies.hasComments) {
throw new MtArgumentError('This message does not have comments section')
}
const [text, params_ = {}] = params
params_.commentTo = message.id
return sendText(client, message.chat.inputPeer, text, params_)
}
/**
* Send a text comment to a given message.
*
* If this is a normal message (not a channel post),
* a simple reply will be sent.
*
* @throws MtArgumentError
* If this is a channel post which does not have comments section.
* To check if a post has comments, use {@link Message#replies}.hasComments
*/
export function commentMedia(
client: BaseTelegramClient,
message: Message,
...params: ParametersSkip2<typeof sendMedia>
): ReturnType<typeof sendMedia> {
if (message.chat.chatType !== 'channel') {
return replyMedia(client, message, ...params)
}
if (!message.replies || !message.replies.hasComments) {
throw new MtArgumentError('This message does not have comments section')
}
const [media, params_ = {}] = params
params_.commentTo = message.id
return sendMedia(client, message.chat.inputPeer, media, params_)
}
/**
* Send a text comment to a given message.
*
* If this is a normal message (not a channel post),
* a simple reply will be sent.
*
* @throws MtArgumentError
* If this is a channel post which does not have comments section.
* To check if a post has comments, use {@link Message#replies}.hasComments
*/
export function commentMediaGroup(
client: BaseTelegramClient,
message: Message,
...params: ParametersSkip2<typeof sendMediaGroup>
): ReturnType<typeof sendMediaGroup> {
if (message.chat.chatType !== 'channel') {
return replyMediaGroup(client, message, ...params)
}
if (!message.replies || !message.replies.hasComments) {
throw new MtArgumentError('This message does not have comments section')
}
const [media, params_ = {}] = params
params_.commentTo = message.id
return sendMediaGroup(client, message.chat.inputPeer, media, params_)
}

View file

@ -0,0 +1,119 @@
import { BaseTelegramClient, getMarkedPeerId, MtArgumentError } from '@mtcute/core'
import { MtMessageNotFoundError } from '../../types/errors'
import { Message } from '../../types/messages/message'
import { InputPeerLike } from '../../types/peers'
import { normalizeMessageId } from '../../utils'
import { resolvePeer } from '../users/resolve-peer'
import { _getDiscussionMessage } from './get-discussion-message'
import { getMessages } from './get-messages'
// @exported
export interface CommonSendParams {
/**
* Message to reply to. Either a message object or message ID.
*
* For forums - can also be an ID of the topic (i.e. its top message ID)
*/
replyTo?: number | Message
/**
* Whether to throw an error if {@link replyTo}
* message does not exist.
*
* If that message was not found, `NotFoundError` is thrown,
* with `text` set to `MESSAGE_NOT_FOUND`.
*
* Incurs an additional request, so only use when really needed.
*
* Defaults to `false`
*/
mustReply?: boolean
/**
* Message to comment to. Either a message object or message ID.
*
* This overwrites `replyTo` if it was passed
*/
commentTo?: number | Message
/**
* 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
/**
* Whether to send this message silently.
*/
silent?: boolean
/**
* If set, the message will be scheduled to this date.
* When passing a number, a UNIX time in ms is expected.
*
* You can also pass `0x7FFFFFFE`, this will send the message
* once the peer is online
*/
schedule?: Date | number
/**
* Whether to clear draft after sending this message.
*
* Defaults to `false`
*/
clearDraft?: boolean
/**
* Whether to disallow further forwards of this message.
*
* Only for bots, works even if the target chat does not
* have content protection.
*/
forbidForwards?: boolean
/**
* Peer to use when sending the message.
*/
sendAs?: InputPeerLike
/**
* Whether to dispatch the returned message
* to the client's update handler.
*/
shouldDispatch?: true
}
/**
* @internal
* @noemit
*/
export async function _processCommonSendParameters(
client: BaseTelegramClient,
chatId: InputPeerLike,
params: CommonSendParams,
) {
let peer = await resolvePeer(client, chatId)
let replyTo = normalizeMessageId(params.replyTo)
if (params.commentTo) {
[peer, replyTo] = await _getDiscussionMessage(client, peer, normalizeMessageId(params.commentTo)!)
}
if (params.mustReply) {
if (!replyTo) {
throw new MtArgumentError('mustReply used, but replyTo was not passed')
}
const msg = await getMessages(client, peer, replyTo)
if (!msg) {
throw new MtMessageNotFoundError(getMarkedPeerId(peer), replyTo, 'to reply to')
}
}
return { peer, replyTo }
}

View file

@ -0,0 +1,70 @@
import { BaseTelegramClient, MtArgumentError, tl } from '@mtcute/core'
import { isPresent } from '@mtcute/core/utils'
import { Message } from '../../types/messages/message'
import { InputPeerLike } from '../../types/peers'
import { resolvePeer } from '../users/resolve-peer'
import { getMessages } from './get-messages'
import { CommonSendParams } from './send-common'
import { sendMediaGroup } from './send-media-group'
// @exported
export interface SendCopyGroupParams extends CommonSendParams {
/** Destination chat ID */
toChatId: InputPeerLike
}
/**
* Copy a message group (i.e. send the same message group, but do not forward it).
*
* Note that all the provided messages must be in the same message group
*/
export async function sendCopyGroup(
client: BaseTelegramClient,
params: SendCopyGroupParams &
(
| {
/** Source chat ID */
fromChatId: InputPeerLike
/** Message IDs to forward */
messages: number[]
}
| { messages: Message[] }
),
): Promise<Message[]> {
const { toChatId, ...rest } = params
let msgs
if ('fromChatId' in params) {
const fromPeer = await resolvePeer(client, params.fromChatId)
msgs = await getMessages(client, fromPeer, params.messages).then((r) => r.filter(isPresent))
} else {
msgs = params.messages
}
const messageGroupId = msgs[0].groupedId!
for (let i = 1; i < msgs.length; i++) {
if (!msgs[i].groupedId?.eq(messageGroupId) || !msgs[i].media) {
throw new MtArgumentError('All messages must be in the same message group')
}
}
return sendMediaGroup(
client,
toChatId,
msgs.map((msg) => {
const raw = msg.raw as tl.RawMessage
return {
type: 'auto',
file: msg.media!.inputMedia,
caption: raw.message,
entities: raw.entities,
}
}),
rest,
)
}

View file

@ -3,28 +3,15 @@ import { BaseTelegramClient, getMarkedPeerId, MtArgumentError, tl } from '@mtcut
import { FormattedString, InputPeerLike, Message, MtMessageNotFoundError, ReplyMarkup } from '../../types' import { FormattedString, InputPeerLike, Message, MtMessageNotFoundError, ReplyMarkup } from '../../types'
import { resolvePeer } from '../users/resolve-peer' import { resolvePeer } from '../users/resolve-peer'
import { getMessages } from './get-messages' import { getMessages } from './get-messages'
import { CommonSendParams } from './send-common'
import { sendMedia } from './send-media' import { sendMedia } from './send-media'
import { sendText } from './send-text' import { sendText } from './send-text'
// @exported // @exported
export interface SendCopyParams { export interface SendCopyParams extends CommonSendParams {
/** Target chat ID */ /** Target chat ID */
toChatId: InputPeerLike toChatId: InputPeerLike
/**
* Whether to send this message silently.
*/
silent?: boolean
/**
* If set, the message will be scheduled to this date.
* When passing a number, a UNIX time in ms is expected.
*
* You can also pass `0x7FFFFFFE`, this will send the message
* once the peer is online
*/
schedule?: Date | number
/** /**
* New message caption (only used for media) * New message caption (only used for media)
*/ */
@ -38,26 +25,6 @@ export interface SendCopyParams {
*/ */
parseMode?: string | null parseMode?: string | null
/**
* Message to reply to. Either a message object or message ID.
*
* For forums - can also be an ID of the topic (i.e. its top message ID)
*/
replyTo?: number | Message
/**
* Whether to throw an error if {@link replyTo}
* message does not exist.
*
* If that message was not found, `NotFoundError` is thrown,
* with `text` set to `MESSAGE_NOT_FOUND`.
*
* Incurs an additional request, so only use when really needed.
*
* Defaults to `false`
*/
mustReply?: boolean
/** /**
* List of formatting entities to use instead of parsing via a * List of formatting entities to use instead of parsing via a
* parse mode. * parse mode.
@ -71,13 +38,6 @@ export interface SendCopyParams {
* to hide a reply keyboard or to force a reply. * to hide a reply keyboard or to force a reply.
*/ */
replyMarkup?: ReplyMarkup replyMarkup?: ReplyMarkup
/**
* Whether to clear draft after sending this message.
*
* Defaults to `false`
*/
clearDraft?: boolean
} }
/** /**
@ -87,8 +47,6 @@ export interface SendCopyParams {
* it will be copied simply as a text message, * it will be copied simply as a text message,
* and if the message contains an invoice, * and if the message contains an invoice,
* it can't be copied. * it can't be copied.
*
* @param params
*/ */
export async function sendCopy( export async function sendCopy(
client: BaseTelegramClient, client: BaseTelegramClient,

View file

@ -1,17 +1,16 @@
import { BaseTelegramClient, getMarkedPeerId, MtArgumentError, tl } from '@mtcute/core' import { BaseTelegramClient, tl } from '@mtcute/core'
import { randomLong } from '@mtcute/core/utils' import { randomLong } from '@mtcute/core/utils'
import { MtMessageNotFoundError } from '../../types/errors'
import { InputMediaLike } from '../../types/media/input-media' import { InputMediaLike } from '../../types/media/input-media'
import { Message } from '../../types/messages/message' import { Message } from '../../types/messages/message'
import { InputPeerLike, PeersIndex } from '../../types/peers' import { InputPeerLike, PeersIndex } from '../../types/peers'
import { normalizeDate, normalizeMessageId } from '../../utils/misc-utils' import { normalizeDate } from '../../utils/misc-utils'
import { assertIsUpdatesGroup } from '../../utils/updates-utils' import { assertIsUpdatesGroup } from '../../utils/updates-utils'
import { _normalizeInputMedia } from '../files/normalize-input-media' import { _normalizeInputMedia } from '../files/normalize-input-media'
import { resolvePeer } from '../users/resolve-peer' import { resolvePeer } from '../users/resolve-peer'
import { _getDiscussionMessage } from './get-discussion-message' import { _getDiscussionMessage } from './get-discussion-message'
import { getMessages } from './get-messages'
import { _parseEntities } from './parse-entities' import { _parseEntities } from './parse-entities'
import { _processCommonSendParameters, CommonSendParams } from './send-common'
/** /**
* Send a group of media. * Send a group of media.
@ -28,56 +27,7 @@ export async function sendMediaGroup(
client: BaseTelegramClient, client: BaseTelegramClient,
chatId: InputPeerLike, chatId: InputPeerLike,
medias: (InputMediaLike | string)[], medias: (InputMediaLike | string)[],
params?: { params?: CommonSendParams & {
/**
* Message to reply to. Either a message object or message ID.
*
* For forums - can also be an ID of the topic (i.e. its top message ID)
*/
replyTo?: number | Message
/**
* Whether to throw an error if {@link replyTo}
* message does not exist.
*
* If that message was not found, `NotFoundError` is thrown,
* with `text` set to `MESSAGE_NOT_FOUND`.
*
* Incurs an additional request, so only use when really needed.
*
* Defaults to `false`
*/
mustReply?: boolean
/**
* Message to comment to. Either a message object or message ID.
*
* This overwrites `replyTo` if it was passed
*/
commentTo?: number | Message
/**
* 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
/**
* Whether to send this message silently.
*/
silent?: boolean
/**
* If set, the message will be scheduled to this date.
* When passing a number, a UNIX time in ms is expected.
*
* You can also pass `0x7FFFFFFE`, this will send the message
* once the peer is online
*/
schedule?: Date | number
/** /**
* Function that will be called after some part has been uploaded. * Function that will be called after some part has been uploaded.
* Only used when a file that requires uploading is passed, * Only used when a file that requires uploading is passed,
@ -88,49 +38,11 @@ export async function sendMediaGroup(
* @param total Total file size * @param total Total file size
*/ */
progressCallback?: (index: number, uploaded: number, total: number) => void progressCallback?: (index: number, uploaded: number, total: number) => void
/**
* Whether to clear draft after sending this message.
*
* Defaults to `false`
*/
clearDraft?: boolean
/**
* Whether to disallow further forwards of this message.
*
* Only for bots, works even if the target chat does not
* have content protection.
*/
forbidForwards?: boolean
/**
* Peer to use when sending the message.
*/
sendAs?: InputPeerLike
}, },
): Promise<Message[]> { ): Promise<Message[]> {
if (!params) params = {} if (!params) params = {}
let peer = await resolvePeer(client, chatId) const { peer, replyTo } = await _processCommonSendParameters(client, chatId, params)
let replyTo = normalizeMessageId(params.replyTo)
if (params.commentTo) {
[peer, replyTo] = await _getDiscussionMessage(client, peer, normalizeMessageId(params.commentTo)!)
}
if (params.mustReply) {
if (!replyTo) {
throw new MtArgumentError('mustReply used, but replyTo was not passed')
}
const msg = await getMessages(client, peer, replyTo)
if (!msg) {
throw new MtMessageNotFoundError(getMarkedPeerId(peer), replyTo, 'to reply to')
}
}
const multiMedia: tl.RawInputSingleMedia[] = [] const multiMedia: tl.RawInputSingleMedia[] = []

View file

@ -1,19 +1,18 @@
import { BaseTelegramClient, getMarkedPeerId, MtArgumentError, tl } from '@mtcute/core' import { BaseTelegramClient, tl } from '@mtcute/core'
import { randomLong } from '@mtcute/core/utils' import { randomLong } from '@mtcute/core/utils'
import { BotKeyboard, ReplyMarkup } from '../../types/bots/keyboards' import { BotKeyboard, ReplyMarkup } from '../../types/bots/keyboards'
import { MtMessageNotFoundError } from '../../types/errors'
import { InputMediaLike } from '../../types/media/input-media' import { InputMediaLike } from '../../types/media/input-media'
import { Message } from '../../types/messages/message' import { Message } from '../../types/messages/message'
import { FormattedString } from '../../types/parser' import { FormattedString } from '../../types/parser'
import { InputPeerLike } from '../../types/peers' import { InputPeerLike } from '../../types/peers'
import { normalizeDate, normalizeMessageId } from '../../utils/misc-utils' import { normalizeDate } from '../../utils/misc-utils'
import { _normalizeInputMedia } from '../files/normalize-input-media' import { _normalizeInputMedia } from '../files/normalize-input-media'
import { resolvePeer } from '../users/resolve-peer' import { resolvePeer } from '../users/resolve-peer'
import { _findMessageInUpdate } from './find-in-update' import { _findMessageInUpdate } from './find-in-update'
import { _getDiscussionMessage } from './get-discussion-message' import { _getDiscussionMessage } from './get-discussion-message'
import { getMessages } from './get-messages'
import { _parseEntities } from './parse-entities' import { _parseEntities } from './parse-entities'
import { _processCommonSendParameters, CommonSendParams } from './send-common'
/** /**
* Send a single media (a photo or a document-based media) * Send a single media (a photo or a document-based media)
@ -30,7 +29,13 @@ export async function sendMedia(
client: BaseTelegramClient, client: BaseTelegramClient,
chatId: InputPeerLike, chatId: InputPeerLike,
media: InputMediaLike | string, media: InputMediaLike | string,
params?: { params?: CommonSendParams & {
/**
* For bots: inline or reply markup or an instruction
* to hide a reply keyboard or to force a reply.
*/
replyMarkup?: ReplyMarkup
/** /**
* Override caption for `media`. * Override caption for `media`.
* *
@ -47,61 +52,6 @@ export async function sendMedia(
*/ */
entities?: tl.TypeMessageEntity[] entities?: tl.TypeMessageEntity[]
/**
* Message to reply to. Either a message object or message ID.
*
* For forums - can also be an ID of the topic (i.e. its top message ID)
*/
replyTo?: number | Message
/**
* Whether to throw an error if {@link replyTo}
* message does not exist.
*
* If that message was not found, `NotFoundError` is thrown,
* with `text` set to `MESSAGE_NOT_FOUND`.
*
* Incurs an additional request, so only use when really needed.
*
* Defaults to `false`
*/
mustReply?: boolean
/**
* Message to comment to. Either a message object or message ID.
*
* This overwrites `replyTo` if it was passed
*/
commentTo?: number | Message
/**
* 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
/**
* Whether to send this message silently.
*/
silent?: boolean
/**
* If set, the message will be scheduled to this date.
* When passing a number, a UNIX time in ms is expected.
*
* You can also pass `0x7FFFFFFE`, this will send the message
* once the peer is online
*/
schedule?: Date | number
/**
* For bots: inline or reply markup or an instruction
* to hide a reply keyboard or to force a reply.
*/
replyMarkup?: ReplyMarkup
/** /**
* Function that will be called after some part has been uploaded. * Function that will be called after some part has been uploaded.
* Only used when a file that requires uploading is passed, * Only used when a file that requires uploading is passed,
@ -111,32 +61,6 @@ export async function sendMedia(
* @param total Total file size * @param total Total file size
*/ */
progressCallback?: (uploaded: number, total: number) => void progressCallback?: (uploaded: number, total: number) => void
/**
* Whether to clear draft after sending this message.
*
* Defaults to `false`
*/
clearDraft?: boolean
/**
* Whether to disallow further forwards of this message.
*
* Only for bots, works even if the target chat does not
* have content protection.
*/
forbidForwards?: boolean
/**
* Peer to use when sending the message.
*/
sendAs?: InputPeerLike
/**
* Whether to dispatch the returned message
* to the client's update handler.
*/
shouldDispatch?: true
}, },
): Promise<Message> { ): Promise<Message> {
if (!params) params = {} if (!params) params = {}
@ -160,26 +84,8 @@ export async function sendMedia(
params.entities || (media as Extract<typeof media, { entities?: unknown }>).entities, params.entities || (media as Extract<typeof media, { entities?: unknown }>).entities,
) )
let peer = await resolvePeer(client, chatId)
const replyMarkup = BotKeyboard._convertToTl(params.replyMarkup) const replyMarkup = BotKeyboard._convertToTl(params.replyMarkup)
const { peer, replyTo } = await _processCommonSendParameters(client, chatId, params)
let replyTo = normalizeMessageId(params.replyTo)
if (params.commentTo) {
[peer, replyTo] = await _getDiscussionMessage(client, peer, normalizeMessageId(params.commentTo)!)
}
if (params.mustReply) {
if (!replyTo) {
throw new MtArgumentError('mustReply used, but replyTo was not passed')
}
const msg = await getMessages(client, peer, replyTo)
if (!msg) {
throw new MtMessageNotFoundError(getMarkedPeerId(peer), replyTo, 'to reply to')
}
}
const res = await client.call({ const res = await client.call({
_: 'messages.sendMedia', _: 'messages.sendMedia',

View file

@ -0,0 +1,43 @@
import { BaseTelegramClient } from '@mtcute/core'
import { Message } from '../../types/messages/message'
import { ParametersSkip2 } from '../../types/utils'
import { sendMedia } from './send-media'
import { sendMediaGroup } from './send-media-group'
import { sendText } from './send-text'
/** Send a text in reply to a given message */
export function replyText(
client: BaseTelegramClient,
message: Message,
...params: ParametersSkip2<typeof sendText>
): ReturnType<typeof sendText> {
const [text, params_ = {}] = params
params_.replyTo = message.id
return sendText(client, message.chat.inputPeer, text, params_)
}
/** Send a media in reply to a given message */
export function replyMedia(
client: BaseTelegramClient,
message: Message,
...params: ParametersSkip2<typeof sendMedia>
): ReturnType<typeof sendMedia> {
const [media, params_ = {}] = params
params_.replyTo = message.id
return sendMedia(client, message.chat.inputPeer, media, params_)
}
/** Send a media group in reply to a given message */
export function replyMediaGroup(
client: BaseTelegramClient,
message: Message,
...params: ParametersSkip2<typeof sendMediaGroup>
): ReturnType<typeof sendMediaGroup> {
const [media, params_ = {}] = params
params_.replyTo = message.id
return sendMediaGroup(client, message.chat.inputPeer, media, params_)
}

View file

@ -1,20 +1,19 @@
import { BaseTelegramClient, getMarkedPeerId, MtArgumentError, MtTypeAssertionError, tl } from '@mtcute/core' import { BaseTelegramClient, getMarkedPeerId, MtTypeAssertionError, tl } from '@mtcute/core'
import { randomLong } from '@mtcute/core/utils' import { randomLong } from '@mtcute/core/utils'
import { BotKeyboard, ReplyMarkup } from '../../types/bots/keyboards' import { BotKeyboard, ReplyMarkup } from '../../types/bots/keyboards'
import { MtMessageNotFoundError } from '../../types/errors'
import { Message } from '../../types/messages/message' import { Message } from '../../types/messages/message'
import { FormattedString } from '../../types/parser' import { FormattedString } from '../../types/parser'
import { InputPeerLike, PeersIndex } from '../../types/peers' import { InputPeerLike, PeersIndex } from '../../types/peers'
import { normalizeDate, normalizeMessageId } from '../../utils/misc-utils' import { normalizeDate } from '../../utils/misc-utils'
import { inputPeerToPeer } from '../../utils/peer-utils' import { inputPeerToPeer } from '../../utils/peer-utils'
import { createDummyUpdate } from '../../utils/updates-utils' import { createDummyUpdate } from '../../utils/updates-utils'
import { getAuthState } from '../auth/_state' import { getAuthState } from '../auth/_state'
import { resolvePeer } from '../users/resolve-peer' import { resolvePeer } from '../users/resolve-peer'
import { _findMessageInUpdate } from './find-in-update' import { _findMessageInUpdate } from './find-in-update'
import { _getDiscussionMessage } from './get-discussion-message' import { _getDiscussionMessage } from './get-discussion-message'
import { getMessages } from './get-messages'
import { _parseEntities } from './parse-entities' import { _parseEntities } from './parse-entities'
import { _processCommonSendParameters, CommonSendParams } from './send-common'
/** /**
* Send a text message * Send a text message
@ -27,41 +26,12 @@ export async function sendText(
client: BaseTelegramClient, client: BaseTelegramClient,
chatId: InputPeerLike, chatId: InputPeerLike,
text: string | FormattedString<string>, text: string | FormattedString<string>,
params?: { params?: CommonSendParams & {
/** /**
* Message to reply to. Either a message object or message ID. * For bots: inline or reply markup or an instruction
* * to hide a reply keyboard or to force a reply.
* For forums - can also be an ID of the topic (i.e. its top message ID)
*/ */
replyTo?: number | Message replyMarkup?: ReplyMarkup
/**
* Whether to throw an error if {@link replyTo}
* message does not exist.
*
* If that message was not found, `NotFoundError` is thrown,
* with `text` set to `MESSAGE_NOT_FOUND`.
*
* Incurs an additional request, so only use when really needed.
*
* Defaults to `false`
*/
mustReply?: boolean
/**
* Message to comment to. Either a message object or message ID.
*
* This overwrites `replyTo` if it was passed
*/
commentTo?: number | Message
/**
* 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 * List of formatting entities to use instead of parsing via a
@ -75,78 +45,14 @@ export async function sendText(
* Whether to disable links preview in this message * Whether to disable links preview in this message
*/ */
disableWebPreview?: boolean disableWebPreview?: boolean
/**
* Whether to send this message silently.
*/
silent?: boolean
/**
* If set, the message will be scheduled to this date.
* When passing a number, a UNIX time in ms is expected.
*
* You can also pass `0x7FFFFFFE`, this will send the message
* once the peer is online
*/
schedule?: Date | number
/**
* For bots: inline or reply markup or an instruction
* to hide a reply keyboard or to force a reply.
*/
replyMarkup?: ReplyMarkup
/**
* Whether to clear draft after sending this message.
*
* Defaults to `false`
*/
clearDraft?: boolean
/**
* Whether to disallow further forwards of this message.
*
* Only for bots, works even if the target chat does not
* have content protection.
*/
forbidForwards?: boolean
/**
* Peer to use when sending the message.
*/
sendAs?: InputPeerLike
/**
* Whether to dispatch the returned message
* to the client's update handler.
*/
shouldDispatch?: true
}, },
): Promise<Message> { ): Promise<Message> {
if (!params) params = {} if (!params) params = {}
const [message, entities] = await _parseEntities(client, text, params.parseMode, params.entities) const [message, entities] = await _parseEntities(client, text, params.parseMode, params.entities)
let peer = await resolvePeer(client, chatId)
const replyMarkup = BotKeyboard._convertToTl(params.replyMarkup) const replyMarkup = BotKeyboard._convertToTl(params.replyMarkup)
const { peer, replyTo } = await _processCommonSendParameters(client, chatId, params)
let replyTo = normalizeMessageId(params.replyTo)
if (params.commentTo) {
[peer, replyTo] = await _getDiscussionMessage(client, peer, normalizeMessageId(params.commentTo)!)
}
if (params.mustReply) {
if (!replyTo) {
throw new MtArgumentError('mustReply used, but replyTo was not passed')
}
const msg = await getMessages(client, peer, replyTo)
if (!msg) {
throw new MtMessageNotFoundError(getMarkedPeerId(peer), replyTo, 'to reply to')
}
}
const res = await client.call({ const res = await client.call({
_: 'messages.sendMessage', _: 'messages.sendMessage',

View file

@ -1,7 +1,7 @@
import { BaseTelegramClient, MtTypeAssertionError, tl } from '@mtcute/core' import { BaseTelegramClient, MaybeArray, MtTypeAssertionError, tl } from '@mtcute/core'
import { assertTypeIs } from '@mtcute/core/utils' import { assertTypeIs, LongSet } from '@mtcute/core/utils'
import { Sticker } from '../../types' import { Message, Sticker } from '../../types'
import { parseDocument } from '../../types/media/document-utils' import { parseDocument } from '../../types/media/document-utils'
/** /**
@ -27,3 +27,30 @@ export async function getCustomEmojis(client: BaseTelegramClient, ids: tl.Long[]
return doc return doc
}) })
} }
/**
* Given one or more messages, extract all unique custom emojis from it and fetch them
*/
export async function getCustomEmojisFromMessages(
client: BaseTelegramClient,
messages: MaybeArray<Message>,
): Promise<Sticker[]> {
const set = new LongSet()
if (!Array.isArray(messages)) messages = [messages]
for (const { raw } of messages) {
if (raw._ === 'messageService' || !raw.entities) continue
for (const entity of raw.entities) {
if (entity._ === 'messageEntityCustomEmoji') {
set.add(entity.documentId)
}
}
}
const arr = set.toArray()
if (!arr.length) return []
return getCustomEmojis(client, arr)
}

View file

@ -8,6 +8,9 @@ import type { Message } from './message'
*/ */
export type InputMessageId = { chatId: InputPeerLike; message: number } | { message: Message } export type InputMessageId = { chatId: InputPeerLike; message: number } | { message: Message }
/** Remove {@link InputMessageId} type from the given type */
export type OmitInputMessageId<T> = Omit<T, 'chatId' | 'message'>
/** @internal */ /** @internal */
export function normalizeInputMessageId(id: InputMessageId) { export function normalizeInputMessageId(id: InputMessageId) {
if ('chatId' in id) return id if ('chatId' in id) return id

View file

@ -47,9 +47,10 @@ export interface MessageForwardInfo {
/** Information about replies to a message */ /** Information about replies to a message */
export interface MessageRepliesInfo { export interface MessageRepliesInfo {
/** /**
* Whether this is a comments thread under a channel post * Whether this message is a channel post that has a comments thread
* in the linked discussion group
*/ */
isComments: false hasComments: false
/** /**
* Total number of replies * Total number of replies
@ -75,7 +76,8 @@ export interface MessageRepliesInfo {
/** Information about comments to a channel post */ /** Information about comments to a channel post */
export interface MessageCommentsInfo extends Omit<MessageRepliesInfo, 'isComments'> { export interface MessageCommentsInfo extends Omit<MessageRepliesInfo, 'isComments'> {
/** /**
* Whether this is a comments thread under a channel post * Whether this message is a channel post that has a comments thread
* in the linked discussion group
*/ */
isComments: true isComments: true
@ -303,7 +305,7 @@ export class Message {
if (!this._replies) { if (!this._replies) {
const r = this.raw.replies const r = this.raw.replies
const obj: MessageRepliesInfo = { const obj: MessageRepliesInfo = {
isComments: r.comments as false, hasComments: r.comments as false,
count: r.replies, count: r.replies,
hasUnread: r.readMaxId !== undefined && r.readMaxId !== r.maxId, hasUnread: r.readMaxId !== undefined && r.readMaxId !== r.maxId,
lastMessageId: r.maxId, lastMessageId: r.maxId,

View file

@ -68,13 +68,6 @@ export class BotChatJoinRequestUpdate {
get invite(): ChatInviteLink { get invite(): ChatInviteLink {
return (this._invite ??= new ChatInviteLink(this.raw.invite)) return (this._invite ??= new ChatInviteLink(this.raw.invite))
} }
/**
* Approve or deny the request.
*/
// hide(action: Parameters<TelegramClient['hideJoinRequest']>[1]['action']): Promise<void> {
// return this.client.hideJoinRequest(this.chat.inputPeer, { action, user: this.user.inputPeer })
// }
} }
makeInspectable(BotChatJoinRequestUpdate) makeInspectable(BotChatJoinRequestUpdate)

View file

@ -47,23 +47,6 @@ export class ChatJoinRequestUpdate {
get totalPending(): number { get totalPending(): number {
return this.raw.requestsPending return this.raw.requestsPending
} }
/**
* Approve or deny the last requested user
*/
// hideLast(action: Parameters<TelegramClient['hideJoinRequest']>[1]['action']): Promise<void> {
// return this.client.hideJoinRequest(this.chatId, { user: this.raw.recentRequesters[0], action })
// }
/**
* Approve or deny all recent requests
* (the ones available in {@link recentRequesters})
*/
// async hideAllRecent(action: Parameters<TelegramClient['hideJoinRequest']>[1]['action']): Promise<void> {
// for (const id of this.raw.recentRequesters) {
// await this.client.hideJoinRequest(this.chatId, { user: id, action })
// }
// }
} }
makeInspectable(ChatJoinRequestUpdate) makeInspectable(ChatJoinRequestUpdate)

View file

@ -76,14 +76,6 @@ export class ChosenInlineResult {
return encodeInlineMessageId(this.raw.msgId) return encodeInlineMessageId(this.raw.msgId)
} }
// async editMessage(params: Parameters<TelegramClient['editInlineMessage']>[1]): Promise<void> {
// if (!this.raw.msgId) {
// throw new MtArgumentError('No message ID, make sure you have included reply markup!')
// }
// return this.client.editInlineMessage(this.raw.msgId, params)
// }
} }
makeInspectable(ChosenInlineResult) makeInspectable(ChosenInlineResult)

View file

@ -24,12 +24,18 @@ export class PollUpdate {
return this.raw.pollId return this.raw.pollId
} }
/**
* Whether this is a shortened version of update, not containing the poll itself.
*/
get isShort(): boolean {
return this.raw.poll === undefined
}
private _poll?: Poll private _poll?: Poll
/** /**
* The poll. * The poll.
* *
* Note that sometimes the update does not have the poll * When {@link isShort} is set, mtcute creates a stub poll
* (Telegram limitation), and mtcute creates a stub poll
* with empty question, answers and flags * with empty question, answers and flags
* (like `quiz`, `public`, etc.) * (like `quiz`, `public`, etc.)
* *
@ -39,8 +45,7 @@ export class PollUpdate {
* *
* Bot API and TDLib do basically the same internally, * Bot API and TDLib do basically the same internally,
* and thus are able to always provide them, * and thus are able to always provide them,
* but mtcute tries to keep it simple in terms of local * but mtcute currently does not have a way to do that.
* storage and only stores the necessary information.
*/ */
get poll(): Poll { get poll(): Poll {
if (!this._poll) { if (!this._poll) {

View file

@ -159,4 +159,14 @@ export class LongSet {
clear() { clear() {
this._set.clear() this._set.clear()
} }
toArray() {
const arr: Long[] = []
for (const v of this._set) {
arr.push(longFromFastString(v))
}
return arr
}
} }

View file

@ -8,19 +8,21 @@ function generateHandler() {
types.forEach((type) => { types.forEach((type) => {
lines.push( lines.push(
`export type ${type.handlerTypeName}Handler<T = ${type.updateType}` + `export type ${type.handlerTypeName}Handler<T = ${type.context}` +
`${type.state ? ', S = never' : ''}> = ParsedUpdateHandler<` + `${type.state ? ', S = never' : ''}> = ParsedUpdateHandler<` +
`'${type.typeName}', T${type.state ? ', S' : ''}>`, `'${type.typeName}', T${type.state ? ', S' : ''}>`,
) )
names.push(`${type.handlerTypeName}Handler`) names.push(`${type.handlerTypeName}Handler`)
}) })
replaceSections('handler.ts', { replaceSections(
'handler.ts',
{
codegen: codegen:
lines.join('\n') + lines.join('\n') + '\n\nexport type UpdateHandler = \n' + names.map((i) => ` | ${i}\n`).join(''),
'\n\nexport type UpdateHandler = \n' + },
names.map((i) => ` | ${i}\n`).join(''), __dirname,
}, __dirname) )
} }
function generateDispatcher() { function generateDispatcher() {
@ -37,9 +39,13 @@ function generateDispatcher() {
* @param handler ${toSentence(type, 'full')} * @param handler ${toSentence(type, 'full')}
* @param group Handler group index * @param group Handler group index
*/ */
on${type.handlerTypeName}(handler: ${type.handlerTypeName}Handler${type.state ? `<${type.updateType}, State extends never ? never : UpdateState<State, SceneName>>` : ''}['callback'], group?: number): void on${type.handlerTypeName}(handler: ${type.handlerTypeName}Handler${
type.state ? `<${type.context}, State extends never ? never : UpdateState<State, SceneName>>` : ''
}['callback'], group?: number): void
${type.state ? ` ${
type.state ?
`
/** /**
* Register ${toSentence(type)} with a filter * Register ${toSentence(type)} with a filter
* *
@ -48,11 +54,15 @@ ${type.state ? `
* @param group Handler group index * @param group Handler group index
*/ */
on${type.handlerTypeName}<Mod>( on${type.handlerTypeName}<Mod>(
filter: UpdateFilter<${type.updateType}, Mod, State>, filter: UpdateFilter<${type.context}, Mod, State>,
handler: ${type.handlerTypeName}Handler<filters.Modify<${type.updateType}, Mod>, State extends never ? never : UpdateState<State, SceneName>>['callback'], handler: ${type.handlerTypeName}Handler<filters.Modify<${
type.context
}, Mod>, State extends never ? never : UpdateState<State, SceneName>>['callback'],
group?: number group?: number
): void ): void
` : ''} ` :
''
}
/** /**
* Register ${toSentence(type)} with a filter * Register ${toSentence(type)} with a filter
@ -62,8 +72,10 @@ ${type.state ? `
* @param group Handler group index * @param group Handler group index
*/ */
on${type.handlerTypeName}<Mod>( on${type.handlerTypeName}<Mod>(
filter: UpdateFilter<${type.updateType}, Mod>, filter: UpdateFilter<${type.context}, Mod>,
handler: ${type.handlerTypeName}Handler<filters.Modify<${type.updateType}, Mod>${type.state ? ', State extends never ? never : UpdateState<State, SceneName>' : ''}>['callback'], handler: ${type.handlerTypeName}Handler<filters.Modify<${type.context}, Mod>${
type.state ? ', State extends never ? never : UpdateState<State, SceneName>' : ''
}>['callback'],
group?: number group?: number
): void ): void
@ -74,13 +86,20 @@ ${type.state ? `
`) `)
}) })
replaceSections('dispatcher.ts', { replaceSections(
'dispatcher.ts',
{
codegen: lines.join('\n'), codegen: lines.join('\n'),
'codegen-imports': 'codegen-imports':
'import {\n' + 'import {\n' +
imports.sort().map((i) => ` ${i},\n`).join('') + imports
.sort()
.map((i) => ` ${i},\n`)
.join('') +
"} from './handler'", "} from './handler'",
}, __dirname) },
__dirname,
)
} }
async function main() { async function main() {

View file

@ -0,0 +1,8 @@
import { ParsedUpdate, TelegramClient } from '@mtcute/client'
export type UpdateContext<T> = T & {
client: TelegramClient
_name: Extract<ParsedUpdate, { data: T }>['name']
}
export type UpdateContextDistributed<T> = T extends never ? never : UpdateContext<T>

View file

@ -0,0 +1,64 @@
import { CallbackQuery, getMarkedPeerId, MtArgumentError, MtMessageNotFoundError, TelegramClient } from '@mtcute/client'
import { UpdateContext } from './base'
/**
* Context of a callback query update.
*
* This is a subclass of {@link CallbackQuery}, so all its fields are also available.
*/
export class CallbackQueryContext extends CallbackQuery implements UpdateContext<CallbackQuery> {
readonly _name = 'callback_query'
constructor(
readonly client: TelegramClient,
query: CallbackQuery,
) {
super(query.raw, query._peers)
}
/** Answer to this callback query */
answer(params: Parameters<TelegramClient['answerCallbackQuery']>[1]) {
return this.client.answerCallbackQuery(this.id, params)
}
/**
* * Message that contained the callback button that was clicked.
*
* Note that the message may have been deleted, in which case
* `MessageNotFoundError` is thrown.
*
* Can only be used if `isInline = false`
*/
async getMessage() {
if (this.raw._ !== 'updateBotCallbackQuery') {
throw new MtArgumentError('Cannot get message for inline callback query')
}
const [msg] = await this.client.getMessages(this.raw.peer, this.raw.msgId)
if (!msg) {
throw new MtMessageNotFoundError(getMarkedPeerId(this.raw.peer), this.raw.msgId, 'Message not found')
}
return msg
}
/**
* Edit the message that contained the callback button that was clicked.
*/
async editMessage(params: Omit<Parameters<TelegramClient['editInlineMessage']>[0], 'messageId'>) {
if (this.raw._ === 'updateInlineBotCallbackQuery') {
return this.client.editInlineMessage({
messageId: this.raw.msgId,
...params,
})
}
return this.client.editMessage({
chatId: this.raw.peer,
message: this.raw.msgId,
...params,
})
}
}

View file

@ -0,0 +1,39 @@
import { BotChatJoinRequestUpdate, TelegramClient } from '@mtcute/client'
import { UpdateContext } from './base'
/**
* Context of a chat join request update (for bots).
*
* This is a subclass of {@link BotChatJoinRequestUpdate}, so all its fields are also available.
*/
export class ChatJoinRequestUpdateContext
extends BotChatJoinRequestUpdate
implements UpdateContext<BotChatJoinRequestUpdate> {
readonly _name = 'bot_chat_join_request'
constructor(
readonly client: TelegramClient,
update: BotChatJoinRequestUpdate,
) {
super(update.raw, update._peers)
}
/** Approve the request */
approve(): Promise<void> {
return this.client.hideJoinRequest({
action: 'approve',
user: this.user.inputPeer,
chatId: this.chat.inputPeer,
})
}
/** Decline the request */
decline(): Promise<void> {
return this.client.hideJoinRequest({
action: 'decline',
user: this.user.inputPeer,
chatId: this.chat.inputPeer,
})
}
}

View file

@ -0,0 +1,38 @@
import { ChosenInlineResult, MtArgumentError, TelegramClient } from '@mtcute/client'
import { UpdateContext } from './base'
/**
* Context of a chosen inline result update.
*
* This is a subclass of {@link ChosenInlineResult}, so all its fields are also available.
*
* > **Note**: To receive these updates, you must enable
* > Inline feedback in [@BotFather](//t.me/botfather)
*/
export class ChosenInlineResultContext extends ChosenInlineResult implements UpdateContext<ChosenInlineResult> {
readonly _name = 'chosen_inline_result'
constructor(
readonly client: TelegramClient,
result: ChosenInlineResult,
) {
super(result.raw, result._peers)
}
/**
* Edit the message that was sent when this inline result that was chosen.
*
* > **Note**: This method can only be used if the message contained a reply markup
*/
async editMessage(params: Parameters<TelegramClient['editInlineMessage']>[0]): Promise<void> {
if (!this.raw.msgId) {
throw new MtArgumentError('No message ID, make sure you have included reply markup!')
}
return this.client.editInlineMessage({
...params,
messageId: this.raw.msgId,
})
}
}

View file

@ -0,0 +1,7 @@
export * from './base'
export * from './callback-query'
export * from './chat-join-request'
export * from './chosen-inline-result'
export * from './inline-query'
export * from './message'
export * from './pre-checkout-query'

View file

@ -0,0 +1,24 @@
import { InlineQuery, ParametersSkip1, TelegramClient } from '@mtcute/client'
import { UpdateContext } from './base'
/**
* Context of an inline query update.
*
* This is a subclass of {@link InlineQuery}, so all its fields are also available.
*/
export class InlineQueryContext extends InlineQuery implements UpdateContext<InlineQuery> {
readonly _name = 'inline_query'
constructor(
readonly client: TelegramClient,
query: InlineQuery,
) {
super(query.raw, query._peers)
}
/** Answer to this inline query */
answer(...params: ParametersSkip1<TelegramClient['answerInlineQuery']>) {
return this.client.answerInlineQuery(this.id, ...params)
}
}

View file

@ -0,0 +1,170 @@
import { Message, OmitInputMessageId, ParametersSkip1, TelegramClient } from '@mtcute/client'
import { DeleteMessagesParams } from '@mtcute/client/src/methods/messages/delete-messages'
import { ForwardMessageOptions } from '@mtcute/client/src/methods/messages/forward-messages'
import { SendCopyParams } from '@mtcute/client/src/methods/messages/send-copy'
import { SendCopyGroupParams } from '@mtcute/client/src/methods/messages/send-copy-group'
import { UpdateContext } from './base'
/**
* Context of a message-related update.
*
* This is a subclass of {@link Message}, 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 MessageContext#messages}.
*/
export class MessageContext extends Message implements UpdateContext<Message> {
// this is primarily for proper types in filters, so don't bother much with actual value
readonly _name = 'new_message'
/**
* List of messages in the message group.
*
* For other updates, this is a list with a single element (`this`).
*/
readonly messages: MessageContext[]
/** Whether this update is about a message group */
readonly isMessageGroup: boolean
constructor(
readonly client: TelegramClient,
message: Message | Message[],
) {
const msg = Array.isArray(message) ? message[message.length - 1] : message
super(msg.raw, msg._peers, msg.isScheduled)
this.messages = Array.isArray(message) ? message.map((it) => new MessageContext(client, it)) : [this]
this.isMessageGroup = Array.isArray(message)
}
/** Get a message that this message is a reply to */
getReplyTo() {
return this.client.getReplyTo(this)
}
/** If this is a channel post, get its automatic forward in the discussion group */
getDiscussionMessage() {
return this.client.getDiscussionMessage({ chatId: this.chat.inputPeer, message: this.id })
}
/** 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']>) {
return this.client.answerText(this, ...params)
}
/** Send a media to the same chat (and topic, if applicable) as a given message */
answerMedia(...params: ParametersSkip1<TelegramClient['answerMedia']>) {
return this.client.answerMedia(this, ...params)
}
/** Send a media group to the same chat (and topic, if applicable) as a given message */
answerMediaGroup(...params: ParametersSkip1<TelegramClient['answerMediaGroup']>) {
return this.client.answerMediaGroup(this, ...params)
}
/** Send a text message in reply to this message */
replyText(...params: ParametersSkip1<TelegramClient['replyText']>) {
return this.client.replyText(this, ...params)
}
/** Send a media in reply to this message */
replyMedia(...params: ParametersSkip1<TelegramClient['replyMedia']>) {
return this.client.replyMedia(this, ...params)
}
/** Send a media group in reply to this message */
replyMediaGroup(...params: ParametersSkip1<TelegramClient['replyMediaGroup']>) {
return this.client.replyMediaGroup(this, ...params)
}
/** Send a text as a comment to this message */
commentText(...params: ParametersSkip1<TelegramClient['commentText']>) {
return this.client.commentText(this, ...params)
}
/** Send a media as a comment to this message */
commentMedia(...params: ParametersSkip1<TelegramClient['commentMedia']>) {
return this.client.commentMedia(this, ...params)
}
/** Send a media group as a comment to this message */
commentMediaGroup(...params: ParametersSkip1<TelegramClient['commentMediaGroup']>) {
return this.client.commentMediaGroup(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

@ -0,0 +1,37 @@
import { ParsedUpdate, TelegramClient } from '@mtcute/client'
import { UpdateContext } from './base'
import { CallbackQueryContext } from './callback-query'
import { ChatJoinRequestUpdateContext } from './chat-join-request'
import { ChosenInlineResultContext } from './chosen-inline-result'
import { InlineQueryContext } from './inline-query'
import { MessageContext } from './message'
import { PreCheckoutQueryContext } from './pre-checkout-query'
/** @internal */
export function _parsedUpdateToContext(client: TelegramClient, update: ParsedUpdate) {
switch (update.name) {
case 'new_message':
case 'edit_message':
case 'message_group':
return new MessageContext(client, update.data)
case 'inline_query':
return new InlineQueryContext(client, update.data)
case 'chosen_inline_result':
return new ChosenInlineResultContext(client, update.data)
case 'callback_query':
return new CallbackQueryContext(client, update.data)
case 'bot_chat_join_request':
return new ChatJoinRequestUpdateContext(client, update.data)
case 'pre_checkout_query':
return new PreCheckoutQueryContext(client, update.data)
}
const _update = update.data as UpdateContext<typeof update.data>
_update.client = client
_update._name = update.name
return _update
}
export type UpdateContextType = ReturnType<typeof _parsedUpdateToContext>

View file

@ -0,0 +1,29 @@
import { PreCheckoutQuery, TelegramClient } from '@mtcute/client'
import { UpdateContext } from './base'
/**
* Context of a pre-checkout query update
*
* This is a subclass of {@link PreCheckoutQuery}, so all its fields are also available.
*/
export class PreCheckoutQueryContext extends PreCheckoutQuery implements UpdateContext<PreCheckoutQuery> {
readonly _name = 'pre_checkout_query'
constructor(
readonly client: TelegramClient,
query: PreCheckoutQuery,
) {
super(query.raw, query._peers)
}
/** Approve the query */
approve(): Promise<void> {
return this.client.answerPreCheckoutQuery(this.raw.queryId)
}
/** Decline the query, optionally with an error */
decline(error = ''): Promise<void> {
return this.client.answerPreCheckoutQuery(this.raw.queryId, { error })
}
}

View file

@ -1,32 +1,38 @@
/* eslint-disable */ /* eslint-disable @typescript-eslint/unified-signatures,@typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-call,max-depth,dot-notation */
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types */ /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types */
// ^^ will be looked into in MTQ-29 // ^^ will be looked into in MTQ-29
import { import {
BotChatJoinRequestUpdate,
BotStoppedUpdate, BotStoppedUpdate,
CallbackQuery,
ChatJoinRequestUpdate, ChatJoinRequestUpdate,
ChatMemberUpdate, ChatMemberUpdate,
ChosenInlineResult,
DeleteMessageUpdate, DeleteMessageUpdate,
DeleteStoryUpdate,
HistoryReadUpdate, HistoryReadUpdate,
InlineQuery,
MaybeAsync, MaybeAsync,
Message,
ParsedUpdate, ParsedUpdate,
PeersIndex, PeersIndex,
PollUpdate, PollUpdate,
PollVoteUpdate, PollVoteUpdate,
PreCheckoutQuery,
StoryUpdate, StoryUpdate,
DeleteStoryUpdate,
TelegramClient, TelegramClient,
tl,
UserStatusUpdate, UserStatusUpdate,
UserTypingUpdate, UserTypingUpdate,
tl,
} from '@mtcute/client' } from '@mtcute/client'
import { MtArgumentError } from '@mtcute/core'
import {
CallbackQueryContext,
ChatJoinRequestUpdateContext,
ChosenInlineResultContext,
InlineQueryContext,
MessageContext,
PreCheckoutQueryContext,
} from './context'
import { UpdateContext } from './context/base'
import { _parsedUpdateToContext, UpdateContextType } from './context/parse'
import { filters, UpdateFilter } from './filters' import { filters, UpdateFilter } from './filters'
// begin-codegen-imports // begin-codegen-imports
import { import {
@ -55,7 +61,6 @@ import {
// end-codegen-imports // end-codegen-imports
import { PropagationAction } from './propagation' import { PropagationAction } from './propagation'
import { defaultStateKeyDelegate, IStateStorage, StateKeyDelegate, UpdateState } from './state' import { defaultStateKeyDelegate, IStateStorage, StateKeyDelegate, UpdateState } from './state'
import { MtArgumentError } from '@mtcute/core'
/** /**
* Updates dispatcher * Updates dispatcher
@ -193,7 +198,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
// order does not matter in the dispatcher, // order does not matter in the dispatcher,
// so we can handle each update in its own task // so we can handle each update in its own task
this.dispatchRawUpdateNow(update, peers).catch((err) => this._client!['_emitError'](err)) this.dispatchRawUpdateNow(update, peers).catch((err) => this._client!._emitError(err))
} }
/** /**
@ -263,7 +268,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
// order does not matter in the dispatcher, // order does not matter in the dispatcher,
// so we can handle each update in its own task // so we can handle each update in its own task
this.dispatchUpdateNow(update).catch((err) => this._client!['_emitError'](err)) this.dispatchUpdateNow(update).catch((err) => this._client!._emitError(err))
} }
/** /**
@ -287,6 +292,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
parsedState?: UpdateState<State, SceneName> | null, parsedState?: UpdateState<State, SceneName> | null,
parsedScene?: string | null, parsedScene?: string | null,
forceScene?: true, forceScene?: true,
parsedContext?: UpdateContextType,
): Promise<boolean> { ): Promise<boolean> {
if (!this._client) return false if (!this._client) return false
@ -301,9 +307,8 @@ export class Dispatcher<State = never, SceneName extends string = string> {
) { ) {
// no need to fetch scene if there are no registered scenes // no need to fetch scene if there are no registered scenes
const key = await this._stateKeyDelegate!( if (!parsedContext) parsedContext = _parsedUpdateToContext(this._client, update)
update.name === 'message_group' ? update.data[0] : update.data, const key = await this._stateKeyDelegate!(parsedContext as any)
)
if (key) { if (key) {
parsedScene = await this._storage.getCurrentScene(key) parsedScene = await this._storage.getCurrentScene(key)
@ -334,16 +339,20 @@ export class Dispatcher<State = never, SceneName extends string = string> {
if (parsedState === undefined) { if (parsedState === undefined) {
if ( if (
this._storage && this._storage &&
(update.name === 'new_message' || update.name === 'edit_message' || update.name === 'callback_query') (update.name === 'new_message' ||
update.name === 'edit_message' ||
update.name === 'callback_query' ||
update.name === 'message_group')
) { ) {
const key = await this._stateKeyDelegate!(update.data) if (!parsedContext) parsedContext = _parsedUpdateToContext(this._client, update)
const key = await this._stateKeyDelegate!(parsedContext as any)
if (key) { if (key) {
let customKey let customKey
if ( if (
!this._customStateKeyDelegate || !this._customStateKeyDelegate ||
(customKey = await this._customStateKeyDelegate(update.data)) (customKey = await this._customStateKeyDelegate(parsedContext as any))
) { ) {
parsedState = new UpdateState( parsedState = new UpdateState(
this._storage, this._storage,
@ -386,8 +395,9 @@ export class Dispatcher<State = never, SceneName extends string = string> {
for (const h of handlers) { for (const h of handlers) {
let result: void | PropagationAction let result: void | PropagationAction
if (!h.check || (await h.check(update.data as any, parsedState as never))) { if (!parsedContext) parsedContext = _parsedUpdateToContext(this._client, update)
result = await h.callback(update.data as any, parsedState as never) if (!h.check || (await h.check(parsedContext as any, parsedState as never))) {
result = await h.callback(parsedContext as any, parsedState as never)
handled = true handled = true
} else continue } else continue
@ -436,7 +446,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
} }
} }
this._postUpdateHandler?.(handled, update, parsedState as any) await this._postUpdateHandler?.(handled, update, parsedState as any)
return handled return handled
} }
@ -601,9 +611,9 @@ export class Dispatcher<State = never, SceneName extends string = string> {
if (child._client) { if (child._client) {
throw new MtArgumentError( throw new MtArgumentError(
'Provided dispatcher is ' + 'Provided dispatcher is ' +
(child._parent (child._parent ?
? 'already a child. Use parent.removeChild() before calling addChild()' 'already a child. Use parent.removeChild() before calling addChild()' :
: 'already bound to a client. Use unbind() before calling addChild()'), 'already bound to a client. Use unbind() before calling addChild()'),
) )
} }
@ -953,21 +963,8 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onNewMessage( onNewMessage(
handler: NewMessageHandler<Message, State extends never ? never : UpdateState<State, SceneName>>['callback'],
group?: number,
): void
/**
* Register a new message handler with a filter
*
* @param filter Update filter
* @param handler New message handler
* @param group Handler group index
*/
onNewMessage<Mod>(
filter: UpdateFilter<Message, Mod, State>,
handler: NewMessageHandler< handler: NewMessageHandler<
filters.Modify<Message, Mod>, MessageContext,
State extends never ? never : UpdateState<State, SceneName> State extends never ? never : UpdateState<State, SceneName>
>['callback'], >['callback'],
group?: number, group?: number,
@ -981,9 +978,25 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onNewMessage<Mod>( onNewMessage<Mod>(
filter: UpdateFilter<Message, Mod>, filter: UpdateFilter<MessageContext, Mod, State>,
handler: NewMessageHandler< handler: NewMessageHandler<
filters.Modify<Message, Mod>, filters.Modify<MessageContext, Mod>,
State extends never ? never : UpdateState<State, SceneName>
>['callback'],
group?: number,
): void
/**
* Register a new message handler with a filter
*
* @param filter Update filter
* @param handler New message handler
* @param group Handler group index
*/
onNewMessage<Mod>(
filter: UpdateFilter<MessageContext, Mod>,
handler: NewMessageHandler<
filters.Modify<MessageContext, Mod>,
State extends never ? never : UpdateState<State, SceneName> State extends never ? never : UpdateState<State, SceneName>
>['callback'], >['callback'],
group?: number, group?: number,
@ -1001,21 +1014,8 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onEditMessage( onEditMessage(
handler: EditMessageHandler<Message, State extends never ? never : UpdateState<State, SceneName>>['callback'],
group?: number,
): void
/**
* Register an edit message handler with a filter
*
* @param filter Update filter
* @param handler Edit message handler
* @param group Handler group index
*/
onEditMessage<Mod>(
filter: UpdateFilter<Message, Mod, State>,
handler: EditMessageHandler< handler: EditMessageHandler<
filters.Modify<Message, Mod>, MessageContext,
State extends never ? never : UpdateState<State, SceneName> State extends never ? never : UpdateState<State, SceneName>
>['callback'], >['callback'],
group?: number, group?: number,
@ -1029,9 +1029,25 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onEditMessage<Mod>( onEditMessage<Mod>(
filter: UpdateFilter<Message, Mod>, filter: UpdateFilter<MessageContext, Mod, State>,
handler: EditMessageHandler< handler: EditMessageHandler<
filters.Modify<Message, Mod>, filters.Modify<MessageContext, Mod>,
State extends never ? never : UpdateState<State, SceneName>
>['callback'],
group?: number,
): void
/**
* Register an edit message handler with a filter
*
* @param filter Update filter
* @param handler Edit message handler
* @param group Handler group index
*/
onEditMessage<Mod>(
filter: UpdateFilter<MessageContext, Mod>,
handler: EditMessageHandler<
filters.Modify<MessageContext, Mod>,
State extends never ? never : UpdateState<State, SceneName> State extends never ? never : UpdateState<State, SceneName>
>['callback'], >['callback'],
group?: number, group?: number,
@ -1050,7 +1066,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
*/ */
onMessageGroup( onMessageGroup(
handler: MessageGroupHandler< handler: MessageGroupHandler<
Message[], MessageContext,
State extends never ? never : UpdateState<State, SceneName> State extends never ? never : UpdateState<State, SceneName>
>['callback'], >['callback'],
group?: number, group?: number,
@ -1064,9 +1080,9 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onMessageGroup<Mod>( onMessageGroup<Mod>(
filter: UpdateFilter<Message[], Mod, State>, filter: UpdateFilter<MessageContext, Mod, State>,
handler: MessageGroupHandler< handler: MessageGroupHandler<
filters.Modify<Message[], Mod>, filters.Modify<MessageContext, Mod>,
State extends never ? never : UpdateState<State, SceneName> State extends never ? never : UpdateState<State, SceneName>
>['callback'], >['callback'],
group?: number, group?: number,
@ -1080,9 +1096,9 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onMessageGroup<Mod>( onMessageGroup<Mod>(
filter: UpdateFilter<Message[], Mod>, filter: UpdateFilter<MessageContext, Mod>,
handler: MessageGroupHandler< handler: MessageGroupHandler<
filters.Modify<Message[], Mod>, filters.Modify<MessageContext, Mod>,
State extends never ? never : UpdateState<State, SceneName> State extends never ? never : UpdateState<State, SceneName>
>['callback'], >['callback'],
group?: number, group?: number,
@ -1109,8 +1125,8 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onDeleteMessage<Mod>( onDeleteMessage<Mod>(
filter: UpdateFilter<DeleteMessageUpdate, Mod>, filter: UpdateFilter<UpdateContext<DeleteMessageUpdate>, Mod>,
handler: DeleteMessageHandler<filters.Modify<DeleteMessageUpdate, Mod>>['callback'], handler: DeleteMessageHandler<filters.Modify<UpdateContext<DeleteMessageUpdate>, Mod>>['callback'],
group?: number, group?: number,
): void ): void
@ -1135,8 +1151,8 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onChatMemberUpdate<Mod>( onChatMemberUpdate<Mod>(
filter: UpdateFilter<ChatMemberUpdate, Mod>, filter: UpdateFilter<UpdateContext<ChatMemberUpdate>, Mod>,
handler: ChatMemberUpdateHandler<filters.Modify<ChatMemberUpdate, Mod>>['callback'], handler: ChatMemberUpdateHandler<filters.Modify<UpdateContext<ChatMemberUpdate>, Mod>>['callback'],
group?: number, group?: number,
): void ): void
@ -1161,8 +1177,8 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onInlineQuery<Mod>( onInlineQuery<Mod>(
filter: UpdateFilter<InlineQuery, Mod>, filter: UpdateFilter<InlineQueryContext, Mod>,
handler: InlineQueryHandler<filters.Modify<InlineQuery, Mod>>['callback'], handler: InlineQueryHandler<filters.Modify<InlineQueryContext, Mod>>['callback'],
group?: number, group?: number,
): void ): void
@ -1187,8 +1203,8 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onChosenInlineResult<Mod>( onChosenInlineResult<Mod>(
filter: UpdateFilter<ChosenInlineResult, Mod>, filter: UpdateFilter<ChosenInlineResultContext, Mod>,
handler: ChosenInlineResultHandler<filters.Modify<ChosenInlineResult, Mod>>['callback'], handler: ChosenInlineResultHandler<filters.Modify<ChosenInlineResultContext, Mod>>['callback'],
group?: number, group?: number,
): void ): void
@ -1205,7 +1221,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
*/ */
onCallbackQuery( onCallbackQuery(
handler: CallbackQueryHandler< handler: CallbackQueryHandler<
CallbackQuery, CallbackQueryContext,
State extends never ? never : UpdateState<State, SceneName> State extends never ? never : UpdateState<State, SceneName>
>['callback'], >['callback'],
group?: number, group?: number,
@ -1219,9 +1235,9 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onCallbackQuery<Mod>( onCallbackQuery<Mod>(
filter: UpdateFilter<CallbackQuery, Mod, State>, filter: UpdateFilter<CallbackQueryContext, Mod, State>,
handler: CallbackQueryHandler< handler: CallbackQueryHandler<
filters.Modify<CallbackQuery, Mod>, filters.Modify<CallbackQueryContext, Mod>,
State extends never ? never : UpdateState<State, SceneName> State extends never ? never : UpdateState<State, SceneName>
>['callback'], >['callback'],
group?: number, group?: number,
@ -1235,9 +1251,9 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onCallbackQuery<Mod>( onCallbackQuery<Mod>(
filter: UpdateFilter<CallbackQuery, Mod>, filter: UpdateFilter<CallbackQueryContext, Mod>,
handler: CallbackQueryHandler< handler: CallbackQueryHandler<
filters.Modify<CallbackQuery, Mod>, filters.Modify<CallbackQueryContext, Mod>,
State extends never ? never : UpdateState<State, SceneName> State extends never ? never : UpdateState<State, SceneName>
>['callback'], >['callback'],
group?: number, group?: number,
@ -1264,8 +1280,8 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onPollUpdate<Mod>( onPollUpdate<Mod>(
filter: UpdateFilter<PollUpdate, Mod>, filter: UpdateFilter<UpdateContext<PollUpdate>, Mod>,
handler: PollUpdateHandler<filters.Modify<PollUpdate, Mod>>['callback'], handler: PollUpdateHandler<filters.Modify<UpdateContext<PollUpdate>, Mod>>['callback'],
group?: number, group?: number,
): void ): void
@ -1290,8 +1306,8 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onPollVote<Mod>( onPollVote<Mod>(
filter: UpdateFilter<PollVoteUpdate, Mod>, filter: UpdateFilter<UpdateContext<PollVoteUpdate>, Mod>,
handler: PollVoteHandler<filters.Modify<PollVoteUpdate, Mod>>['callback'], handler: PollVoteHandler<filters.Modify<UpdateContext<PollVoteUpdate>, Mod>>['callback'],
group?: number, group?: number,
): void ): void
@ -1316,8 +1332,8 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onUserStatusUpdate<Mod>( onUserStatusUpdate<Mod>(
filter: UpdateFilter<UserStatusUpdate, Mod>, filter: UpdateFilter<UpdateContext<UserStatusUpdate>, Mod>,
handler: UserStatusUpdateHandler<filters.Modify<UserStatusUpdate, Mod>>['callback'], handler: UserStatusUpdateHandler<filters.Modify<UpdateContext<UserStatusUpdate>, Mod>>['callback'],
group?: number, group?: number,
): void ): void
@ -1342,8 +1358,8 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onUserTyping<Mod>( onUserTyping<Mod>(
filter: UpdateFilter<UserTypingUpdate, Mod>, filter: UpdateFilter<UpdateContext<UserTypingUpdate>, Mod>,
handler: UserTypingHandler<filters.Modify<UserTypingUpdate, Mod>>['callback'], handler: UserTypingHandler<filters.Modify<UpdateContext<UserTypingUpdate>, Mod>>['callback'],
group?: number, group?: number,
): void ): void
@ -1368,8 +1384,8 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onHistoryRead<Mod>( onHistoryRead<Mod>(
filter: UpdateFilter<HistoryReadUpdate, Mod>, filter: UpdateFilter<UpdateContext<HistoryReadUpdate>, Mod>,
handler: HistoryReadHandler<filters.Modify<HistoryReadUpdate, Mod>>['callback'], handler: HistoryReadHandler<filters.Modify<UpdateContext<HistoryReadUpdate>, Mod>>['callback'],
group?: number, group?: number,
): void ): void
@ -1394,8 +1410,8 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onBotStopped<Mod>( onBotStopped<Mod>(
filter: UpdateFilter<BotStoppedUpdate, Mod>, filter: UpdateFilter<UpdateContext<BotStoppedUpdate>, Mod>,
handler: BotStoppedHandler<filters.Modify<BotStoppedUpdate, Mod>>['callback'], handler: BotStoppedHandler<filters.Modify<UpdateContext<BotStoppedUpdate>, Mod>>['callback'],
group?: number, group?: number,
): void ): void
@ -1420,8 +1436,8 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onBotChatJoinRequest<Mod>( onBotChatJoinRequest<Mod>(
filter: UpdateFilter<BotChatJoinRequestUpdate, Mod>, filter: UpdateFilter<ChatJoinRequestUpdateContext, Mod>,
handler: BotChatJoinRequestHandler<filters.Modify<BotChatJoinRequestUpdate, Mod>>['callback'], handler: BotChatJoinRequestHandler<filters.Modify<ChatJoinRequestUpdateContext, Mod>>['callback'],
group?: number, group?: number,
): void ): void
@ -1446,8 +1462,8 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onChatJoinRequest<Mod>( onChatJoinRequest<Mod>(
filter: UpdateFilter<ChatJoinRequestUpdate, Mod>, filter: UpdateFilter<UpdateContext<ChatJoinRequestUpdate>, Mod>,
handler: ChatJoinRequestHandler<filters.Modify<ChatJoinRequestUpdate, Mod>>['callback'], handler: ChatJoinRequestHandler<filters.Modify<UpdateContext<ChatJoinRequestUpdate>, Mod>>['callback'],
group?: number, group?: number,
): void ): void
@ -1472,8 +1488,8 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onPreCheckoutQuery<Mod>( onPreCheckoutQuery<Mod>(
filter: UpdateFilter<PreCheckoutQuery, Mod>, filter: UpdateFilter<PreCheckoutQueryContext, Mod>,
handler: PreCheckoutQueryHandler<filters.Modify<PreCheckoutQuery, Mod>>['callback'], handler: PreCheckoutQueryHandler<filters.Modify<PreCheckoutQueryContext, Mod>>['callback'],
group?: number, group?: number,
): void ): void
@ -1498,8 +1514,8 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onStoryUpdate<Mod>( onStoryUpdate<Mod>(
filter: UpdateFilter<StoryUpdate, Mod>, filter: UpdateFilter<UpdateContext<StoryUpdate>, Mod>,
handler: StoryUpdateHandler<filters.Modify<StoryUpdate, Mod>>['callback'], handler: StoryUpdateHandler<filters.Modify<UpdateContext<StoryUpdate>, Mod>>['callback'],
group?: number, group?: number,
): void ): void
@ -1524,8 +1540,8 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param group Handler group index * @param group Handler group index
*/ */
onDeleteStory<Mod>( onDeleteStory<Mod>(
filter: UpdateFilter<DeleteStoryUpdate, Mod>, filter: UpdateFilter<UpdateContext<DeleteStoryUpdate>, Mod>,
handler: DeleteStoryHandler<filters.Modify<DeleteStoryUpdate, Mod>>['callback'], handler: DeleteStoryHandler<filters.Modify<UpdateContext<DeleteStoryUpdate>, Mod>>['callback'],
group?: number, group?: number,
): void ): void

View file

@ -1,6 +1,7 @@
import { Message } from '@mtcute/client' import { Message } from '@mtcute/client'
import { MaybeArray, MaybeAsync } from '@mtcute/core' import { MaybeArray, MaybeAsync } from '@mtcute/core'
import { MessageContext } from '../context'
import { chat } from './chat' import { chat } from './chat'
import { and } from './logic' import { and } from './logic'
import { UpdateFilter } from './types' import { UpdateFilter } from './types'
@ -25,7 +26,7 @@ export const command = (
commands: MaybeArray<string | RegExp>, commands: MaybeArray<string | RegExp>,
prefixes: MaybeArray<string> | null = '/', prefixes: MaybeArray<string> | null = '/',
caseSensitive = false, caseSensitive = false,
): UpdateFilter<Message, { command: string[] }> => { ): UpdateFilter<MessageContext, { command: string[] }> => {
if (!Array.isArray(commands)) commands = [commands] if (!Array.isArray(commands)) commands = [commands]
commands = commands.map((i) => (typeof i === 'string' ? i.toLowerCase() : i)) commands = commands.map((i) => (typeof i === 'string' ? i.toLowerCase() : i))
@ -44,7 +45,9 @@ export const command = (
const _prefixes = prefixes const _prefixes = prefixes
const check = (msg: Message): MaybeAsync<boolean> => { const check = (msg: MessageContext): MaybeAsync<boolean> => {
if (msg.isMessageGroup) return check(msg.messages[0])
for (const pref of _prefixes) { for (const pref of _prefixes) {
if (!msg.text.startsWith(pref)) continue if (!msg.text.startsWith(pref)) continue
@ -54,17 +57,15 @@ export const command = (
const m = withoutPrefix.match(regex) const m = withoutPrefix.match(regex)
if (!m) continue if (!m) continue
// const lastGroup = m[m.length - 1] const lastGroup = m[m.length - 1]
// eslint-disable-next-line dot-notation if (lastGroup) {
// todo const state = msg.client.getAuthState()
// if (lastGroup && msg.client['_isBot']) {
// // check bot username if (state.isBot && lastGroup !== state.selfUsername) {
// // eslint-disable-next-line dot-notation return false
// if (lastGroup !== msg.client['_selfUsername']) { }
// return false }
// }
// }
const match = m.slice(1, -1) const match = m.slice(1, -1)
@ -74,7 +75,7 @@ export const command = (
return '' return ''
}) })
;(msg as Message & { command: string[] }).command = match ;(msg as MessageContext & { command: string[] }).command = match
return true return true
} }
@ -98,7 +99,7 @@ export const start = and(chat('private'), command('start'))
* If the parameter is a regex, groups are added to `msg.command`, * If the parameter is a regex, groups are added to `msg.command`,
* meaning that the first group is available in `msg.command[2]`. * meaning that the first group is available in `msg.command[2]`.
*/ */
export const deeplink = (params: MaybeArray<string | RegExp>): UpdateFilter<Message, { command: string[] }> => { export const deeplink = (params: MaybeArray<string | RegExp>): UpdateFilter<MessageContext, { command: string[] }> => {
if (!Array.isArray(params)) { if (!Array.isArray(params)) {
return and(start, (_msg: Message) => { return and(start, (_msg: Message) => {
const msg = _msg as Message & { command: string[] } const msg = _msg as Message & { command: string[] }

View file

@ -1,5 +1,6 @@
export * from './bots' export * from './bots'
export * from './chat' export * from './chat'
export * from './group'
export * from './logic' export * from './logic'
export * from './message' export * from './message'
export * from './state' export * from './state'

View file

@ -1,6 +1,17 @@
import { Chat, ChatType, Message, PollVoteUpdate, User } from '@mtcute/client' import {
BotChatJoinRequestUpdate,
Chat,
ChatMemberUpdate,
ChatType,
HistoryReadUpdate,
Message,
PollVoteUpdate,
User,
UserTypingUpdate,
} from '@mtcute/client'
import { MaybeArray } from '@mtcute/core' import { MaybeArray } from '@mtcute/core'
import { UpdateContextDistributed } from '../context'
import { Modify, UpdateFilter } from './types' import { Modify, UpdateFilter } from './types'
/** /**
@ -19,59 +30,68 @@ export const chat =
(msg) => (msg) =>
msg.chat.chatType === type msg.chat.chatType === type
// prettier-ignore
/** /**
* Filter updates by chat ID(s) or username(s) * Filter updates by marked chat ID(s) or username(s)
*
* Note that only some updates support filtering by username.
*
* For messages, this filter checks for chat where the message
* was sent to, NOT the chat sender.
*/ */
export const chatId = (id: MaybeArray<number | string>): UpdateFilter<Message | PollVoteUpdate> => { export const chatId: {
if (Array.isArray(id)) { (id: MaybeArray<number>): UpdateFilter<UpdateContextDistributed<
const index: Record<number | string, true> = {} | Message
| ChatMemberUpdate
| PollVoteUpdate
| BotChatJoinRequestUpdate
>>
(id: MaybeArray<number | string>): UpdateFilter<UpdateContextDistributed<
| Message
| ChatMemberUpdate
| UserTypingUpdate
| HistoryReadUpdate
| PollVoteUpdate
| BotChatJoinRequestUpdate
>>
} = (id) => {
const indexId = new Set<number>()
const indexUsername = new Set<string>()
let matchSelf = false let matchSelf = false
if (!Array.isArray(id)) id = [id]
id.forEach((id) => { id.forEach((id) => {
if (id === 'me' || id === 'self') { if (id === 'me' || id === 'self') {
matchSelf = true matchSelf = true
} else if (typeof id === 'number') {
indexId.add(id)
} else { } else {
index[id] = true indexUsername.add(id)
} }
}) })
return (upd) => { return (upd) => {
if (upd.constructor === PollVoteUpdate) { switch (upd._name) {
case 'poll_vote': {
const peer = upd.peer const peer = upd.peer
return peer.type === 'chat' && peer.id in index return peer.type === 'chat' && (
indexId.has(peer.id) ||
Boolean(peer.usernames?.some((u) => indexUsername.has(u.username)))
)
} }
case 'history_read':
case 'user_typing': {
const id = upd.chatId
const chat = (upd as Exclude<typeof upd, PollVoteUpdate>).chat return (matchSelf && id === upd.client.getAuthState().userId) || indexId.has(id)
return (matchSelf && chat.isSelf) || chat.id in index || chat.username! in index
} }
} }
if (id === 'me' || id === 'self') { const chat = upd.chat
return (upd) => {
if (upd.constructor === PollVoteUpdate) {
return upd.peer.type === 'chat' && upd.peer.isSelf
}
return (upd as Exclude<typeof upd, PollVoteUpdate>).chat.isSelf return (matchSelf && chat.isSelf) ||
} indexId.has(chat.id) ||
} Boolean(chat.usernames?.some((u) => indexUsername.has(u.username)))
if (typeof id === 'string') {
return (upd) => {
if (upd.constructor === PollVoteUpdate) {
return upd.peer.type === 'chat' && upd.peer.username === id
}
return (upd as Exclude<typeof upd, PollVoteUpdate>).chat.username === id
}
}
return (upd) => {
if (upd.constructor === PollVoteUpdate) {
return upd.peer.type === 'chat' && upd.peer.id === id
}
return (upd as Exclude<typeof upd, PollVoteUpdate>).chat.id === id
} }
} }

View file

@ -0,0 +1,88 @@
import { Message } from '@mtcute/client'
import { MaybeAsync } from '@mtcute/core'
import { MessageContext } from '../context'
import { Modify, UpdateFilter } from './types'
/**
* For message groups, apply a filter to every message in the group.
* Filter will match if **all** messages match.
*
* > **Note**: This also applies type modification to every message in the group
*
* @param filter
* @returns
*/
export function every<Mod, State>(
filter: UpdateFilter<Message, Mod, State>,
): UpdateFilter<
MessageContext,
Mod & {
messages: Modify<MessageContext, Mod>[]
},
State
> {
return (ctx, state) => {
let i = 0
const upds = ctx.messages
const max = upds.length
const next = (): MaybeAsync<boolean> => {
if (i === max) return true
const res = filter(upds[i++], state)
if (typeof res === 'boolean') {
if (!res) return false
return next()
}
return res.then((r: boolean) => {
if (!r) return false
return next()
})
}
return next()
}
}
/**
* For message groups, apply a filter to every message in the group.
* Filter will match if **any** message matches.
*
* > **Note**: This *does not* apply type modification to any message
*
* @param filter
* @returns
*/
// eslint-disable-next-line
export function some<State>(filter: UpdateFilter<Message, any, State>): UpdateFilter<MessageContext, {}, State> {
return (ctx, state) => {
let i = 0
const upds = ctx.messages
const max = upds.length
const next = (): MaybeAsync<boolean> => {
if (i === max) return false
const res = filter(upds[i++], state)
if (typeof res === 'boolean') {
if (res) return true
return next()
}
return res.then((r: boolean) => {
if (r) return true
return next()
})
}
return next()
}
}

View file

@ -256,8 +256,6 @@ export function or<
* @param fns Filters to combine * @param fns Filters to combine
*/ */
export function or(...fns: UpdateFilter<any, any, any>[]): UpdateFilter<any, any, any> { export function or(...fns: UpdateFilter<any, any, any>[]): UpdateFilter<any, any, any> {
if (fns.length === 2) return or(fns[0], fns[1])
return (upd, state) => { return (upd, state) => {
let i = 0 let i = 0
const max = fns.length const max = fns.length
@ -283,79 +281,3 @@ export function or(...fns: UpdateFilter<any, any, any>[]): UpdateFilter<any, any
return next() return next()
} }
} }
/**
* For updates that contain an array of updates (e.g. `message_group`),
* apply a filter to every element of the array.
*
* Filter will match if **all** elements match.
*
* > **Note**: This also applies type modification to every element of the array.
*
* @param filter
* @returns
*/
export function every<Base, Mod, State>(filter: UpdateFilter<Base, Mod, State>): UpdateFilter<Base[], Mod, State> {
return (upds, state) => {
let i = 0
const max = upds.length
const next = (): MaybeAsync<boolean> => {
if (i === max) return true
const res = filter(upds[i++], state)
if (typeof res === 'boolean') {
if (!res) return false
return next()
}
return res.then((r: boolean) => {
if (!r) return false
return next()
})
}
return next()
}
}
/**
* For updates that contain an array of updates (e.g. `message_group`),
* apply a filter to every element of the array.
*
* Filter will match if **all** elements match.
*
* > **Note**: This *does not* apply type modification to any element of the array
*
* @param filter
* @returns
*/
export function some<Base, Mod, State>(filter: UpdateFilter<Base, Mod, State>): UpdateFilter<Base[], Mod, State> {
return (upds, state) => {
let i = 0
const max = upds.length
const next = (): MaybeAsync<boolean> => {
if (i === max) return false
const res = filter(upds[i++], state)
if (typeof res === 'boolean') {
if (res) return true
return next()
}
return res.then((r: boolean) => {
if (r) return true
return next()
})
}
return next()
}
}

View file

@ -201,3 +201,10 @@ export const action = <T extends Exclude<MessageAction, null>['type']>(
return (msg) => msg.action?.type === type return (msg) => msg.action?.type === type
} }
export const sender =
<T extends Message['sender']['type']>(
type: T,
): UpdateFilter<Message, { sender: Extract<Message['sender'], { type: T }> }> =>
(msg) =>
msg.sender.type === type

View file

@ -1,4 +1,4 @@
import { CallbackQuery, Message } from '@mtcute/client' /* eslint-disable @typescript-eslint/no-explicit-any */
import { MaybeAsync } from '@mtcute/core' import { MaybeAsync } from '@mtcute/core'
import { UpdateFilter } from './types' import { UpdateFilter } from './types'
@ -6,7 +6,7 @@ import { UpdateFilter } from './types'
/** /**
* Create a filter for the cases when the state is empty * Create a filter for the cases when the state is empty
*/ */
export const stateEmpty: UpdateFilter<Message> = async (upd, state) => { export const stateEmpty: UpdateFilter<any> = async (upd, state) => {
if (!state) return false if (!state) return false
return !(await state.get()) return !(await state.get())
@ -23,7 +23,7 @@ export const stateEmpty: UpdateFilter<Message> = async (upd, state) => {
export const state = <T>( export const state = <T>(
predicate: (state: T) => MaybeAsync<boolean>, predicate: (state: T) => MaybeAsync<boolean>,
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
): UpdateFilter<Message | Message[] | CallbackQuery, {}, T> => { ): UpdateFilter<any, {}, T> => {
return async (upd, state) => { return async (upd, state) => {
if (!state) return false if (!state) return false
const data = await state.get() const data = await state.get()

View file

@ -1,17 +1,19 @@
// ^^ will be looked into in MTQ-29
import { CallbackQuery, ChosenInlineResult, InlineQuery, Message } from '@mtcute/client' import { CallbackQuery, ChosenInlineResult, InlineQuery, Message } from '@mtcute/client'
import { UpdateContextDistributed } from '../context'
import { UpdateFilter } from './types' import { UpdateFilter } from './types'
function extractText(obj: Message | InlineQuery | ChosenInlineResult | CallbackQuery): string | null { type UpdatesWithText = UpdateContextDistributed<Message | InlineQuery | ChosenInlineResult | CallbackQuery>
if (obj.constructor === Message) {
function extractText(obj: UpdatesWithText): string | null {
switch (obj._name) {
case 'new_message':
return obj.text return obj.text
} else if (obj.constructor === InlineQuery) { case 'inline_query':
return obj.query return obj.query
} else if (obj.constructor === ChosenInlineResult) { case 'chosen_inline_result':
return obj.id return obj.id
} else if (obj.constructor === CallbackQuery) { case 'callback_query':
if (obj.raw.data) return obj.dataStr if (obj.raw.data) return obj.dataStr
} }
@ -31,9 +33,7 @@ function extractText(obj: Message | InlineQuery | ChosenInlineResult | CallbackQ
* @param regex Regex to be matched * @param regex Regex to be matched
*/ */
export const regex = export const regex =
( (regex: RegExp): UpdateFilter<UpdatesWithText, { match: RegExpMatchArray }> =>
regex: RegExp,
): UpdateFilter<Message | InlineQuery | ChosenInlineResult | CallbackQuery, { match: RegExpMatchArray }> =>
(obj) => { (obj) => {
const txt = extractText(obj) const txt = extractText(obj)
if (!txt) return false if (!txt) return false
@ -59,10 +59,7 @@ export const regex =
* @param str String to be matched * @param str String to be matched
* @param ignoreCase Whether string case should be ignored * @param ignoreCase Whether string case should be ignored
*/ */
export const equals = ( export const equals = (str: string, ignoreCase = false): UpdateFilter<UpdatesWithText> => {
str: string,
ignoreCase = false,
): UpdateFilter<Message | InlineQuery | ChosenInlineResult | CallbackQuery> => {
if (ignoreCase) { if (ignoreCase) {
str = str.toLowerCase() str = str.toLowerCase()
@ -82,10 +79,7 @@ export const equals = (
* @param str Substring to be matched * @param str Substring to be matched
* @param ignoreCase Whether string case should be ignored * @param ignoreCase Whether string case should be ignored
*/ */
export const contains = ( export const contains = (str: string, ignoreCase = false): UpdateFilter<UpdatesWithText> => {
str: string,
ignoreCase = false,
): UpdateFilter<Message | InlineQuery | ChosenInlineResult | CallbackQuery> => {
if (ignoreCase) { if (ignoreCase) {
str = str.toLowerCase() str = str.toLowerCase()
@ -113,10 +107,7 @@ export const contains = (
* @param str Substring to be matched * @param str Substring to be matched
* @param ignoreCase Whether string case should be ignored * @param ignoreCase Whether string case should be ignored
*/ */
export const startsWith = ( export const startsWith = (str: string, ignoreCase = false): UpdateFilter<UpdatesWithText> => {
str: string,
ignoreCase = false,
): UpdateFilter<Message | InlineQuery | ChosenInlineResult | CallbackQuery> => {
if (ignoreCase) { if (ignoreCase) {
str = str.toLowerCase() str = str.toLowerCase()
@ -144,10 +135,7 @@ export const startsWith = (
* @param str Substring to be matched * @param str Substring to be matched
* @param ignoreCase Whether string case should be ignored * @param ignoreCase Whether string case should be ignored
*/ */
export const endsWith = ( export const endsWith = (str: string, ignoreCase = false): UpdateFilter<UpdatesWithText> => {
str: string,
ignoreCase = false,
): UpdateFilter<Message | InlineQuery | ChosenInlineResult | CallbackQuery> => {
if (ignoreCase) { if (ignoreCase) {
str = str.toLowerCase() str = str.toLowerCase()

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
// ^^ will be looked into in MTQ-29 // ^^ will be looked into in MTQ-29
@ -73,13 +74,13 @@ import { UpdateState } from '../state'
* > like `and`, `or`, etc. Those are meant to be inferred by the compiler! * > like `and`, `or`, etc. Those are meant to be inferred by the compiler!
*/ */
// we need the second parameter because it carries meta information // we need the second parameter because it carries meta information
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/no-unused-vars
export type UpdateFilter<Base, Mod = {}, State = never> = ( export type UpdateFilter<Base, Mod = {}, State = never> = (
update: Base, update: Base,
state?: UpdateState<State>, state?: UpdateState<State>,
) => MaybeAsync<boolean> ) => MaybeAsync<boolean>
export type Modify<Base, Mod> = Base extends (infer T)[] ? Modify<T, Mod>[] : Omit<Base, keyof Mod> & Mod export type Modify<Base, Mod> = Omit<Base, keyof Mod> & Mod
export type Invert<Base, Mod> = { export type Invert<Base, Mod> = {
[P in keyof Mod & keyof Base]: Exclude<Base[P], Mod[P]> [P in keyof Mod & keyof Base]: Exclude<Base[P], Mod[P]>
} }

View file

@ -3,15 +3,19 @@ import {
CallbackQuery, CallbackQuery,
ChatMemberUpdate, ChatMemberUpdate,
ChosenInlineResult, ChosenInlineResult,
DeleteStoryUpdate,
HistoryReadUpdate,
InlineQuery, InlineQuery,
Message, Message,
PollVoteUpdate, PollVoteUpdate,
StoryUpdate,
User, User,
UserStatusUpdate, UserStatusUpdate,
UserTypingUpdate, UserTypingUpdate,
} from '@mtcute/client' } from '@mtcute/client'
import { MaybeArray } from '@mtcute/core' import { MaybeArray } from '@mtcute/core'
import { UpdateContextDistributed } from '../context'
import { UpdateFilter } from './types' import { UpdateFilter } from './types'
/** /**
@ -25,130 +29,95 @@ export const me: UpdateFilter<Message, { sender: User }> = (msg) =>
*/ */
export const bot: UpdateFilter<Message, { sender: User }> = (msg) => msg.sender.constructor === User && msg.sender.isBot export const bot: UpdateFilter<Message, { sender: User }> = (msg) => msg.sender.constructor === User && msg.sender.isBot
// prettier-ignore
/** /**
* Filter updates by user ID(s) or username(s) * Filter updates by user ID(s) or username(s)
* *
* Usernames are not supported for UserStatusUpdate * Note that only some updates support filtering by username.
* and UserTypingUpdate.
*
*
* For chat member updates, uses `user.id`
*/ */
export const userId = ( export const userId: {
id: MaybeArray<number | string>, (id: MaybeArray<number>): UpdateFilter<UpdateContextDistributed<
): UpdateFilter<
| Message | Message
| UserStatusUpdate | StoryUpdate
| UserTypingUpdate | DeleteStoryUpdate
| InlineQuery | InlineQuery
| ChatMemberUpdate | ChatMemberUpdate
| ChosenInlineResult | ChosenInlineResult
| CallbackQuery | CallbackQuery
| PollVoteUpdate | PollVoteUpdate
| BotChatJoinRequestUpdate | BotChatJoinRequestUpdate
> => { >>
if (Array.isArray(id)) { (id: MaybeArray<number | string>): UpdateFilter<UpdateContextDistributed<
const index: Record<number | string, true> = {} | Message
| UserStatusUpdate
| UserTypingUpdate
| StoryUpdate
| HistoryReadUpdate
| DeleteStoryUpdate
| InlineQuery
| ChatMemberUpdate
| ChosenInlineResult
| CallbackQuery
| PollVoteUpdate
| BotChatJoinRequestUpdate
>>
} = (id) => {
const indexId = new Set<number>()
const indexUsername = new Set<string>()
let matchSelf = false let matchSelf = false
if (!Array.isArray(id)) id = [id]
id.forEach((id) => { id.forEach((id) => {
if (id === 'me' || id === 'self') { if (id === 'me' || id === 'self') {
matchSelf = true matchSelf = true
} else if (typeof id === 'string') {
indexUsername.add(id)
} else { } else {
index[id] = true indexId.add(id)
} }
}) })
return (upd) => { return (upd) => {
const ctor = upd.constructor switch (upd._name) {
case 'new_message':
case 'edit_message': {
const sender = upd.sender
if (ctor === Message) { return (matchSelf && sender.isSelf) ||
const sender = (upd as Message).sender indexId.has(sender.id) ||
indexUsername.has(sender.username!)
}
case 'user_status':
case 'user_typing': {
const id = upd.userId
return (matchSelf && sender.isSelf) || sender.id in index || sender.username! in index return (matchSelf && id === upd.client.getAuthState().userId) ||
} else if (ctor === UserStatusUpdate || ctor === UserTypingUpdate) { indexId.has(id)
// const id = (upd as UserStatusUpdate | UserTypingUpdate).userId }
case 'poll_vote':
return false case 'story':
// todo case 'delete_story': {
// eslint-disable-next-line dot-notation const peer = upd.peer
// (matchSelf && id === upd.client['_userId']) || id in index
} else if (ctor === PollVoteUpdate) {
const peer = (upd as PollVoteUpdate).peer
if (peer.type !== 'user') return false if (peer.type !== 'user') return false
return (matchSelf && peer.isSelf) || peer.id in index || peer.username! in index return (matchSelf && peer.isSelf) ||
indexId.has(peer.id) ||
Boolean(peer.usernames?.some((u) => indexUsername.has(u.username)))
} }
case 'history_read': {
const id = upd.chatId
const user = (upd as Exclude<typeof upd, Message | UserStatusUpdate | UserTypingUpdate | PollVoteUpdate>) return (matchSelf && id === upd.client.getAuthState().userId) ||
.user indexId.has(id)
return (matchSelf && user.isSelf) || user.id in index || user.username! in index
} }
} }
if (id === 'me' || id === 'self') { const user = upd.user
return (upd) => {
const ctor = upd.constructor
if (ctor === Message) {
return (upd as Message).sender.isSelf
} else if (ctor === UserStatusUpdate || ctor === UserTypingUpdate) {
return false
// todo
// (upd as UserStatusUpdate | UserTypingUpdate).userId ===
// eslint-disable-next-line dot-notation
// upd.client['_userId']
} else if (ctor === PollVoteUpdate) {
const peer = (upd as PollVoteUpdate).peer
if (peer.type !== 'user') return false
return peer.isSelf
}
return (upd as Exclude<typeof upd, Message | UserStatusUpdate | UserTypingUpdate | PollVoteUpdate>).user
.isSelf
}
}
if (typeof id === 'string') {
return (upd) => {
const ctor = upd.constructor
if (ctor === Message) {
return (upd as Message).sender.username === id
} else if (ctor === UserStatusUpdate || ctor === UserTypingUpdate) {
// username is not available
return false
} else if (ctor === PollVoteUpdate) {
const peer = (upd as PollVoteUpdate).peer
if (peer.type !== 'user') return false
return peer.username === id
}
return ( return (
(upd as Exclude<typeof upd, Message | UserStatusUpdate | UserTypingUpdate | PollVoteUpdate>).user (matchSelf && user.isSelf) ||
.username === id indexId.has(user.id) ||
) Boolean(user.usernames?.some((u) => indexUsername.has(u.username)))
}
}
return (upd) => {
const ctor = upd.constructor
if (ctor === Message) {
return (upd as Message).sender.id === id
} else if (ctor === UserStatusUpdate || ctor === UserTypingUpdate) {
return (upd as UserStatusUpdate | UserTypingUpdate).userId === id
} else if (ctor === PollVoteUpdate) {
const peer = (upd as PollVoteUpdate).peer
if (peer.type !== 'user') return false
return peer.id === id
}
return (
(upd as Exclude<typeof upd, Message | UserStatusUpdate | UserTypingUpdate | PollVoteUpdate>).user.id === id
) )
} }
} }

View file

@ -1,20 +1,14 @@
import { import {
BotChatJoinRequestUpdate,
BotStoppedUpdate, BotStoppedUpdate,
CallbackQuery,
ChatJoinRequestUpdate, ChatJoinRequestUpdate,
ChatMemberUpdate, ChatMemberUpdate,
ChosenInlineResult,
DeleteMessageUpdate, DeleteMessageUpdate,
DeleteStoryUpdate, DeleteStoryUpdate,
HistoryReadUpdate, HistoryReadUpdate,
InlineQuery,
MaybeAsync, MaybeAsync,
Message,
PeersIndex, PeersIndex,
PollUpdate, PollUpdate,
PollVoteUpdate, PollVoteUpdate,
PreCheckoutQuery,
StoryUpdate, StoryUpdate,
TelegramClient, TelegramClient,
tl, tl,
@ -22,6 +16,15 @@ import {
UserTypingUpdate, UserTypingUpdate,
} from '@mtcute/client' } from '@mtcute/client'
import {
CallbackQueryContext,
ChatJoinRequestUpdateContext,
ChosenInlineResultContext,
InlineQueryContext,
MessageContext,
PreCheckoutQueryContext,
} from './context'
import { UpdateContext } from './context/base'
import { PropagationAction } from './propagation' import { PropagationAction } from './propagation'
export interface BaseUpdateHandler<Name, Handler, Checker> { export interface BaseUpdateHandler<Name, Handler, Checker> {
@ -48,25 +51,31 @@ export type RawUpdateHandler = BaseUpdateHandler<
> >
// begin-codegen // begin-codegen
export type NewMessageHandler<T = Message, S = never> = ParsedUpdateHandler<'new_message', T, S> export type NewMessageHandler<T = MessageContext, S = never> = ParsedUpdateHandler<'new_message', T, S>
export type EditMessageHandler<T = Message, S = never> = ParsedUpdateHandler<'edit_message', T, S> export type EditMessageHandler<T = MessageContext, S = never> = ParsedUpdateHandler<'edit_message', T, S>
export type MessageGroupHandler<T = Message[], S = never> = ParsedUpdateHandler<'message_group', T, S> export type MessageGroupHandler<T = MessageContext, S = never> = ParsedUpdateHandler<'message_group', T, S>
export type DeleteMessageHandler<T = DeleteMessageUpdate> = ParsedUpdateHandler<'delete_message', T> export type DeleteMessageHandler<T = UpdateContext<DeleteMessageUpdate>> = ParsedUpdateHandler<'delete_message', T>
export type ChatMemberUpdateHandler<T = ChatMemberUpdate> = ParsedUpdateHandler<'chat_member', T> export type ChatMemberUpdateHandler<T = UpdateContext<ChatMemberUpdate>> = ParsedUpdateHandler<'chat_member', T>
export type InlineQueryHandler<T = InlineQuery> = ParsedUpdateHandler<'inline_query', T> export type InlineQueryHandler<T = InlineQueryContext> = ParsedUpdateHandler<'inline_query', T>
export type ChosenInlineResultHandler<T = ChosenInlineResult> = ParsedUpdateHandler<'chosen_inline_result', T> export type ChosenInlineResultHandler<T = ChosenInlineResultContext> = ParsedUpdateHandler<'chosen_inline_result', T>
export type CallbackQueryHandler<T = CallbackQuery, S = never> = ParsedUpdateHandler<'callback_query', T, S> export type CallbackQueryHandler<T = CallbackQueryContext, S = never> = ParsedUpdateHandler<'callback_query', T, S>
export type PollUpdateHandler<T = PollUpdate> = ParsedUpdateHandler<'poll', T> export type PollUpdateHandler<T = UpdateContext<PollUpdate>> = ParsedUpdateHandler<'poll', T>
export type PollVoteHandler<T = PollVoteUpdate> = ParsedUpdateHandler<'poll_vote', T> export type PollVoteHandler<T = UpdateContext<PollVoteUpdate>> = ParsedUpdateHandler<'poll_vote', T>
export type UserStatusUpdateHandler<T = UserStatusUpdate> = ParsedUpdateHandler<'user_status', T> export type UserStatusUpdateHandler<T = UpdateContext<UserStatusUpdate>> = ParsedUpdateHandler<'user_status', T>
export type UserTypingHandler<T = UserTypingUpdate> = ParsedUpdateHandler<'user_typing', T> export type UserTypingHandler<T = UpdateContext<UserTypingUpdate>> = ParsedUpdateHandler<'user_typing', T>
export type HistoryReadHandler<T = HistoryReadUpdate> = ParsedUpdateHandler<'history_read', T> export type HistoryReadHandler<T = UpdateContext<HistoryReadUpdate>> = ParsedUpdateHandler<'history_read', T>
export type BotStoppedHandler<T = BotStoppedUpdate> = ParsedUpdateHandler<'bot_stopped', T> export type BotStoppedHandler<T = UpdateContext<BotStoppedUpdate>> = ParsedUpdateHandler<'bot_stopped', T>
export type BotChatJoinRequestHandler<T = BotChatJoinRequestUpdate> = ParsedUpdateHandler<'bot_chat_join_request', T> export type BotChatJoinRequestHandler<T = ChatJoinRequestUpdateContext> = ParsedUpdateHandler<
export type ChatJoinRequestHandler<T = ChatJoinRequestUpdate> = ParsedUpdateHandler<'chat_join_request', T> 'bot_chat_join_request',
export type PreCheckoutQueryHandler<T = PreCheckoutQuery> = ParsedUpdateHandler<'pre_checkout_query', T> T
export type StoryUpdateHandler<T = StoryUpdate> = ParsedUpdateHandler<'story', T> >
export type DeleteStoryHandler<T = DeleteStoryUpdate> = ParsedUpdateHandler<'delete_story', T> export type ChatJoinRequestHandler<T = UpdateContext<ChatJoinRequestUpdate>> = ParsedUpdateHandler<
'chat_join_request',
T
>
export type PreCheckoutQueryHandler<T = PreCheckoutQueryContext> = ParsedUpdateHandler<'pre_checkout_query', T>
export type StoryUpdateHandler<T = UpdateContext<StoryUpdate>> = ParsedUpdateHandler<'story', T>
export type DeleteStoryHandler<T = UpdateContext<DeleteStoryUpdate>> = ParsedUpdateHandler<'delete_story', T>
export type UpdateHandler = export type UpdateHandler =
| RawUpdateHandler | RawUpdateHandler

View file

@ -1,4 +1,5 @@
export * from './callback-data-builder' export * from './callback-data-builder'
export * from './context'
export * from './dispatcher' export * from './dispatcher'
export * from './filters' export * from './filters'
export * from './handler' export * from './handler'

View file

@ -1,5 +1,6 @@
import { MaybeAsync, Message } from '@mtcute/client' import { MaybeAsync } from '@mtcute/client'
import { MessageContext } from './context'
import { Dispatcher } from './dispatcher' import { Dispatcher } from './dispatcher'
import { filters } from './filters' import { filters } from './filters'
import { UpdateState } from './state' import { UpdateState } from './state'
@ -54,13 +55,13 @@ export class WizardScene<State, SceneName extends string = string> extends Dispa
* Add a step to the wizard * Add a step to the wizard
*/ */
addStep( addStep(
handler: (msg: Message, state: UpdateState<State, SceneName>) => MaybeAsync<WizardSceneAction | number>, handler: (msg: MessageContext, state: UpdateState<State, SceneName>) => MaybeAsync<WizardSceneAction | number>,
): void { ): void {
const step = this._steps++ const step = this._steps++
const filter = filters.state<WizardInternalState>((it) => it.$step === step) const filter = filters.state<WizardInternalState>((it) => it.$step === step)
this.onNewMessage(step === 0 ? filters.or(filters.stateEmpty, filter) : filter, async (msg: Message, state) => { this.onNewMessage(step === 0 ? filters.or(filters.stateEmpty, filter) : filter, async (msg, state) => {
const result = await handler(msg, state) const result = await handler(msg, state)
if (typeof result === 'number') { if (typeof result === 'number') {