feat: basic inline queries support (only articles for now)

This commit is contained in:
teidesu 2021-04-28 23:00:51 +03:00
parent 8bb23cd464
commit 3336f295ee
12 changed files with 705 additions and 86 deletions

View file

@ -15,6 +15,7 @@ import { signIn } from './methods/auth/sign-in'
import { signUp } from './methods/auth/sign-up'
import { startTest } from './methods/auth/start-test'
import { start } from './methods/auth/start'
import { answerInlineQuery } from './methods/bots/answer-inline-query'
import { addChatMembers } from './methods/chats/add-chat-members'
import { archiveChats } from './methods/chats/archive-chats'
import { createChannel } from './methods/chats/create-channel'
@ -104,6 +105,7 @@ import {
FileDownloadParameters,
InputChatPermissions,
InputFileLike,
InputInlineResult,
InputMediaLike,
InputPeerLike,
MaybeDynamic,
@ -376,13 +378,108 @@ export interface TelegramClient extends BaseTelegramClient {
/**
* Whether to "catch up" (load missed updates).
* Note: you should register your handlers
* before calling `start()`
* Only applicable if the saved session already
* contained authorization and updates state.
*
* Defaults to true.
* Note: you should register your handlers
* before calling `start()`, otherwise they will
* not be called.
*
* Note: In case the storage was not properly
* closed the last time, "catching up" might
* result in duplicate updates.
*
* Defaults to `false`.
*/
catchUp?: boolean
}): Promise<User>
/**
* Answer an inline query.
*
* @param queryId Inline query ID
* @param results Results of the query
* @param params Additional parameters
*/
answerInlineQuery(
queryId: tl.Long,
results: InputInlineResult[],
params?: {
/**
* Maximum number of time in seconds that the results of the
* query may be cached on the server for.
*
* Defaults to `300`
*/
cacheTime?: number
/**
* Whether the results should be displayed as a gallery instead
* of a vertical list. Only applicable to some media types.
*
* Defaults to `false`
*/
gallery?: boolean
/**
* Whether the results should only be cached on the server
* for the user who sent the query.
*
* Defaults to `false`
*/
private?: boolean
/**
* Next pagination offset (up to 64 bytes).
*
* When user has reached the end of the current results,
* it will re-send the inline query with the same text, but
* with `offset` set to this value.
*
* If omitted or empty string is provided, it is assumed that
* there are no more results.
*/
nextOffset?: string
/**
* If passed, clients will display a button before any other results,
* that when clicked switches the user to a private chat with the bot
* and sends the bot `/start ${parameter}`.
*
* An example from the Bot API docs:
*
* An inline bot that sends YouTube videos can ask the user to connect
* the bot to their YouTube account to adapt search results accordingly.
* To do this, it displays a "Connect your YouTube account" button above
* the results, or even before showing any. The user presses the button,
* switches to a private chat with the bot and, in doing so, passes a start
* parameter that instructs the bot to return an oauth link. Once done, the
* bot can offer a switch_inline button so that the user can easily return to
* the chat where they wanted to use the bot's inline capabilities
*/
switchPm?: {
/**
* Text of the button
*/
text: string
/**
* Parameter for `/start` command
*/
parameter: string
}
/**
* Parse mode to use when parsing inline message text.
* Defaults to current default parse mode (if any).
*
* Passing `null` will explicitly disable formatting.
*
* **Note**: inline results themselves *can not* have markup
* entities, only the messages that are sent once a result is clicked.
*/
parseMode?: string | null
}
): Promise<void>
/**
* Add new members to a group, supergroup or channel.
*
@ -1883,6 +1980,7 @@ export class TelegramClient extends BaseTelegramClient {
signUp = signUp
startTest = startTest
start = start
answerInlineQuery = answerInlineQuery
addChatMembers = addChatMembers
archiveChats = archiveChats
createChannel = createChannel

View file

@ -27,6 +27,7 @@ import {
Message,
ReplyMarkup,
InputMediaLike,
InputInlineResult,
TakeoutSession,
StickerSet
} from '../types'

View file

@ -0,0 +1,112 @@
import { TelegramClient } from '../../client'
import { tl } from '@mtcute/tl'
import { BotInline, InputInlineResult } from '../../types'
/**
* Answer an inline query.
*
* @param queryId Inline query ID
* @param results Results of the query
* @param params Additional parameters
* @internal
*/
export async function answerInlineQuery(
this: TelegramClient,
queryId: tl.Long,
results: InputInlineResult[],
params?: {
/**
* Maximum number of time in seconds that the results of the
* query may be cached on the server for.
*
* Defaults to `300`
*/
cacheTime?: number
/**
* Whether the results should be displayed as a gallery instead
* of a vertical list. Only applicable to some media types.
*
* Defaults to `false`
*/
gallery?: boolean
/**
* Whether the results should only be cached on the server
* for the user who sent the query.
*
* Defaults to `false`
*/
private?: boolean
/**
* Next pagination offset (up to 64 bytes).
*
* When user has reached the end of the current results,
* it will re-send the inline query with the same text, but
* with `offset` set to this value.
*
* If omitted or empty string is provided, it is assumed that
* there are no more results.
*/
nextOffset?: string
/**
* If passed, clients will display a button before any other results,
* that when clicked switches the user to a private chat with the bot
* and sends the bot `/start ${parameter}`.
*
* An example from the Bot API docs:
*
* An inline bot that sends YouTube videos can ask the user to connect
* the bot to their YouTube account to adapt search results accordingly.
* To do this, it displays a "Connect your YouTube account" button above
* the results, or even before showing any. The user presses the button,
* switches to a private chat with the bot and, in doing so, passes a start
* parameter that instructs the bot to return an oauth link. Once done, the
* bot can offer a switch_inline button so that the user can easily return to
* the chat where they wanted to use the bot's inline capabilities
*/
switchPm?: {
/**
* Text of the button
*/
text: string
/**
* Parameter for `/start` command
*/
parameter: string
}
/**
* Parse mode to use when parsing inline message text.
* Defaults to current default parse mode (if any).
*
* Passing `null` will explicitly disable formatting.
*
* **Note**: inline results themselves *can not* have markup
* entities, only the messages that are sent once a result is clicked.
*/
parseMode?: string | null
}
): Promise<void> {
if (!params) params = {}
const tlResults = await Promise.all(results.map(it => BotInline._convertToTl(this, it, params!.parseMode)))
await this.call({
_: 'messages.setInlineBotResults',
queryId,
results: tlResults,
cacheTime: params.cacheTime ?? 300,
gallery: params.gallery,
private: params.private,
nextOffset: params.nextOffset,
switchPm: params.switchPm ? {
_: 'inlineBotSwitchPM',
text: params.switchPm.text,
startParam: params.switchPm.parameter
} : undefined
})
}

View file

@ -1 +1,3 @@
export * from './keyboards'
export * from './inline-query'
export * from './input'

View file

@ -0,0 +1,111 @@
import { makeInspectable } from '@mtcute/client/src/types/utils'
import { tl } from '@mtcute/tl'
import { PeerType, User } from '../peers'
import { TelegramClient } from '../../client'
import { Location } from '../media'
import { InputInlineResult } from './input'
const PEER_TYPE_MAP: Record<tl.TypeInlineQueryPeerType['_'], PeerType> = {
inlineQueryPeerTypeBroadcast: 'channel',
inlineQueryPeerTypeChat: 'group',
inlineQueryPeerTypeMegagroup: 'supergroup',
inlineQueryPeerTypePM: 'user',
inlineQueryPeerTypeSameBotPM: 'bot',
}
export class InlineQuery {
readonly client: TelegramClient
readonly raw: tl.RawUpdateBotInlineQuery
/** Map of users in this message. Mainly for internal use */
readonly _users: Record<number, tl.TypeUser>
constructor(
client: TelegramClient,
raw: tl.RawUpdateBotInlineQuery,
users: Record<number, tl.TypeUser>
) {
this.client = client
this.raw = raw
this._users = users
}
/**
* Unique query ID
*/
get id(): tl.Long {
return this.raw.queryId
}
private _user?: User
/**
* User who sent this query
*/
get user(): User {
if (!this._user) {
this._user = new User(this.client, this._users[this.raw.userId])
}
return this._user
}
/**
* Text of the query (0-512 characters)
*/
get query(): string {
return this.raw.query
}
private _location?: Location
/**
* Attached geolocation.
*
* Only used in case the bot requested user location
*/
get location(): Location | null {
if (this.raw.geo?._ !== 'geoPoint') return null
if (!this._location) {
this._location = new Location(this.raw.geo)
}
return this._location
}
/**
* Inline query scroll offset, controlled by the bot
*/
get offset(): string {
return this.raw.offset
}
/**
* Peer type from which this query was sent.
*
* Can be:
* - `bot`: Query was sent in this bot's PM
* - `user`: Query was sent in somebody's PM
* - `group`: Query was sent in a legacy group
* - `supergroup`: Query was sent in a supergroup
* - `channel`: Query was sent in a channel
* - `null`, in case this information is not available
*/
get peerType(): PeerType | null {
return this.raw.peerType ? PEER_TYPE_MAP[this.raw.peerType._] : null
}
/**
* Answer to this inline query
*
* @param results Inline results
* @param params Additional parameters
*/
async answer(
results: InputInlineResult[],
params: Parameters<TelegramClient['answerInlineQuery']>[2]
): Promise<void> {
return this.client.answerInlineQuery(this.raw.queryId, results, params)
}
}
makeInspectable(InlineQuery)

View file

@ -0,0 +1,2 @@
export * from './input-inline-message'
export * from './input-inline-result'

View file

@ -0,0 +1,65 @@
import { tl } from '@mtcute/tl'
import { BotKeyboard, ReplyMarkup } from '../keyboards'
import { TelegramClient } from '../../../client'
export interface InputInlineMessageText {
type: 'text'
/**
* Text of the message
*/
text: string
/**
* Text markup entities.
* If passed, parse mode is ignored
*/
entities?: tl.TypeMessageEntity[]
/**
* Message reply markup
*/
replyMarkup?: ReplyMarkup
/**
* Whether to disable links preview in this message
*/
disableWebPreview?: boolean
}
export type InputInlineMessage =
| InputInlineMessageText
export namespace BotInlineMessage {
export function text (
text: string,
params?: Omit<InputInlineMessageText, 'type' | 'text'>,
): InputInlineMessageText {
return {
type: 'text',
text,
...(
params || {}
),
}
}
export async function _convertToTl (
client: TelegramClient,
obj: InputInlineMessage,
parseMode?: string | null,
): Promise<tl.TypeInputBotInlineMessage> {
if (obj.type === 'text') {
const [message, entities] = await client['_parseEntities'](obj.text, parseMode, obj.entities)
return {
_: 'inputBotInlineMessageText',
message,
entities,
replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup)
}
}
return obj as never
}
}

View file

@ -0,0 +1,149 @@
import { tl } from '@mtcute/tl'
import { BotInlineMessage, InputInlineMessage } from './input-inline-message'
import { TelegramClient } from '../../../client'
interface BaseInputInlineResult {
/**
* Unique ID of the result
*/
id: string
/**
* Message to send when the result is selected.
*
* By default, is automatically generated,
* and details about how it is generated can be found
* in subclasses' description
*/
message?: InputInlineMessage
}
/**
* Represents an input article.
*
* If `message` is not provided, a {@link InputInlineMessageText} is created
* with web preview enabled and text generated as follows:
* ```
* {{#if url}}
* <a href="{{url}}"><b>{{title}}</b></a>
* {{else}}
* <b>{{title}}</b>
* {{/if}}
* {{#if description}}
* {{description}}
* {{/if}}
* ```
* > Handlebars syntax is used. HTML tags are used to signify entities,
* > but in fact raw TL entity objects are created
*/
export interface InputInlineResultArticle extends BaseInputInlineResult {
type: 'article'
/**
* Title of the result (must not be empty)
*/
title: string
/**
* Description of the result
*/
description?: string
/**
* URL of the article
*/
url?: string
/**
* Whether to prevent article URL from
* displaying by the client
*
* Defaults to `false`
*/
hideUrl?: boolean
/**
* Article thumbnail URL (only jpeg).
*/
thumb?: string | tl.RawInputWebDocument
}
export type InputInlineResult = InputInlineResultArticle
export namespace BotInline {
export function article(
params: Omit<InputInlineResultArticle, 'type'>
): InputInlineResultArticle {
return {
type: 'article',
...params,
}
}
export async function _convertToTl(
client: TelegramClient,
obj: InputInlineResult,
parseMode?: string | null
): Promise<tl.TypeInputBotInlineResult> {
if (obj.type === 'article') {
let sendMessage: tl.TypeInputBotInlineMessage
if (obj.message) {
sendMessage = await BotInlineMessage._convertToTl(client, obj.message, parseMode)
} else {
let message = obj.title
const entities: tl.TypeMessageEntity[] = [
{
_: 'messageEntityBold',
offset: 0,
length: message.length
}
]
if (obj.url) {
entities.push({
_: 'messageEntityTextUrl',
url: obj.url,
offset: 0,
length: message.length
})
}
if (obj.description) {
message += '\n' + obj.description
}
sendMessage = {
_: 'inputBotInlineMessageText',
message,
entities
}
}
return {
_: 'inputBotInlineResult',
id: obj.id,
type: obj.type,
title: obj.title,
description: obj.description,
url: obj.hideUrl ? undefined : obj.url,
content: obj.url && obj.hideUrl ? {
_: 'inputWebDocument',
url: obj.url,
mimeType: 'text/html',
size: 0,
attributes: []
} : undefined,
thumb: typeof obj.thumb === 'string' ? {
_: 'inputWebDocument',
size: 0,
url: obj.thumb,
mimeType: 'image/jpeg',
attributes: [],
} : obj.thumb,
sendMessage
}
}
return obj as never
}
}

View file

@ -14,7 +14,7 @@ interface BaseInputMedia {
/**
* Caption entities of the media.
* If passed, {@link caption} is ignored
* If passed, parse mode is ignored
*/
entities?: tl.TypeMessageEntity[]

View file

@ -1,9 +1,35 @@
import { ChatMemberUpdateHandler, NewMessageHandler, RawUpdateHandler } from './handler'
import {
ChatMemberUpdateHandler,
InlineQueryHandler,
NewMessageHandler,
RawUpdateHandler,
UpdateHandler,
} from './handler'
import { filters, UpdateFilter } from './filters'
import { Message } from '@mtcute/client'
import { InlineQuery, Message } from '@mtcute/client'
import { ChatMemberUpdate } from './updates'
function _create<T extends UpdateHandler>(
type: T['type'],
filter: any,
handler?: any
): T {
if (handler) {
return {
type,
check: filter,
callback: handler
} as any
}
return {
type,
callback: filter
} as any
}
export namespace handlers {
/**
* Create a {@link RawUpdateHandler}
*
@ -25,18 +51,7 @@ export namespace handlers {
): RawUpdateHandler
export function rawUpdate(filter: any, handler?: any): RawUpdateHandler {
if (handler) {
return {
type: 'raw',
check: filter,
callback: handler
}
}
return {
type: 'raw',
callback: filter
}
return _create('raw', filter, handler)
}
/**
@ -63,18 +78,7 @@ export namespace handlers {
filter: any,
handler?: any
): NewMessageHandler {
if (handler) {
return {
type: 'new_message',
check: filter,
callback: handler,
}
}
return {
type: 'new_message',
callback: filter,
}
return _create('new_message', filter, handler)
}
/**
@ -101,17 +105,33 @@ export namespace handlers {
filter: any,
handler?: any
): ChatMemberUpdateHandler {
if (handler) {
return {
type: 'chat_member',
check: filter,
callback: handler,
}
return _create('chat_member', filter, handler)
}
return {
type: 'chat_member',
callback: filter,
}
/**
* Create an inline query handler
*
* @param handler Inline query handler
*/
export function inlineQuery(
handler: InlineQueryHandler['callback']
): InlineQueryHandler
/**
* Create an inline query with a filter
*
* @param filter Inline query update filter
* @param handler Inline query handler
*/
export function inlineQuery<Mod>(
filter: UpdateFilter<InlineQuery, Mod>,
handler: InlineQueryHandler<filters.Modify<InlineQuery, Mod>>['callback']
): InlineQueryHandler
export function inlineQuery(
filter: any,
handler?: any
): InlineQueryHandler {
return _create('inline_query', filter, handler)
}
}

View file

@ -1,4 +1,9 @@
import { Message, MtCuteArgumentError, TelegramClient } from '@mtcute/client'
import {
InlineQuery,
Message,
MtCuteArgumentError,
TelegramClient,
} from '@mtcute/client'
import { tl } from '@mtcute/tl'
import {
ContinuePropagation,
@ -7,7 +12,7 @@ import {
StopPropagation,
} from './propagation'
import {
ChatMemberUpdateHandler,
ChatMemberUpdateHandler, InlineQueryHandler,
NewMessageHandler,
RawUpdateHandler,
UpdateHandler,
@ -18,6 +23,54 @@ import { ChatMemberUpdate } from './updates'
const noop = () => {}
type ParserFunction = (
client: TelegramClient,
upd: tl.TypeUpdate | tl.TypeMessage,
users: Record<number, tl.TypeUser>,
chats: Record<number, tl.TypeChat>
) => any
type UpdateParser = [Exclude<UpdateHandler['type'], 'raw'>, ParserFunction]
const baseMessageParser: ParserFunction = (
client: TelegramClient,
upd,
users,
chats
) =>
new Message(
client,
tl.isAnyMessage(upd) ? upd : (upd as any).message,
users,
chats
)
const newMessageParser: UpdateParser = ['new_message', baseMessageParser]
const editMessageParser: UpdateParser = ['edit_message', baseMessageParser]
const chatMemberParser: UpdateParser = [
'chat_member',
(client, upd, users, chats) =>
new ChatMemberUpdate(client, upd as any, users, chats),
]
const PARSERS: Partial<
Record<(tl.TypeUpdate | tl.TypeMessage)['_'], UpdateParser>
> = {
message: newMessageParser,
messageEmpty: newMessageParser,
messageService: newMessageParser,
updateNewMessage: newMessageParser,
updateNewChannelMessage: newMessageParser,
updateNewScheduledMessage: newMessageParser,
updateEditMessage: editMessageParser,
updateEditChannelMessage: editMessageParser,
updateChatParticipant: chatMemberParser,
updateChannelParticipant: chatMemberParser,
updateBotInlineQuery: [
'inline_query',
(client, upd, users) => new InlineQuery(client, upd as any, users),
],
}
/**
* The dispatcher
*/
@ -115,36 +168,10 @@ export class Dispatcher {
if (!this._client) return
const isRawMessage = tl.isAnyMessage(update)
let message: Message | null = null
if (
update._ === 'updateNewMessage' ||
update._ === 'updateNewChannelMessage' ||
update._ === 'updateNewScheduledMessage' ||
update._ === 'updateEditMessage' ||
update._ === 'updateEditChannelMessage' ||
isRawMessage
) {
message = new Message(
this._client,
isRawMessage ? update : (update as any).message,
users,
chats
)
}
let chatMember: ChatMemberUpdate | null = null
if (
update._ === 'updateChatParticipant' ||
update._ === 'updateChannelParticipant'
) {
chatMember = new ChatMemberUpdate(
this._client,
update,
users,
chats
)
}
const pair = PARSERS[update._]
const parsed = pair
? pair[1](this._client, update, users, chats)
: undefined
outer: for (const grp of this._groupsOrder) {
for (const handler of this._groups[grp]) {
@ -168,19 +195,12 @@ export class Dispatcher {
chats
)
} else if (
handler.type === 'new_message' &&
message &&
pair &&
handler.type === pair[0] &&
(!handler.check ||
(await handler.check(message, this._client)))
(await handler.check(parsed, this._client)))
) {
result = await handler.callback(message, this._client)
} else if (
handler.type === 'chat_member' &&
chatMember &&
(!handler.check ||
(await handler.check(chatMember, this._client)))
) {
result = await handler.callback(chatMember, this._client)
result = await handler.callback(parsed, this._client)
} else continue
if (result === ContinuePropagation) continue
@ -407,7 +427,7 @@ export class Dispatcher {
}
/**
* Register a chat member update filter without any filters.
* Register a chat member update handler without any filters.
*
* @param handler Update handler
* @param group Handler group index
@ -437,4 +457,36 @@ export class Dispatcher {
onChatMemberUpdate(filter: any, handler?: any, group?: number): void {
this._addKnownHandler('chatMemberUpdate', filter, handler, group)
}
/**
* Register an inline query handler without any filters.
*
* @param handler Update handler
* @param group Handler group index
* @internal
*/
onInlineQuery(
handler: InlineQueryHandler['callback'],
group?: number
): void
/**
* Register an inline query handler with a given filter
*
* @param filter Update filter
* @param handler Update handler
* @param group Handler group index
*/
onInlineQuery<Mod>(
filter: UpdateFilter<InlineQuery, Mod>,
handler: InlineQueryHandler<
filters.Modify<InlineQuery, Mod>
>['callback'],
group?: number
): void
/** @internal */
onInlineQuery(filter: any, handler?: any, group?: number): void {
this._addKnownHandler('inlineQuery', filter, handler, group)
}
}

View file

@ -1,4 +1,4 @@
import { MaybeAsync, Message, TelegramClient } from '@mtcute/client'
import { MaybeAsync, Message, TelegramClient, InlineQuery } from '@mtcute/client'
import { tl } from '@mtcute/tl'
import { PropagationSymbol } from './propagation'
import { ChatMemberUpdate } from './updates'
@ -39,12 +39,19 @@ export type NewMessageHandler<T = Message> = ParsedUpdateHandler<
'new_message',
T
>
export type EditMessageHandler<T = Message> = ParsedUpdateHandler<
'edit_message',
T
>
export type ChatMemberUpdateHandler<T = ChatMemberUpdate> = ParsedUpdateHandler<
'chat_member',
T
>
export type InlineQueryHandler<T = InlineQuery> = ParsedUpdateHandler<'inline_query', T>
export type UpdateHandler =
| RawUpdateHandler
| NewMessageHandler
| EditMessageHandler
| ChatMemberUpdateHandler
| InlineQueryHandler