From a0b3e9cc6e81b922bad57b64abdcd8163a5dacec Mon Sep 17 00:00:00 2001 From: Alina Sireneva Date: Thu, 7 Mar 2024 18:11:02 +0300 Subject: [PATCH] chore: avoid using namespaces in favor of esm --- .../methods/files/normalize-input-media.ts | 2 +- .../methods/messages/send-media-group.ts | 2 +- .../highlevel/methods/messages/send-media.ts | 4 +- .../highlevel/methods/messages/send-text.ts | 2 +- .../src/highlevel/types/bots/command-scope.ts | 106 -- .../types/bots/command-scope/index.ts | 12 + .../types/bots/command-scope/inner.ts | 96 ++ .../core/src/highlevel/types/bots/index.ts | 8 +- .../types/bots/inline-message/factories.ts | 204 ++++ .../types/bots/inline-message/index.ts | 5 + .../types/bots/inline-message/types.ts | 147 +++ .../types/bots/inline-result/factories.ts | 552 +++++++++ .../types/bots/inline-result/index.ts | 4 + .../types/bots/inline-result/types.ts | 501 ++++++++ .../src/highlevel/types/bots/input/index.ts | 2 - .../types/bots/input/input-inline-message.ts | 340 ------ .../types/bots/input/input-inline-result.ts | 1044 ----------------- .../src/highlevel/types/bots/keyboards.ts | 478 -------- .../builder.test.ts} | 2 +- .../builder.ts} | 2 +- .../types/bots/keyboards/factories.ts | 427 +++++++ .../highlevel/types/bots/keyboards/index.ts | 16 + .../bots/{ => keyboards}/keyboards.test.ts | 2 +- .../highlevel/types/bots/keyboards/types.ts | 46 + .../core/src/highlevel/types/media/index.ts | 2 +- .../types/media/input-media/factories.ts | 300 +++++ .../types/media/input-media/index.ts | 3 + .../{input-media.ts => input-media/types.ts} | 291 +---- .../src/highlevel/types/messages/message.ts | 2 +- .../core/src/highlevel/types/misc/index.ts | 2 +- .../types/misc/input-privacy-rule.ts | 97 -- .../types/misc/input-privacy-rule/allow.ts | 38 + .../types/misc/input-privacy-rule/bundle.ts | 4 + .../types/misc/input-privacy-rule/disallow.ts | 34 + .../types/misc/input-privacy-rule/index.ts | 4 + .../types/misc/input-privacy-rule/types.ts | 15 + packages/file-id/src/types-inner.ts | 232 ++++ packages/file-id/src/types.ts | 242 +--- 38 files changed, 2662 insertions(+), 2608 deletions(-) delete mode 100644 packages/core/src/highlevel/types/bots/command-scope.ts create mode 100644 packages/core/src/highlevel/types/bots/command-scope/index.ts create mode 100644 packages/core/src/highlevel/types/bots/command-scope/inner.ts create mode 100644 packages/core/src/highlevel/types/bots/inline-message/factories.ts create mode 100644 packages/core/src/highlevel/types/bots/inline-message/index.ts create mode 100644 packages/core/src/highlevel/types/bots/inline-message/types.ts create mode 100644 packages/core/src/highlevel/types/bots/inline-result/factories.ts create mode 100644 packages/core/src/highlevel/types/bots/inline-result/index.ts create mode 100644 packages/core/src/highlevel/types/bots/inline-result/types.ts delete mode 100644 packages/core/src/highlevel/types/bots/input/index.ts delete mode 100644 packages/core/src/highlevel/types/bots/input/input-inline-message.ts delete mode 100644 packages/core/src/highlevel/types/bots/input/input-inline-result.ts delete mode 100644 packages/core/src/highlevel/types/bots/keyboards.ts rename packages/core/src/highlevel/types/bots/{keyboard-builder.test.ts => keyboards/builder.test.ts} (99%) rename packages/core/src/highlevel/types/bots/{keyboard-builder.ts => keyboards/builder.ts} (99%) create mode 100644 packages/core/src/highlevel/types/bots/keyboards/factories.ts create mode 100644 packages/core/src/highlevel/types/bots/keyboards/index.ts rename packages/core/src/highlevel/types/bots/{ => keyboards}/keyboards.test.ts (99%) create mode 100644 packages/core/src/highlevel/types/bots/keyboards/types.ts create mode 100644 packages/core/src/highlevel/types/media/input-media/factories.ts create mode 100644 packages/core/src/highlevel/types/media/input-media/index.ts rename packages/core/src/highlevel/types/media/{input-media.ts => input-media/types.ts} (61%) delete mode 100644 packages/core/src/highlevel/types/misc/input-privacy-rule.ts create mode 100644 packages/core/src/highlevel/types/misc/input-privacy-rule/allow.ts create mode 100644 packages/core/src/highlevel/types/misc/input-privacy-rule/bundle.ts create mode 100644 packages/core/src/highlevel/types/misc/input-privacy-rule/disallow.ts create mode 100644 packages/core/src/highlevel/types/misc/input-privacy-rule/index.ts create mode 100644 packages/core/src/highlevel/types/misc/input-privacy-rule/types.ts create mode 100644 packages/file-id/src/types-inner.ts diff --git a/packages/core/src/highlevel/methods/files/normalize-input-media.ts b/packages/core/src/highlevel/methods/files/normalize-input-media.ts index 894cb6ef..4cca8f76 100644 --- a/packages/core/src/highlevel/methods/files/normalize-input-media.ts +++ b/packages/core/src/highlevel/methods/files/normalize-input-media.ts @@ -8,7 +8,7 @@ import { assertTypeIs } from '../../../utils/type-assertions.js' import { ITelegramClient } from '../../client.types.js' import { isUploadedFile } from '../../types/files/uploaded-file.js' import { UploadFileLike } from '../../types/files/utils.js' -import { InputMediaLike } from '../../types/media/input-media.js' +import { InputMediaLike } from '../../types/media/input-media/types.js' import { fileIdToInputDocument, fileIdToInputPhoto } from '../../utils/convert-file-id.js' import { extractFileName } from '../../utils/file-utils.js' import { normalizeDate } from '../../utils/misc-utils.js' diff --git a/packages/core/src/highlevel/methods/messages/send-media-group.ts b/packages/core/src/highlevel/methods/messages/send-media-group.ts index 8eab137e..b36a7529 100644 --- a/packages/core/src/highlevel/methods/messages/send-media-group.ts +++ b/packages/core/src/highlevel/methods/messages/send-media-group.ts @@ -2,7 +2,7 @@ import { tl } from '@mtcute/tl' import { randomLong } from '../../../utils/long-utils.js' import { ITelegramClient } from '../../client.types.js' -import { InputMediaLike } from '../../types/media/input-media.js' +import { InputMediaLike } from '../../types/media/input-media/types.js' import { Message } from '../../types/messages/message.js' import { InputPeerLike, PeersIndex } from '../../types/peers/index.js' import { assertIsUpdatesGroup } from '../../updates/utils.js' diff --git a/packages/core/src/highlevel/methods/messages/send-media.ts b/packages/core/src/highlevel/methods/messages/send-media.ts index 414ecdeb..2cb4ade3 100644 --- a/packages/core/src/highlevel/methods/messages/send-media.ts +++ b/packages/core/src/highlevel/methods/messages/send-media.ts @@ -1,7 +1,7 @@ import { randomLong } from '../../../utils/long-utils.js' import { ITelegramClient } from '../../client.types.js' -import { BotKeyboard, ReplyMarkup } from '../../types/bots/keyboards.js' -import { InputMediaLike } from '../../types/media/input-media.js' +import { BotKeyboard, ReplyMarkup } from '../../types/bots/keyboards/index.js' +import { InputMediaLike } from '../../types/media/input-media/types.js' import { Message } from '../../types/messages/message.js' import { InputText } from '../../types/misc/entities.js' import { InputPeerLike } from '../../types/peers/index.js' diff --git a/packages/core/src/highlevel/methods/messages/send-text.ts b/packages/core/src/highlevel/methods/messages/send-text.ts index daaf2092..f33aa4dc 100644 --- a/packages/core/src/highlevel/methods/messages/send-text.ts +++ b/packages/core/src/highlevel/methods/messages/send-text.ts @@ -4,7 +4,7 @@ import { MtTypeAssertionError } from '../../../types/errors.js' import { randomLong } from '../../../utils/long-utils.js' import { getMarkedPeerId } from '../../../utils/peer-utils.js' import { ITelegramClient } from '../../client.types.js' -import { BotKeyboard, ReplyMarkup } from '../../types/bots/keyboards.js' +import { BotKeyboard, ReplyMarkup } from '../../types/bots/keyboards/index.js' import { Message } from '../../types/messages/message.js' import { InputText } from '../../types/misc/entities.js' import { InputPeerLike, PeersIndex } from '../../types/peers/index.js' diff --git a/packages/core/src/highlevel/types/bots/command-scope.ts b/packages/core/src/highlevel/types/bots/command-scope.ts deleted file mode 100644 index 3e840668..00000000 --- a/packages/core/src/highlevel/types/bots/command-scope.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { tl } from '@mtcute/tl' - -import { InputPeerLike } from '../peers/index.js' - -/** - * Helper constants and builder functions for methods - * related to bot commands. - * - * You can learn more about bot command scopes in - * [Bot API docs](https://core.telegram.org/bots/api#botcommandscope) - */ -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace BotCommands { - /** - * Intermediate bot scope, that is converted to - * TL type `BotCommandScope` by the respective functions. - * - * Used to avoid manually resolving peers. - */ - export type IntermediateScope = - | { - type: 'peer' | 'peer_admins' - peer: InputPeerLike - } - | { - type: 'member' - chat: InputPeerLike - user: InputPeerLike - } - - /** - * Default commands scope. - * - * Used if no commands with a narrower scope are available. - */ - export const default_: tl.RawBotCommandScopeDefault = { - _: 'botCommandScopeDefault', - } as const - - /** - * Scope that covers all private chats - */ - export const allPrivate: tl.RawBotCommandScopeUsers = { - _: 'botCommandScopeUsers', - } as const - - /** - * Scope that covers all group chats (both legacy and supergroups) - */ - export const allGroups: tl.RawBotCommandScopeChats = { - _: 'botCommandScopeChats', - } as const - - /** - * Scope that covers all group chat administrators (both legacy and supergroups) - */ - export const allGroupAdmins: tl.RawBotCommandScopeChatAdmins = { - _: 'botCommandScopeChatAdmins', - } as const - - /** - * Scope that covers a specific peer (a single user in PMs, - * or all users of a legacy group or a supergroup) - */ - export function peer(peer: InputPeerLike): IntermediateScope { - return { - type: 'peer', - peer, - } - } - - /** - * Scope that covers admins in a specific group - */ - export function groupAdmins(peer: InputPeerLike): IntermediateScope { - return { - type: 'peer_admins', - peer, - } - } - - /** - * Scope that covers a specific user in a specific group - */ - export function groupMember(chat: InputPeerLike, user: InputPeerLike): IntermediateScope { - return { - type: 'member', - chat, - user, - } - } - - /** - * Helper function to create a bot command object - * - * @param command Bot command (without slash) - * @param description Command description - */ - export function cmd(command: string, description: string): tl.RawBotCommand { - return { - _: 'botCommand', - command, - description, - } - } -} diff --git a/packages/core/src/highlevel/types/bots/command-scope/index.ts b/packages/core/src/highlevel/types/bots/command-scope/index.ts new file mode 100644 index 00000000..031c27a9 --- /dev/null +++ b/packages/core/src/highlevel/types/bots/command-scope/index.ts @@ -0,0 +1,12 @@ +import * as BotCommands from './inner.js' + +export { + /** + * Helper constants and builder functions for methods + * related to bot commands. + * + * You can learn more about bot command scopes in + * [Bot API docs](https://core.telegram.org/bots/api#botcommandscope) + */ + BotCommands, +} diff --git a/packages/core/src/highlevel/types/bots/command-scope/inner.ts b/packages/core/src/highlevel/types/bots/command-scope/inner.ts new file mode 100644 index 00000000..5f84cd65 --- /dev/null +++ b/packages/core/src/highlevel/types/bots/command-scope/inner.ts @@ -0,0 +1,96 @@ +import { tl } from '@mtcute/tl' + +import { InputPeerLike } from '../../peers/index.js' + +/** + * Intermediate bot scope, that is converted to + * TL type `BotCommandScope` by the respective functions. + * + * Used to avoid manually resolving peers. + */ +export type IntermediateScope = + | { + type: 'peer' | 'peer_admins' + peer: InputPeerLike + } + | { + type: 'member' + chat: InputPeerLike + user: InputPeerLike + } + +/** + * Default commands scope. + * + * Used if no commands with a narrower scope are available. + */ +export const default_: tl.RawBotCommandScopeDefault = { + _: 'botCommandScopeDefault', +} as const + +/** + * Scope that covers all private chats + */ +export const allPrivate: tl.RawBotCommandScopeUsers = { + _: 'botCommandScopeUsers', +} as const + +/** + * Scope that covers all group chats (both legacy and supergroups) + */ +export const allGroups: tl.RawBotCommandScopeChats = { + _: 'botCommandScopeChats', +} as const + +/** + * Scope that covers all group chat administrators (both legacy and supergroups) + */ +export const allGroupAdmins: tl.RawBotCommandScopeChatAdmins = { + _: 'botCommandScopeChatAdmins', +} as const + +/** + * Scope that covers a specific peer (a single user in PMs, + * or all users of a legacy group or a supergroup) + */ +export function peer(peer: InputPeerLike): IntermediateScope { + return { + type: 'peer', + peer, + } +} + +/** + * Scope that covers admins in a specific group + */ +export function groupAdmins(peer: InputPeerLike): IntermediateScope { + return { + type: 'peer_admins', + peer, + } +} + +/** + * Scope that covers a specific user in a specific group + */ +export function groupMember(chat: InputPeerLike, user: InputPeerLike): IntermediateScope { + return { + type: 'member', + chat, + user, + } +} + +/** + * Helper function to create a bot command object + * + * @param command Bot command (without slash) + * @param description Command description + */ +export function cmd(command: string, description: string): tl.RawBotCommand { + return { + _: 'botCommand', + command, + description, + } +} diff --git a/packages/core/src/highlevel/types/bots/index.ts b/packages/core/src/highlevel/types/bots/index.ts index 3c944903..72c17dce 100644 --- a/packages/core/src/highlevel/types/bots/index.ts +++ b/packages/core/src/highlevel/types/bots/index.ts @@ -1,5 +1,5 @@ -export * from './command-scope.js' +export * from './command-scope/index.js' export * from './game-high-score.js' -export * from './input/index.js' -export * from './keyboard-builder.js' -export * from './keyboards.js' +export * from './inline-message/index.js' +export * from './inline-result/index.js' +export * from './keyboards/index.js' diff --git a/packages/core/src/highlevel/types/bots/inline-message/factories.ts b/packages/core/src/highlevel/types/bots/inline-message/factories.ts new file mode 100644 index 00000000..a12fb671 --- /dev/null +++ b/packages/core/src/highlevel/types/bots/inline-message/factories.ts @@ -0,0 +1,204 @@ +import { tl } from '@mtcute/tl' + +import { assertNever } from '../../../../types/utils.js' +import { ITelegramClient } from '../../../client.types.js' +import { _normalizeInputText } from '../../../methods/misc/normalize-text.js' +import { InputText } from '../../../types/misc/entities.js' +import { InputMediaGeoLive } from '../../media/index.js' +import { BotKeyboard } from '../keyboards/index.js' +import { + InputInlineMessage, + InputInlineMessageContact, + InputInlineMessageGame, + InputInlineMessageGeo, + InputInlineMessageGeoLive, + InputInlineMessageMedia, + InputInlineMessageText, + InputInlineMessageVenue, + InputInlineMessageWebpage, +} from './types.js' + +/** + * Create a text inline message + * + * @param text Message text + * @param params + */ +export function text( + text: InputText, + params: Omit = {}, +): InputInlineMessageText { + const ret = params as tl.Mutable + ret.type = 'text' + ret.text = text + + return ret +} + +/** + * Create an inline message containing + * media from the result + */ +export function media(params: Omit = {}): InputInlineMessageMedia { + const ret = params as tl.Mutable + ret.type = 'media' + + return ret +} + +/** + * Create an inline message containing a geolocation + * + * @param params Additional parameters + */ +export function geo(params: Omit): InputInlineMessageGeo { + const ret = params as tl.Mutable + ret.type = 'geo' + + return ret +} + +/** + * Create an inline message containing a live geolocation + * + * @param params Additional parameters + */ +export function geoLive(params: Omit): InputInlineMessageGeoLive { + const ret = params as tl.Mutable + ret.type = 'geo_live' + + return ret +} + +/** + * Create an inline message containing a venue + */ +export function venue(params: Omit): InputInlineMessageVenue { + const ret = params as tl.Mutable + ret.type = 'venue' + + return ret +} + +/** + * Create an inline message containing a game + * from the inline result + */ +export function game(params: Omit): InputInlineMessageGame { + const ret = params as tl.Mutable + ret.type = 'game' + + return ret +} + +/** + * Create an inline message containing a contact + */ +export function contact(params: Omit): InputInlineMessageContact { + const ret = params as tl.Mutable + ret.type = 'contact' + + return ret +} + +/** + * Create an inline message containing a webpage + */ +export function webpage(params: Omit): InputInlineMessageWebpage { + const ret = params as tl.Mutable + ret.type = 'webpage' + + return ret +} + +/** @internal */ +export async function _convertToTl( + client: ITelegramClient, + obj: InputInlineMessage, +): Promise { + switch (obj.type) { + case 'text': { + const [message, entities] = await _normalizeInputText(client, obj.text) + + return { + _: 'inputBotInlineMessageText', + message, + entities, + replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup), + invertMedia: obj.invertMedia, + } + } + case 'media': { + const [message, entities] = await _normalizeInputText(client, obj.text) + + return { + _: 'inputBotInlineMessageMediaAuto', + message, + entities, + replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup), + invertMedia: obj.invertMedia, + } + } + case 'geo': + case 'geo_live': + return { + _: 'inputBotInlineMessageMediaGeo', + geoPoint: { + _: 'inputGeoPoint', + lat: obj.latitude, + long: obj.longitude, + }, + // fields will be `undefined` if this is a `geo` + heading: (obj as InputMediaGeoLive).heading, + period: (obj as InputMediaGeoLive).period, + proximityNotificationRadius: (obj as InputMediaGeoLive).proximityNotificationRadius, + replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup), + } + case 'venue': + return { + _: 'inputBotInlineMessageMediaVenue', + geoPoint: { + _: 'inputGeoPoint', + lat: obj.latitude, + long: obj.longitude, + }, + title: obj.title, + address: obj.address, + provider: obj.source?.provider ?? '', + venueId: obj.source?.id ?? '', + venueType: obj.source?.type ?? '', + replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup), + } + case 'game': + return { + _: 'inputBotInlineMessageGame', + replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup), + } + case 'contact': + return { + _: 'inputBotInlineMessageMediaContact', + phoneNumber: obj.phone, + firstName: obj.firstName, + lastName: obj.lastName ?? '', + vcard: obj.vcard ?? '', + replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup), + } + case 'webpage': { + const [message, entities] = await _normalizeInputText(client, obj.text) + + return { + _: 'inputBotInlineMessageMediaWebPage', + message, + entities, + replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup), + invertMedia: obj.invertMedia, + forceLargeMedia: obj.size === 'large', + forceSmallMedia: obj.size === 'small', + optional: !obj.required, + url: obj.url, + } + } + default: + assertNever(obj) + } +} diff --git a/packages/core/src/highlevel/types/bots/inline-message/index.ts b/packages/core/src/highlevel/types/bots/inline-message/index.ts new file mode 100644 index 00000000..edfa35a1 --- /dev/null +++ b/packages/core/src/highlevel/types/bots/inline-message/index.ts @@ -0,0 +1,5 @@ +export * from './types.js' + +import * as BotInlineMessage from './factories.js' + +export { BotInlineMessage } diff --git a/packages/core/src/highlevel/types/bots/inline-message/types.ts b/packages/core/src/highlevel/types/bots/inline-message/types.ts new file mode 100644 index 00000000..a2b28e87 --- /dev/null +++ b/packages/core/src/highlevel/types/bots/inline-message/types.ts @@ -0,0 +1,147 @@ +import { + InputMediaContact, + InputMediaGeo, + InputMediaGeoLive, + InputMediaVenue, + InputMediaWebpage, +} from '../../media/index.js' +import { InputText } from '../../misc/entities.js' +import { ReplyMarkup } from '../index.js' + +/** + * Inline message containing only text + */ +export interface InputInlineMessageText { + type: 'text' + + /** + * Text of the message + */ + text: InputText + + /** + * Message reply markup + */ + replyMarkup?: ReplyMarkup + + /** + * Whether to disable links preview in this message + */ + disableWebPreview?: boolean + + /** + * Whether to invert media position. + * + * Currently only supported for web previews and makes the + * client render the preview above the caption and not below. + */ + invertMedia?: boolean +} + +/** + * Inline message containing media, which is automatically + * inferred from the result itself. + */ +export interface InputInlineMessageMedia { + type: 'media' + + /** + * Caption for the media + */ + text?: InputText + + /** + * Message reply markup + */ + replyMarkup?: ReplyMarkup + + /** + * Whether to invert media position. + * + * Currently only supported for web previews and makes the + * client render the preview above the caption and not below. + */ + invertMedia?: boolean +} + +/** + * Inline message containing a geolocation + */ +export interface InputInlineMessageGeo extends InputMediaGeo { + /** + * Message's reply markup + */ + replyMarkup?: ReplyMarkup +} + +/** + * Inline message containing a live geolocation + */ +export interface InputInlineMessageGeoLive extends InputMediaGeoLive { + /** + * Message's reply markup + */ + replyMarkup?: ReplyMarkup +} + +/** + * Inline message containing a venue + */ +export interface InputInlineMessageVenue extends InputMediaVenue { + /** + * Message's reply markup + */ + replyMarkup?: ReplyMarkup +} + +/** + * Inline message containing a game + */ +export interface InputInlineMessageGame { + type: 'game' + + /** + * Message's reply markup + */ + replyMarkup?: ReplyMarkup +} + +/** + * Inline message containing a contact + */ +export interface InputInlineMessageContact extends InputMediaContact { + /** + * Message's reply markup + */ + replyMarkup?: ReplyMarkup +} + +export interface InputInlineMessageWebpage extends InputMediaWebpage { + /** + * Text of the message + */ + text: InputText + + /** + * Message reply markup + */ + replyMarkup?: ReplyMarkup + + /** + * Whether to invert media position. + * + * Currently only supported for web previews and makes the + * client render the preview above the caption and not below. + */ + invertMedia?: boolean +} + +export type InputInlineMessage = + | InputInlineMessageText + | InputInlineMessageMedia + | InputInlineMessageGeo + | InputInlineMessageGeoLive + | InputInlineMessageVenue + | InputInlineMessageGame + | InputInlineMessageContact + | InputInlineMessageWebpage diff --git a/packages/core/src/highlevel/types/bots/inline-result/factories.ts b/packages/core/src/highlevel/types/bots/inline-result/factories.ts new file mode 100644 index 00000000..2745945e --- /dev/null +++ b/packages/core/src/highlevel/types/bots/inline-result/factories.ts @@ -0,0 +1,552 @@ +import { tl } from '@mtcute/tl' + +import { MtArgumentError } from '../../../../types/errors.js' +import { ITelegramClient } from '../../../client.types.js' +import { fileIdToInputDocument, fileIdToInputPhoto } from '../../../utils/convert-file-id.js' +import { extractFileName } from '../../../utils/file-utils.js' +import { BotInlineMessage } from '../inline-message/index.js' +import { + InputInlineResult, + InputInlineResultArticle, + InputInlineResultAudio, + InputInlineResultContact, + InputInlineResultFile, + InputInlineResultGame, + InputInlineResultGeo, + InputInlineResultGif, + InputInlineResultPhoto, + InputInlineResultSticker, + InputInlineResultVenue, + InputInlineResultVideo, + InputInlineResultVoice, +} from './types.js' + +/** + * Create an inline result containing an article + * + * @param id Inline result ID + * @param params Article + */ +export function article(id: string, params: Omit): InputInlineResultArticle { + const ret = params as tl.Mutable + ret.id = id + ret.type = 'article' + + return ret +} + +/** + * Create an inline result containing a GIF + * + * @param id Inline result ID + * @param media GIF animation + * @param params Additional parameters + */ +export function gif( + id: string, + media: string | tl.RawInputWebDocument | tl.RawInputDocument, + params: Omit = {}, +): InputInlineResultGif { + const ret = params as tl.Mutable + ret.id = id + ret.type = 'gif' + ret.media = media + + return ret +} + +/** + * Create an inline result containing a video + * + * @param id Inline result ID + * @param media Video + * @param params Additional parameters + */ +export function video( + id: string, + media: string | tl.RawInputWebDocument | tl.RawInputDocument, + params: Omit, +): InputInlineResultVideo { + const ret = params as tl.Mutable + ret.id = id + ret.type = 'video' + ret.media = media + + return ret +} + +/** + * Create an inline result containing an audio file + * + * @param id Inline result ID + * @param media Audio file + * @param params Additional parameters + */ +export function audio( + id: string, + media: string | tl.RawInputWebDocument | tl.RawInputDocument, + params: Omit, +): InputInlineResultAudio { + const ret = params as tl.Mutable + ret.id = id + ret.type = 'audio' + ret.media = media + + return ret +} + +/** + * Create an inline result containing a voice note + * + * @param id Inline result ID + * @param media Voice note + * @param params Additional parameters + */ +export function voice( + id: string, + media: string | tl.RawInputWebDocument | tl.RawInputDocument, + params: Omit, +): InputInlineResultVoice { + const ret = params as tl.Mutable + ret.id = id + ret.type = 'voice' + ret.media = media + + return ret +} + +/** + * Create an inline result containing a photo + * + * @param id Inline result ID + * @param media Photo + * @param params Additional parameters + */ +export function photo( + id: string, + media: string | tl.RawInputWebDocument | tl.RawInputPhoto, + params: Omit = {}, +): InputInlineResultPhoto { + const ret = params as tl.Mutable + ret.id = id + ret.type = 'photo' + ret.media = media + + return ret +} + +/** + * Create an inline result containing a sticker + * + * @param id Inline result ID + * @param media Sticker + */ +export function sticker(id: string, media: string | tl.RawInputDocument): InputInlineResultSticker { + return { + id, + type: 'sticker', + media, + } +} + +/** + * Create an inline result containing a document + * (only PDF and ZIP are supported when using URL) + * + * @param id Inline result ID + * @param media Document + * @param params Additional parameters + */ +export function file( + id: string, + media: string | tl.RawInputWebDocument | tl.RawInputDocument, + params: Omit, +): InputInlineResultFile { + const ret = params as tl.Mutable + ret.id = id + ret.type = 'file' + ret.media = media + + return ret +} + +/** + * Create an inline result containing a geolocation + * + * @param id Inline result ID + * @param params Additional parameters + */ +export function geo(id: string, params: Omit): InputInlineResultGeo { + const ret = params as tl.Mutable + ret.id = id + ret.type = 'geo' + + return ret +} + +/** + * Create an inline result containing a venue + * + * @param id Inline result ID + * @param params Venue parameters + */ +export function venue(id: string, params: Omit): InputInlineResultVenue { + const ret = params as tl.Mutable + ret.id = id + ret.type = 'venue' + + return ret +} + +/** + * Create an inline result containing a contact + * + * @param id Inline result ID + * @param params Contact parameters + */ +export function contact(id: string, params: Omit): InputInlineResultContact { + const ret = params as tl.Mutable + ret.id = id + ret.type = 'contact' + + return ret +} + +/** + * Create an inline result containing a game + * + * @param id Inline result ID + * @param shortName Short name of the game + * @param params Additional parameters + */ +export function game( + id: string, + shortName: string, + params: Omit = {}, +): InputInlineResultGame { + const ret = params as tl.Mutable + ret.id = id + ret.type = 'game' + ret.shortName = shortName + + return ret +} + +/** @internal */ +export async function _convertToTl( + client: ITelegramClient, + results: InputInlineResult[], +): Promise<[boolean, tl.TypeInputBotInlineResult[]]> { + const normalizeThumb = (obj: InputInlineResult, fallback?: string): tl.RawInputWebDocument | undefined => { + if (obj.type !== 'voice' && obj.type !== 'audio' && obj.type !== 'sticker' && obj.type !== 'game') { + if (!obj.thumb || typeof obj.thumb === 'string') { + if (!obj.thumb && !fallback) { + return undefined + } + + return { + _: 'inputWebDocument', + size: 0, + url: obj.thumb || fallback!, + mimeType: obj.type === 'gif' ? obj.thumbMime ?? obj.mime ?? 'video/mp4' : 'image/jpeg', + attributes: [], + } + } + + return obj.thumb + } + } + + const items: tl.TypeInputBotInlineResult[] = [] + + let isGallery = false + let forceVertical = false + + for (const obj of results) { + switch (obj.type) { + case 'article': { + forceVertical = true + + let sendMessage: tl.TypeInputBotInlineMessage + + if (obj.message) { + sendMessage = await BotInlineMessage._convertToTl(client, obj.message) + } 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, + } + } + + items.push({ + _: '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' ? normalizeThumb(obj) : obj.thumb, + sendMessage, + }) + continue + } + case 'game': { + let sendMessage: tl.TypeInputBotInlineMessage + + if (obj.message) { + sendMessage = await BotInlineMessage._convertToTl(client, obj.message) + + if (sendMessage._ !== 'inputBotInlineMessageGame') { + throw new MtArgumentError('game inline result must contain a game inline message') + } + } else { + sendMessage = { + _: 'inputBotInlineMessageGame', + } + } + + items.push({ + _: 'inputBotInlineResultGame', + id: obj.id, + shortName: obj.shortName, + sendMessage, + }) + continue + } + case 'gif': + case 'photo': + case 'sticker': + isGallery = true + break + case 'audio': + case 'contact': + case 'voice': + forceVertical = true + } + + let sendMessage: tl.TypeInputBotInlineMessage + + if (obj.message) { + sendMessage = await BotInlineMessage._convertToTl(client, obj.message) + } else if (obj.type === 'venue') { + if (obj.latitude && obj.longitude) { + sendMessage = { + _: 'inputBotInlineMessageMediaVenue', + title: obj.title, + address: obj.address, + geoPoint: { + _: 'inputGeoPoint', + lat: obj.latitude, + long: obj.longitude, + }, + provider: '', + venueId: '', + venueType: '', + } + } else { + throw new MtArgumentError('message or location (lat&lon) bust be supplied for venue inline result') + } + } else if (obj.type === 'video' && obj.isEmbed && typeof obj.media === 'string') { + sendMessage = { + _: 'inputBotInlineMessageText', + message: obj.media, + } + } else if (obj.type === 'geo') { + sendMessage = { + _: 'inputBotInlineMessageMediaGeo', + geoPoint: { + _: 'inputGeoPoint', + lat: obj.latitude, + long: obj.longitude, + }, + } + } else if (obj.type === 'contact') { + sendMessage = { + _: 'inputBotInlineMessageMediaContact', + phoneNumber: obj.phone, + firstName: obj.firstName, + lastName: obj.lastName ?? '', + vcard: '', + } + } else { + sendMessage = { + _: 'inputBotInlineMessageMediaAuto', + message: '', + } + } + + let media: tl.TypeInputWebDocument | tl.TypeInputDocument | tl.TypeInputPhoto | undefined = undefined + + if (obj.type !== 'geo' && obj.type !== 'venue' && obj.type !== 'contact') { + if (typeof obj.media === 'string') { + // file id or url + if (obj.media.match(/^https?:\/\//)) { + if (obj.type === 'sticker') { + throw new MtArgumentError('sticker inline result cannot contain a URL') + } + + let mime: string + if (obj.type === 'video') mime = 'video/mp4' + else if (obj.type === 'audio') { + mime = obj.mime ?? 'audio/mpeg' + } else if (obj.type === 'gif') { + mime = obj.mime ?? 'video/mp4' + } else if (obj.type === 'voice') mime = 'audio/ogg' + else if (obj.type === 'file') { + if (!obj.mime) { + throw new MtArgumentError('MIME type must be specified for file inline result') + } + + mime = obj.mime + } else mime = 'image/jpeg' + + const attributes: tl.TypeDocumentAttribute[] = [] + + if ( + (obj.type === 'video' || obj.type === 'gif' || obj.type === 'photo') && + obj.width && + obj.height + ) { + if (obj.type !== 'photo' && obj.duration) { + attributes.push({ + _: 'documentAttributeVideo', + w: obj.width, + h: obj.height, + duration: obj.duration, + }) + } else { + attributes.push({ + _: 'documentAttributeImageSize', + w: obj.width, + h: obj.height, + }) + } + } else if (obj.type === 'audio' || obj.type === 'voice') { + attributes.push({ + _: 'documentAttributeAudio', + voice: obj.type === 'voice', + duration: obj.duration ?? 0, + title: obj.type === 'audio' ? obj.title : '', + performer: obj.type === 'audio' ? obj.performer : '', + }) + } + + attributes.push({ + _: 'documentAttributeFilename', + fileName: extractFileName(obj.media), + }) + + media = { + _: 'inputWebDocument', + url: obj.media, + mimeType: mime, + size: 0, + attributes, + } + } else if (obj.type === 'photo') { + media = fileIdToInputPhoto(obj.media) + } else { + media = fileIdToInputDocument(obj.media) + } + } else { + media = obj.media + } + } + + let title: string | undefined = undefined + let description: string | undefined = undefined + + // incredible hacks by durov team. + // i honestly don't understand why didn't they just + // make a bunch of types, as they normally do, + // but whatever. + // ref: https://github.com/tdlib/td/blob/master/td/telegram/InlineQueriesManager.cpp + if (obj.type === 'contact') { + title = obj.lastName?.length ? `${obj.firstName} ${obj.lastName}` : obj.firstName + } else if (obj.type !== 'sticker') { + title = obj.title + } + + if (obj.type === 'audio') { + description = obj.performer + } else if (obj.type === 'geo') { + description = `${obj.latitude} ${obj.longitude}` + } else if (obj.type === 'venue') { + description = obj.address + } else if (obj.type === 'contact') { + description = obj.phone + } else if (obj.type !== 'voice' && obj.type !== 'sticker') { + description = obj.description + } + + if (!media || media._ === 'inputWebDocument') { + items.push({ + _: 'inputBotInlineResult', + id: obj.id, + type: obj.type, + title, + description, + content: media, + thumb: normalizeThumb(obj, media?.url), + sendMessage, + }) + continue + } + + if (media._ === 'inputPhoto') { + items.push({ + _: 'inputBotInlineResultPhoto', + id: obj.id, + type: obj.type, + photo: media, + sendMessage, + }) + continue + } + + items.push({ + _: 'inputBotInlineResultDocument', + id: obj.id, + type: obj.type, + title, + description, + document: media, + sendMessage, + }) + } + + return [isGallery && !forceVertical, items] +} diff --git a/packages/core/src/highlevel/types/bots/inline-result/index.ts b/packages/core/src/highlevel/types/bots/inline-result/index.ts new file mode 100644 index 00000000..e73728f6 --- /dev/null +++ b/packages/core/src/highlevel/types/bots/inline-result/index.ts @@ -0,0 +1,4 @@ +import * as BotInline from './factories.js' + +export * from './types.js' +export { BotInline } diff --git a/packages/core/src/highlevel/types/bots/inline-result/types.ts b/packages/core/src/highlevel/types/bots/inline-result/types.ts new file mode 100644 index 00000000..f5cc2a80 --- /dev/null +++ b/packages/core/src/highlevel/types/bots/inline-result/types.ts @@ -0,0 +1,501 @@ +import { tl } from '@mtcute/tl' + +import { InputInlineMessage } from '../inline-message/types.js' + +export 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 +} + +/** + * Inline result containing an article. + * + * If `message` is not provided, a {@link InputInlineMessageText} is created + * with web preview enabled and text generated as follows: + * ``` + * {{#if url}} + * {{title}} + * {{else}} + * {{title}} + * {{/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 + * + * @default `false` + */ + hideUrl?: boolean + + /** + * Article thumbnail URL (must be jpeg). + */ + thumb?: string | tl.RawInputWebDocument +} + +/** + * Inline result containing an animation (silent mp4 or gif). + * + * If `message` is not provided, {@link InputInlineMessageMedia} is used + * with empty caption + */ +export interface InputInlineResultGif extends BaseInputInlineResult { + type: 'gif' + + /** + * The animation itself. + * + * Can be a URL, a TDLib and Bot API compatible File ID, + * or a TL object representing either of them. + */ + media: string | tl.RawInputWebDocument | tl.RawInputDocument + + /** + * Media MIME type, only applicable to URLs. + * + * Usually unnecessary, since Telegram automatically infers it. + * + * @default `video/mp4` + */ + mime?: string + + /** + * Title of the result + */ + title?: string + + /** + * Title of the result + */ + description?: string + + /** + * Animation thumbnail URL, only applicable in case `media` is a URL + * + * @default `media` + */ + thumb?: string | tl.RawInputWebDocument + + /** + * Thumbnail MIME type + * + * @default `image/jpeg` + */ + thumbMime?: string + + /** + * Width of the animation in pixels + */ + width?: number + + /** + * Height of the animation in pixels + */ + height?: number + + /** + * Duration of the animation in seconds + */ + duration?: number +} + +/** + * Inline result containing a video (only MP4) + * + * If `message` is not provided, {@link InputInlineMessageMedia} is used + * with empty caption for non-embed videos, {@link InputInlineMessageText} + * is used with text containing the URL for embed videos. + */ +export interface InputInlineResultVideo extends BaseInputInlineResult { + type: 'video' + + /** + * The video itself, or a page containing an embedded video + * + * Can be a URL, a TDLib and Bot API compatible File ID, + * or a TL object representing either of them. + */ + media: string | tl.RawInputWebDocument | tl.RawInputDocument + + /** + * In case `media` is a URL, whether that URL is a link + * to an embedded video player. + */ + isEmbed?: boolean + + /** + * Title of the result + */ + title: string + + /** + * Description of the result + */ + description?: string + + /** + * Video thumbnail URL (must be jpeg), only applicable in case `media` is a URL. + * + * Must be provided explicitly if this is a video loaded by URL. + * + * @default `media` + */ + thumb?: string | tl.RawInputWebDocument + + /** + * Width of the video in pixels + */ + width?: number + + /** + * Height of the video in pixels + */ + height?: number + + /** + * Duration of the video in seconds + */ + duration?: number +} + +/** + * Inline result containing an audio file + * + * If `message` is not provided, {@link InputInlineMessageMedia} is used + * with empty caption. + */ +export interface InputInlineResultAudio extends BaseInputInlineResult { + type: 'audio' + + /** + * The audio itself + * + * Can be a URL, a TDLib and Bot API compatible File ID, + * or a TL object representing either of them. + */ + media: string | tl.RawInputWebDocument | tl.RawInputDocument + + /** + * MIME type of the audio file + * + * Usually unnecessary, since Telegram infers it automatically. + * + * @default `audio/mpeg` + */ + mime?: string + + /** + * Title of the audio track + */ + title: string + + /** + * Performer of the audio track + */ + performer?: string + + /** + * Duration of the audio in seconds + */ + duration?: number +} + +/** + * Inline result containing a voice note + * + * If `message` is not provided, {@link InputInlineMessageMedia} is used + * with empty caption. + */ +export interface InputInlineResultVoice extends BaseInputInlineResult { + type: 'voice' + + /** + * The voice itself (.ogg, preferably encoded with OPUS) + * + * Can be a URL, a TDLib and Bot API compatible File ID, + * or a TL object representing either of them. + */ + media: string | tl.RawInputWebDocument | tl.RawInputDocument + + /** + * Title of the result + */ + title: string + + /** + * Duration of the voice note in seconds + */ + duration?: number +} + +/** + * Inline result containing a photo + * + * If `message` is not provided, {@link InputInlineMessageMedia} is used + * with empty caption. + */ +export interface InputInlineResultPhoto extends BaseInputInlineResult { + type: 'photo' + + /** + * The photo itself + * + * Can be a URL, a TDLib and Bot API compatible File ID, + * or a TL object representing either of them. + */ + media: string | tl.RawInputWebDocument | tl.RawInputPhoto + + /** + * Title of the result + */ + title?: string + + /** + * Description of the result + */ + description?: string + + /** + * Width of the photo in pixels + */ + width?: number + + /** + * Height of the photo in pixels + */ + height?: number + + /** + * Photo thumbnail URL (must be jpeg), only applicable in case `media` is a URL + * + * @default `media` + */ + thumb?: string | tl.RawInputWebDocument +} + +/** + * Inline result containing a sticker + * + * If `message` is not provided, {@link InputInlineMessageMedia} is used. + */ +export interface InputInlineResultSticker extends BaseInputInlineResult { + type: 'sticker' + + /** + * The sticker itself. Can't be a URL. + */ + media: string | tl.RawInputDocument +} + +/** + * Inline result containing a document + * + * If `message` is not provided, {@link InputInlineMessageMedia} is used + * with empty caption. + */ +export interface InputInlineResultFile extends BaseInputInlineResult { + type: 'file' + + /** + * The file itself. When using URL, only PDF and ZIP are supported. + * + * Can be a URL, a TDLib and Bot API compatible File ID, + * or a TL object representing either of them. + */ + media: string | tl.RawInputWebDocument | tl.RawInputDocument + + /** + * MIME type of the file. + * + * Due to some Telegram limitation, you can only send + * PDF and ZIP files from URL + * (`application/pdf` and `application/zip` MIMEs respectively). + * + * Must be provided if `media` is a URL + */ + mime?: string + + /** + * Title of the result + */ + title: string + + /** + * Description of the result + */ + description?: string + + /** + * Photo thumbnail URL (must be jpeg), only applicable in case `media` is a URL + * + * @default `media` + */ + thumb?: string | tl.RawInputWebDocument +} + +/** + * Inline result containing a geolocation. + * + * If `message` is not passed, a {@link InputInlineMessageGeo} is + * used, with the `latitude` and `longitude` parameters set + * accordingly + */ +export interface InputInlineResultGeo extends BaseInputInlineResult { + type: 'geo' + + /** + * Title of the result + */ + title: string + + /** + * Latitude of the geolocation + */ + latitude: number + + /** + * Longitude of the geolocation + */ + longitude: number + + /** + * Location thumbnail URL (must be jpeg). + * + * By default, Telegram generates one based on + * the location set by `latitude` and `longitude` + */ + thumb?: string | tl.RawInputWebDocument +} + +/** + * Inline result containing a venue. + * + * If `message` is not passed, {@link BotInlineMessage.venue} is used with + * given `latitude` and `longitude` were passed. + * If they weren't passed either, an error is thrown. + */ +export interface InputInlineResultVenue extends BaseInputInlineResult { + type: 'venue' + + /** + * Title of the venue + */ + title: string + + /** + * Address of the venue + */ + address: string + + /** + * Latitude of the geolocation + */ + latitude?: number + + /** + * Longitude of the geolocation + */ + longitude?: number + + /** + * Venue thumbnail URL (must be jpeg). + * + * By default, Telegram generates one based on + * the location in the `message` + */ + thumb?: string | tl.RawInputWebDocument +} + +/** + * Inline result containing a game. + * + * If `message` is not passed, {@link InputInlineMessageGame} is used. + * + * Note that `message` can only be {@link InputInlineMessageGame} + */ +export interface InputInlineResultGame extends BaseInputInlineResult { + type: 'game' + + /** + * Short name of the game + */ + shortName: string +} + +/** + * Inline result containing a contact. + * + * If `message` is not passed, {@link InputInlineMessageContact} is used. + */ +export interface InputInlineResultContact extends BaseInputInlineResult { + type: 'contact' + + /** + * First name of the contact + */ + firstName: string + + /** + * Last name of the contact + */ + lastName?: string + + /** + * Phone number of the contact + */ + phone: string + + /** + * Contact thumbnail URL (i.e. their avatar) (must be jpeg) + */ + thumb?: string | tl.RawInputWebDocument +} + +export type InputInlineResult = + | InputInlineResultArticle + | InputInlineResultGif + | InputInlineResultVideo + | InputInlineResultAudio + | InputInlineResultVoice + | InputInlineResultPhoto + | InputInlineResultSticker + | InputInlineResultFile + | InputInlineResultGeo + | InputInlineResultVenue + | InputInlineResultGame + | InputInlineResultContact diff --git a/packages/core/src/highlevel/types/bots/input/index.ts b/packages/core/src/highlevel/types/bots/input/index.ts deleted file mode 100644 index a36c1b8f..00000000 --- a/packages/core/src/highlevel/types/bots/input/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './input-inline-message.js' -export * from './input-inline-result.js' diff --git a/packages/core/src/highlevel/types/bots/input/input-inline-message.ts b/packages/core/src/highlevel/types/bots/input/input-inline-message.ts deleted file mode 100644 index e60d7128..00000000 --- a/packages/core/src/highlevel/types/bots/input/input-inline-message.ts +++ /dev/null @@ -1,340 +0,0 @@ -import { tl } from '@mtcute/tl' - -import { assertNever } from '../../../../types/utils.js' -import { ITelegramClient } from '../../../client.types.js' -import { _normalizeInputText } from '../../../methods/misc/normalize-text.js' -import { InputText } from '../../../types/misc/entities.js' -import { - InputMediaContact, - InputMediaGeo, - InputMediaGeoLive, - InputMediaVenue, - InputMediaWebpage, -} from '../../media/index.js' -import { BotKeyboard, ReplyMarkup } from '../keyboards.js' - -/** - * Inline message containing only text - */ -export interface InputInlineMessageText { - type: 'text' - - /** - * Text of the message - */ - text: InputText - - /** - * Message reply markup - */ - replyMarkup?: ReplyMarkup - - /** - * Whether to disable links preview in this message - */ - disableWebPreview?: boolean - - /** - * Whether to invert media position. - * - * Currently only supported for web previews and makes the - * client render the preview above the caption and not below. - */ - invertMedia?: boolean -} - -/** - * Inline message containing media, which is automatically - * inferred from the result itself. - */ -export interface InputInlineMessageMedia { - type: 'media' - - /** - * Caption for the media - */ - text?: InputText - - /** - * Message reply markup - */ - replyMarkup?: ReplyMarkup - - /** - * Whether to invert media position. - * - * Currently only supported for web previews and makes the - * client render the preview above the caption and not below. - */ - invertMedia?: boolean -} - -/** - * Inline message containing a geolocation - */ -export interface InputInlineMessageGeo extends InputMediaGeo { - /** - * Message's reply markup - */ - replyMarkup?: ReplyMarkup -} - -/** - * Inline message containing a live geolocation - */ -export interface InputInlineMessageGeoLive extends InputMediaGeoLive { - /** - * Message's reply markup - */ - replyMarkup?: ReplyMarkup -} - -/** - * Inline message containing a venue - */ -export interface InputInlineMessageVenue extends InputMediaVenue { - /** - * Message's reply markup - */ - replyMarkup?: ReplyMarkup -} - -/** - * Inline message containing a game - */ -export interface InputInlineMessageGame { - type: 'game' - - /** - * Message's reply markup - */ - replyMarkup?: ReplyMarkup -} - -/** - * Inline message containing a contact - */ -export interface InputInlineMessageContact extends InputMediaContact { - /** - * Message's reply markup - */ - replyMarkup?: ReplyMarkup -} - -export interface InputInlineMessageWebpage extends InputMediaWebpage { - /** - * Text of the message - */ - text: InputText - - /** - * Message reply markup - */ - replyMarkup?: ReplyMarkup - - /** - * Whether to invert media position. - * - * Currently only supported for web previews and makes the - * client render the preview above the caption and not below. - */ - invertMedia?: boolean -} - -export type InputInlineMessage = - | InputInlineMessageText - | InputInlineMessageMedia - | InputInlineMessageGeo - | InputInlineMessageGeoLive - | InputInlineMessageVenue - | InputInlineMessageGame - | InputInlineMessageContact - | InputInlineMessageWebpage - -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace BotInlineMessage { - /** - * Create a text inline message - * - * @param text Message text - * @param params - */ - export function text( - text: InputText, - params: Omit = {}, - ): InputInlineMessageText { - const ret = params as tl.Mutable - ret.type = 'text' - ret.text = text - - return ret - } - - /** - * Create an inline message containing - * media from the result - */ - export function media(params: Omit = {}): InputInlineMessageMedia { - const ret = params as tl.Mutable - ret.type = 'media' - - return ret - } - - /** - * Create an inline message containing a geolocation - * - * @param params Additional parameters - */ - export function geo(params: Omit): InputInlineMessageGeo { - const ret = params as tl.Mutable - ret.type = 'geo' - - return ret - } - - /** - * Create an inline message containing a live geolocation - * - * @param params Additional parameters - */ - export function geoLive(params: Omit): InputInlineMessageGeoLive { - const ret = params as tl.Mutable - ret.type = 'geo_live' - - return ret - } - - /** - * Create an inline message containing a venue - */ - export function venue(params: Omit): InputInlineMessageVenue { - const ret = params as tl.Mutable - ret.type = 'venue' - - return ret - } - - /** - * Create an inline message containing a game - * from the inline result - */ - export function game(params: Omit): InputInlineMessageGame { - const ret = params as tl.Mutable - ret.type = 'game' - - return ret - } - - /** - * Create an inline message containing a contact - */ - export function contact(params: Omit): InputInlineMessageContact { - const ret = params as tl.Mutable - ret.type = 'contact' - - return ret - } - - /** - * Create an inline message containing a webpage - */ - export function webpage(params: Omit): InputInlineMessageWebpage { - const ret = params as tl.Mutable - ret.type = 'webpage' - - return ret - } - - /** @internal */ - export async function _convertToTl( - client: ITelegramClient, - obj: InputInlineMessage, - ): Promise { - switch (obj.type) { - case 'text': { - const [message, entities] = await _normalizeInputText(client, obj.text) - - return { - _: 'inputBotInlineMessageText', - message, - entities, - replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup), - invertMedia: obj.invertMedia, - } - } - case 'media': { - const [message, entities] = await _normalizeInputText(client, obj.text) - - return { - _: 'inputBotInlineMessageMediaAuto', - message, - entities, - replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup), - invertMedia: obj.invertMedia, - } - } - case 'geo': - case 'geo_live': - return { - _: 'inputBotInlineMessageMediaGeo', - geoPoint: { - _: 'inputGeoPoint', - lat: obj.latitude, - long: obj.longitude, - }, - // fields will be `undefined` if this is a `geo` - heading: (obj as InputMediaGeoLive).heading, - period: (obj as InputMediaGeoLive).period, - proximityNotificationRadius: (obj as InputMediaGeoLive).proximityNotificationRadius, - replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup), - } - case 'venue': - return { - _: 'inputBotInlineMessageMediaVenue', - geoPoint: { - _: 'inputGeoPoint', - lat: obj.latitude, - long: obj.longitude, - }, - title: obj.title, - address: obj.address, - provider: obj.source?.provider ?? '', - venueId: obj.source?.id ?? '', - venueType: obj.source?.type ?? '', - replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup), - } - case 'game': - return { - _: 'inputBotInlineMessageGame', - replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup), - } - case 'contact': - return { - _: 'inputBotInlineMessageMediaContact', - phoneNumber: obj.phone, - firstName: obj.firstName, - lastName: obj.lastName ?? '', - vcard: obj.vcard ?? '', - replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup), - } - case 'webpage': { - const [message, entities] = await _normalizeInputText(client, obj.text) - - return { - _: 'inputBotInlineMessageMediaWebPage', - message, - entities, - replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup), - invertMedia: obj.invertMedia, - forceLargeMedia: obj.size === 'large', - forceSmallMedia: obj.size === 'small', - optional: !obj.required, - url: obj.url, - } - } - default: - assertNever(obj) - } - } -} diff --git a/packages/core/src/highlevel/types/bots/input/input-inline-result.ts b/packages/core/src/highlevel/types/bots/input/input-inline-result.ts deleted file mode 100644 index 023dcc40..00000000 --- a/packages/core/src/highlevel/types/bots/input/input-inline-result.ts +++ /dev/null @@ -1,1044 +0,0 @@ -import { tl } from '@mtcute/tl' - -import { MtArgumentError } from '../../../../types/errors.js' -import { ITelegramClient } from '../../../client.types.js' -import { fileIdToInputDocument, fileIdToInputPhoto } from '../../../utils/convert-file-id.js' -import { extractFileName } from '../../../utils/file-utils.js' -import { BotInlineMessage, InputInlineMessage } from './input-inline-message.js' - -export 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 -} - -/** - * Inline result containing an article. - * - * If `message` is not provided, a {@link InputInlineMessageText} is created - * with web preview enabled and text generated as follows: - * ``` - * {{#if url}} - * {{title}} - * {{else}} - * {{title}} - * {{/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 - * - * @default `false` - */ - hideUrl?: boolean - - /** - * Article thumbnail URL (must be jpeg). - */ - thumb?: string | tl.RawInputWebDocument -} - -/** - * Inline result containing an animation (silent mp4 or gif). - * - * If `message` is not provided, {@link InputInlineMessageMedia} is used - * with empty caption - */ -export interface InputInlineResultGif extends BaseInputInlineResult { - type: 'gif' - - /** - * The animation itself. - * - * Can be a URL, a TDLib and Bot API compatible File ID, - * or a TL object representing either of them. - */ - media: string | tl.RawInputWebDocument | tl.RawInputDocument - - /** - * Media MIME type, only applicable to URLs. - * - * Usually unnecessary, since Telegram automatically infers it. - * - * @default `video/mp4` - */ - mime?: string - - /** - * Title of the result - */ - title?: string - - /** - * Title of the result - */ - description?: string - - /** - * Animation thumbnail URL, only applicable in case `media` is a URL - * - * @default `media` - */ - thumb?: string | tl.RawInputWebDocument - - /** - * Thumbnail MIME type - * - * @default `image/jpeg` - */ - thumbMime?: string - - /** - * Width of the animation in pixels - */ - width?: number - - /** - * Height of the animation in pixels - */ - height?: number - - /** - * Duration of the animation in seconds - */ - duration?: number -} - -/** - * Inline result containing a video (only MP4) - * - * If `message` is not provided, {@link InputInlineMessageMedia} is used - * with empty caption for non-embed videos, {@link InputInlineMessageText} - * is used with text containing the URL for embed videos. - */ -export interface InputInlineResultVideo extends BaseInputInlineResult { - type: 'video' - - /** - * The video itself, or a page containing an embedded video - * - * Can be a URL, a TDLib and Bot API compatible File ID, - * or a TL object representing either of them. - */ - media: string | tl.RawInputWebDocument | tl.RawInputDocument - - /** - * In case `media` is a URL, whether that URL is a link - * to an embedded video player. - */ - isEmbed?: boolean - - /** - * Title of the result - */ - title: string - - /** - * Description of the result - */ - description?: string - - /** - * Video thumbnail URL (must be jpeg), only applicable in case `media` is a URL. - * - * Must be provided explicitly if this is a video loaded by URL. - * - * @default `media` - */ - thumb?: string | tl.RawInputWebDocument - - /** - * Width of the video in pixels - */ - width?: number - - /** - * Height of the video in pixels - */ - height?: number - - /** - * Duration of the video in seconds - */ - duration?: number -} - -/** - * Inline result containing an audio file - * - * If `message` is not provided, {@link InputInlineMessageMedia} is used - * with empty caption. - */ -export interface InputInlineResultAudio extends BaseInputInlineResult { - type: 'audio' - - /** - * The audio itself - * - * Can be a URL, a TDLib and Bot API compatible File ID, - * or a TL object representing either of them. - */ - media: string | tl.RawInputWebDocument | tl.RawInputDocument - - /** - * MIME type of the audio file - * - * Usually unnecessary, since Telegram infers it automatically. - * - * @default `audio/mpeg` - */ - mime?: string - - /** - * Title of the audio track - */ - title: string - - /** - * Performer of the audio track - */ - performer?: string - - /** - * Duration of the audio in seconds - */ - duration?: number -} - -/** - * Inline result containing a voice note - * - * If `message` is not provided, {@link InputInlineMessageMedia} is used - * with empty caption. - */ -export interface InputInlineResultVoice extends BaseInputInlineResult { - type: 'voice' - - /** - * The voice itself (.ogg, preferably encoded with OPUS) - * - * Can be a URL, a TDLib and Bot API compatible File ID, - * or a TL object representing either of them. - */ - media: string | tl.RawInputWebDocument | tl.RawInputDocument - - /** - * Title of the result - */ - title: string - - /** - * Duration of the voice note in seconds - */ - duration?: number -} - -/** - * Inline result containing a photo - * - * If `message` is not provided, {@link InputInlineMessageMedia} is used - * with empty caption. - */ -export interface InputInlineResultPhoto extends BaseInputInlineResult { - type: 'photo' - - /** - * The photo itself - * - * Can be a URL, a TDLib and Bot API compatible File ID, - * or a TL object representing either of them. - */ - media: string | tl.RawInputWebDocument | tl.RawInputPhoto - - /** - * Title of the result - */ - title?: string - - /** - * Description of the result - */ - description?: string - - /** - * Width of the photo in pixels - */ - width?: number - - /** - * Height of the photo in pixels - */ - height?: number - - /** - * Photo thumbnail URL (must be jpeg), only applicable in case `media` is a URL - * - * @default `media` - */ - thumb?: string | tl.RawInputWebDocument -} - -/** - * Inline result containing a sticker - * - * If `message` is not provided, {@link InputInlineMessageMedia} is used. - */ -export interface InputInlineResultSticker extends BaseInputInlineResult { - type: 'sticker' - - /** - * The sticker itself. Can't be a URL. - */ - media: string | tl.RawInputDocument -} - -/** - * Inline result containing a document - * - * If `message` is not provided, {@link InputInlineMessageMedia} is used - * with empty caption. - */ -export interface InputInlineResultFile extends BaseInputInlineResult { - type: 'file' - - /** - * The file itself. When using URL, only PDF and ZIP are supported. - * - * Can be a URL, a TDLib and Bot API compatible File ID, - * or a TL object representing either of them. - */ - media: string | tl.RawInputWebDocument | tl.RawInputDocument - - /** - * MIME type of the file. - * - * Due to some Telegram limitation, you can only send - * PDF and ZIP files from URL - * (`application/pdf` and `application/zip` MIMEs respectively). - * - * Must be provided if `media` is a URL - */ - mime?: string - - /** - * Title of the result - */ - title: string - - /** - * Description of the result - */ - description?: string - - /** - * Photo thumbnail URL (must be jpeg), only applicable in case `media` is a URL - * - * @default `media` - */ - thumb?: string | tl.RawInputWebDocument -} - -/** - * Inline result containing a geolocation. - * - * If `message` is not passed, a {@link InputInlineMessageGeo} is - * used, with the `latitude` and `longitude` parameters set - * accordingly - */ -export interface InputInlineResultGeo extends BaseInputInlineResult { - type: 'geo' - - /** - * Title of the result - */ - title: string - - /** - * Latitude of the geolocation - */ - latitude: number - - /** - * Longitude of the geolocation - */ - longitude: number - - /** - * Location thumbnail URL (must be jpeg). - * - * By default, Telegram generates one based on - * the location set by `latitude` and `longitude` - */ - thumb?: string | tl.RawInputWebDocument -} - -/** - * Inline result containing a venue. - * - * If `message` is not passed, {@link BotInlineMessage.venue} is used with - * given `latitude` and `longitude` were passed. - * If they weren't passed either, an error is thrown. - */ -export interface InputInlineResultVenue extends BaseInputInlineResult { - type: 'venue' - - /** - * Title of the venue - */ - title: string - - /** - * Address of the venue - */ - address: string - - /** - * Latitude of the geolocation - */ - latitude?: number - - /** - * Longitude of the geolocation - */ - longitude?: number - - /** - * Venue thumbnail URL (must be jpeg). - * - * By default, Telegram generates one based on - * the location in the `message` - */ - thumb?: string | tl.RawInputWebDocument -} - -/** - * Inline result containing a game. - * - * If `message` is not passed, {@link InputInlineMessageGame} is used. - * - * Note that `message` can only be {@link InputInlineMessageGame} - */ -export interface InputInlineResultGame extends BaseInputInlineResult { - type: 'game' - - /** - * Short name of the game - */ - shortName: string -} - -/** - * Inline result containing a contact. - * - * If `message` is not passed, {@link InputInlineMessageContact} is used. - */ -export interface InputInlineResultContact extends BaseInputInlineResult { - type: 'contact' - - /** - * First name of the contact - */ - firstName: string - - /** - * Last name of the contact - */ - lastName?: string - - /** - * Phone number of the contact - */ - phone: string - - /** - * Contact thumbnail URL (i.e. their avatar) (must be jpeg) - */ - thumb?: string | tl.RawInputWebDocument -} - -export type InputInlineResult = - | InputInlineResultArticle - | InputInlineResultGif - | InputInlineResultVideo - | InputInlineResultAudio - | InputInlineResultVoice - | InputInlineResultPhoto - | InputInlineResultSticker - | InputInlineResultFile - | InputInlineResultGeo - | InputInlineResultVenue - | InputInlineResultGame - | InputInlineResultContact - -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace BotInline { - /** - * Create an inline result containing an article - * - * @param id Inline result ID - * @param params Article - */ - export function article( - id: string, - params: Omit, - ): InputInlineResultArticle { - const ret = params as tl.Mutable - ret.id = id - ret.type = 'article' - - return ret - } - - /** - * Create an inline result containing a GIF - * - * @param id Inline result ID - * @param media GIF animation - * @param params Additional parameters - */ - export function gif( - id: string, - media: string | tl.RawInputWebDocument | tl.RawInputDocument, - params: Omit = {}, - ): InputInlineResultGif { - const ret = params as tl.Mutable - ret.id = id - ret.type = 'gif' - ret.media = media - - return ret - } - - /** - * Create an inline result containing a video - * - * @param id Inline result ID - * @param media Video - * @param params Additional parameters - */ - export function video( - id: string, - media: string | tl.RawInputWebDocument | tl.RawInputDocument, - params: Omit, - ): InputInlineResultVideo { - const ret = params as tl.Mutable - ret.id = id - ret.type = 'video' - ret.media = media - - return ret - } - - /** - * Create an inline result containing an audio file - * - * @param id Inline result ID - * @param media Audio file - * @param params Additional parameters - */ - export function audio( - id: string, - media: string | tl.RawInputWebDocument | tl.RawInputDocument, - params: Omit, - ): InputInlineResultAudio { - const ret = params as tl.Mutable - ret.id = id - ret.type = 'audio' - ret.media = media - - return ret - } - - /** - * Create an inline result containing a voice note - * - * @param id Inline result ID - * @param media Voice note - * @param params Additional parameters - */ - export function voice( - id: string, - media: string | tl.RawInputWebDocument | tl.RawInputDocument, - params: Omit, - ): InputInlineResultVoice { - const ret = params as tl.Mutable - ret.id = id - ret.type = 'voice' - ret.media = media - - return ret - } - - /** - * Create an inline result containing a photo - * - * @param id Inline result ID - * @param media Photo - * @param params Additional parameters - */ - export function photo( - id: string, - media: string | tl.RawInputWebDocument | tl.RawInputPhoto, - params: Omit = {}, - ): InputInlineResultPhoto { - const ret = params as tl.Mutable - ret.id = id - ret.type = 'photo' - ret.media = media - - return ret - } - - /** - * Create an inline result containing a sticker - * - * @param id Inline result ID - * @param media Sticker - */ - export function sticker(id: string, media: string | tl.RawInputDocument): InputInlineResultSticker { - return { - id, - type: 'sticker', - media, - } - } - - /** - * Create an inline result containing a document - * (only PDF and ZIP are supported when using URL) - * - * @param id Inline result ID - * @param media Document - * @param params Additional parameters - */ - export function file( - id: string, - media: string | tl.RawInputWebDocument | tl.RawInputDocument, - params: Omit, - ): InputInlineResultFile { - const ret = params as tl.Mutable - ret.id = id - ret.type = 'file' - ret.media = media - - return ret - } - - /** - * Create an inline result containing a geolocation - * - * @param id Inline result ID - * @param params Additional parameters - */ - export function geo(id: string, params: Omit): InputInlineResultGeo { - const ret = params as tl.Mutable - ret.id = id - ret.type = 'geo' - - return ret - } - - /** - * Create an inline result containing a venue - * - * @param id Inline result ID - * @param params Venue parameters - */ - export function venue(id: string, params: Omit): InputInlineResultVenue { - const ret = params as tl.Mutable - ret.id = id - ret.type = 'venue' - - return ret - } - - /** - * Create an inline result containing a contact - * - * @param id Inline result ID - * @param params Contact parameters - */ - export function contact( - id: string, - params: Omit, - ): InputInlineResultContact { - const ret = params as tl.Mutable - ret.id = id - ret.type = 'contact' - - return ret - } - - /** - * Create an inline result containing a game - * - * @param id Inline result ID - * @param shortName Short name of the game - * @param params Additional parameters - */ - export function game( - id: string, - shortName: string, - params: Omit = {}, - ): InputInlineResultGame { - const ret = params as tl.Mutable - ret.id = id - ret.type = 'game' - ret.shortName = shortName - - return ret - } - - /** @internal */ - export async function _convertToTl( - client: ITelegramClient, - results: InputInlineResult[], - ): Promise<[boolean, tl.TypeInputBotInlineResult[]]> { - const normalizeThumb = (obj: InputInlineResult, fallback?: string): tl.RawInputWebDocument | undefined => { - if (obj.type !== 'voice' && obj.type !== 'audio' && obj.type !== 'sticker' && obj.type !== 'game') { - if (!obj.thumb || typeof obj.thumb === 'string') { - if (!obj.thumb && !fallback) { - return undefined - } - - return { - _: 'inputWebDocument', - size: 0, - url: obj.thumb || fallback!, - mimeType: obj.type === 'gif' ? obj.thumbMime ?? obj.mime ?? 'video/mp4' : 'image/jpeg', - attributes: [], - } - } - - return obj.thumb - } - } - - const items: tl.TypeInputBotInlineResult[] = [] - - let isGallery = false - let forceVertical = false - - for (const obj of results) { - switch (obj.type) { - case 'article': { - forceVertical = true - - let sendMessage: tl.TypeInputBotInlineMessage - - if (obj.message) { - sendMessage = await BotInlineMessage._convertToTl(client, obj.message) - } 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, - } - } - - items.push({ - _: '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' ? normalizeThumb(obj) : obj.thumb, - sendMessage, - }) - continue - } - case 'game': { - let sendMessage: tl.TypeInputBotInlineMessage - - if (obj.message) { - sendMessage = await BotInlineMessage._convertToTl(client, obj.message) - - if (sendMessage._ !== 'inputBotInlineMessageGame') { - throw new MtArgumentError('game inline result must contain a game inline message') - } - } else { - sendMessage = { - _: 'inputBotInlineMessageGame', - } - } - - items.push({ - _: 'inputBotInlineResultGame', - id: obj.id, - shortName: obj.shortName, - sendMessage, - }) - continue - } - case 'gif': - case 'photo': - case 'sticker': - isGallery = true - break - case 'audio': - case 'contact': - case 'voice': - forceVertical = true - } - - let sendMessage: tl.TypeInputBotInlineMessage - - if (obj.message) { - sendMessage = await BotInlineMessage._convertToTl(client, obj.message) - } else if (obj.type === 'venue') { - if (obj.latitude && obj.longitude) { - sendMessage = { - _: 'inputBotInlineMessageMediaVenue', - title: obj.title, - address: obj.address, - geoPoint: { - _: 'inputGeoPoint', - lat: obj.latitude, - long: obj.longitude, - }, - provider: '', - venueId: '', - venueType: '', - } - } else { - throw new MtArgumentError('message or location (lat&lon) bust be supplied for venue inline result') - } - } else if (obj.type === 'video' && obj.isEmbed && typeof obj.media === 'string') { - sendMessage = { - _: 'inputBotInlineMessageText', - message: obj.media, - } - } else if (obj.type === 'geo') { - sendMessage = { - _: 'inputBotInlineMessageMediaGeo', - geoPoint: { - _: 'inputGeoPoint', - lat: obj.latitude, - long: obj.longitude, - }, - } - } else if (obj.type === 'contact') { - sendMessage = { - _: 'inputBotInlineMessageMediaContact', - phoneNumber: obj.phone, - firstName: obj.firstName, - lastName: obj.lastName ?? '', - vcard: '', - } - } else { - sendMessage = { - _: 'inputBotInlineMessageMediaAuto', - message: '', - } - } - - let media: tl.TypeInputWebDocument | tl.TypeInputDocument | tl.TypeInputPhoto | undefined = undefined - - if (obj.type !== 'geo' && obj.type !== 'venue' && obj.type !== 'contact') { - if (typeof obj.media === 'string') { - // file id or url - if (obj.media.match(/^https?:\/\//)) { - if (obj.type === 'sticker') { - throw new MtArgumentError('sticker inline result cannot contain a URL') - } - - let mime: string - if (obj.type === 'video') mime = 'video/mp4' - else if (obj.type === 'audio') { - mime = obj.mime ?? 'audio/mpeg' - } else if (obj.type === 'gif') { - mime = obj.mime ?? 'video/mp4' - } else if (obj.type === 'voice') mime = 'audio/ogg' - else if (obj.type === 'file') { - if (!obj.mime) { - throw new MtArgumentError('MIME type must be specified for file inline result') - } - - mime = obj.mime - } else mime = 'image/jpeg' - - const attributes: tl.TypeDocumentAttribute[] = [] - - if ( - (obj.type === 'video' || obj.type === 'gif' || obj.type === 'photo') && - obj.width && - obj.height - ) { - if (obj.type !== 'photo' && obj.duration) { - attributes.push({ - _: 'documentAttributeVideo', - w: obj.width, - h: obj.height, - duration: obj.duration, - }) - } else { - attributes.push({ - _: 'documentAttributeImageSize', - w: obj.width, - h: obj.height, - }) - } - } else if (obj.type === 'audio' || obj.type === 'voice') { - attributes.push({ - _: 'documentAttributeAudio', - voice: obj.type === 'voice', - duration: obj.duration ?? 0, - title: obj.type === 'audio' ? obj.title : '', - performer: obj.type === 'audio' ? obj.performer : '', - }) - } - - attributes.push({ - _: 'documentAttributeFilename', - fileName: extractFileName(obj.media), - }) - - media = { - _: 'inputWebDocument', - url: obj.media, - mimeType: mime, - size: 0, - attributes, - } - } else if (obj.type === 'photo') { - media = fileIdToInputPhoto(obj.media) - } else { - media = fileIdToInputDocument(obj.media) - } - } else { - media = obj.media - } - } - - let title: string | undefined = undefined - let description: string | undefined = undefined - - // incredible hacks by durov team. - // i honestly don't understand why didn't they just - // make a bunch of types, as they normally do, - // but whatever. - // ref: https://github.com/tdlib/td/blob/master/td/telegram/InlineQueriesManager.cpp - if (obj.type === 'contact') { - title = obj.lastName?.length ? `${obj.firstName} ${obj.lastName}` : obj.firstName - } else if (obj.type !== 'sticker') { - title = obj.title - } - - if (obj.type === 'audio') { - description = obj.performer - } else if (obj.type === 'geo') { - description = `${obj.latitude} ${obj.longitude}` - } else if (obj.type === 'venue') { - description = obj.address - } else if (obj.type === 'contact') { - description = obj.phone - } else if (obj.type !== 'voice' && obj.type !== 'sticker') { - description = obj.description - } - - if (!media || media._ === 'inputWebDocument') { - items.push({ - _: 'inputBotInlineResult', - id: obj.id, - type: obj.type, - title, - description, - content: media, - thumb: normalizeThumb(obj, media?.url), - sendMessage, - }) - continue - } - - if (media._ === 'inputPhoto') { - items.push({ - _: 'inputBotInlineResultPhoto', - id: obj.id, - type: obj.type, - photo: media, - sendMessage, - }) - continue - } - - items.push({ - _: 'inputBotInlineResultDocument', - id: obj.id, - type: obj.type, - title, - description, - document: media, - sendMessage, - }) - } - - return [isGallery && !forceVertical, items] - } -} diff --git a/packages/core/src/highlevel/types/bots/keyboards.ts b/packages/core/src/highlevel/types/bots/keyboards.ts deleted file mode 100644 index f19b6f64..00000000 --- a/packages/core/src/highlevel/types/bots/keyboards.ts +++ /dev/null @@ -1,478 +0,0 @@ -import { tl } from '@mtcute/tl' - -import { getPlatform } from '../../../platform.js' -import { assertNever } from '../../../types/utils.js' -import { toInputUser } from '../../utils/peer-utils.js' -import { BotKeyboardBuilder } from './keyboard-builder.js' - -/** - * Reply keyboard markup - */ -export interface ReplyKeyboardMarkup extends Omit { - readonly type: 'reply' - - /** - * Two-dimensional array of buttons - */ - readonly buttons: tl.TypeKeyboardButton[][] -} - -/** - * Hide previously sent bot keyboard - */ -export interface ReplyKeyboardHide extends Omit { - readonly type: 'reply_hide' -} - -/** - * Force the user to send a reply - */ -export interface ReplyKeyboardForceReply extends Omit { - readonly type: 'force_reply' -} - -/** - * Inline keyboard markup - */ -export interface InlineKeyboardMarkup { - readonly type: 'inline' - - /** - * Two-dimensional array of buttons - */ - readonly buttons: tl.TypeKeyboardButton[][] -} - -export type ReplyMarkup = - | ReplyKeyboardMarkup - | ReplyKeyboardHide - | ReplyKeyboardForceReply - | InlineKeyboardMarkup - | tl.TypeReplyMarkup - -/** - * Convenience methods wrapping TL - * objects creation for bot keyboard buttons. - * - * You can also use the type-discriminated objects directly. - * - * > **Note**: Button creation functions are intended to be used - * > with inline reply markup, unless stated otherwise - * > in the description. - */ -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace BotKeyboard { - /** Create a keyboard builder */ - export function builder(maxRowWidth?: number | null): BotKeyboardBuilder { - return new BotKeyboardBuilder(maxRowWidth) - } - - /** - * Create an inline keyboard markup - * - * @param buttons Two-dimensional array of buttons - */ - export function inline(buttons: tl.TypeKeyboardButton[][]): InlineKeyboardMarkup { - return { - type: 'inline', - buttons, - } - } - - /** - * Create a reply keyboard markup - * - * @param buttons Two-dimensional array of buttons - * @param params Additional parameters for the keyboard - */ - export function reply( - buttons: tl.TypeKeyboardButton[][], - params: Omit = {}, - ): ReplyKeyboardMarkup { - const ret = params as tl.Mutable - ret.type = 'reply' - ret.buttons = buttons - - return ret - } - - /** - * Hide the previously sent reply keyboard - * - * @param selective - * Whether to remove the keyboard for specific users only. Targets: - * - users that are @mentioned in the text of the Message - * - in case this is a reply, sender of the original message - */ - export function hideReply(selective?: boolean): ReplyKeyboardHide { - return { - type: 'reply_hide', - selective, - } - } - - /** - * Force the user to send a reply - */ - export function forceReply(params: Omit = {}): ReplyKeyboardForceReply { - const ret = params as tl.Mutable - ret.type = 'force_reply' - - return ret - } - - /** - * Create a text-only keyboard button. - * - * Used for reply keyboards, not inline! - * - * @param text Button text - */ - export function text(text: string): tl.RawKeyboardButton { - return { - _: 'keyboardButton', - text, - } - } - - /** - * Create a keyboard button requesting for user's contact. - * Available only for private chats. - * - * Used for reply keyboards, not inline! - * - * @param text Button text - */ - export function requestContact(text: string): tl.RawKeyboardButtonRequestPhone { - return { - _: 'keyboardButtonRequestPhone', - text, - } - } - - /** - * Create a keyboard button requesting for user's geo location. - * Available only for private chats. - * - * Used for reply keyboards, not inline! - * - * @param text Button text - */ - export function requestGeo(text: string): tl.RawKeyboardButtonRequestGeoLocation { - return { - _: 'keyboardButtonRequestGeoLocation', - text, - } - } - - /** - * Create a keyboard button requesting the user to create and send a poll. - * Available only for private chats. - * - * Used for reply keyboards, not inline! - * - * @param text Button text - * @param quiz If set, only quiz polls can be sent - */ - export function requestPoll(text: string, quiz?: boolean): tl.RawKeyboardButtonRequestPoll { - return { - _: 'keyboardButtonRequestPoll', - text, - quiz, - } - } - - /** - * Create a keyboard button with a link. - * - * Used for inline keyboards, not reply! - * - * @param text Button text - * @param url URL - */ - export function url(text: string, url: string): tl.RawKeyboardButtonUrl { - return { - _: 'keyboardButtonUrl', - text, - url, - } - } - - /** - * Create a keyboard button with a link. - * - * Used for inline keyboards, not reply! - * - * @param text Button text - * @param data Callback data (1-64 bytes). String will be converted to `Buffer` - * @param requiresPassword - * Whether the user should verify their identity by entering 2FA password. - * See more: {@link tl.RawKeyboardButtonCallback#requiresPassword} - */ - export function callback( - text: string, - data: string | Uint8Array, - requiresPassword?: boolean, - ): tl.RawKeyboardButtonCallback { - return { - _: 'keyboardButtonCallback', - text, - requiresPassword, - data: typeof data === 'string' ? getPlatform().utf8Encode(data) : data, - } - } - - /** - * Button to force a user to switch to inline mode. - * - * Pressing the button will prompt the user to select - * one of their chats, open that chat and insert the bot‘s - * username and the specified inline query (if any) in the input field. - * - * Used for inline keyboards, not reply! - * - * @param text Button text - * @param query Inline query (can be empty or omitted) - * @param currentChat - * If set, pressing the button will insert the bot's username - * and the specified inline query in the current chat's input field - */ - export function switchInline(text: string, query = '', currentChat?: boolean): tl.RawKeyboardButtonSwitchInline { - return { - _: 'keyboardButtonSwitchInline', - samePeer: currentChat, - text, - query, - } - } - - /** - * Button to start a game - * - * Used for inline keyboards, not reply! - * - * **Note**: This type of button must always be - * the first button in the first row. ID of the - * game is inferred from {@link InputMedia.game}, - * thus this button should only be used with it. - */ - export function game(text: string): tl.RawKeyboardButtonGame { - return { _: 'keyboardButtonGame', text } - } - - /** - * Button to pay for a product. - * - * Used for inline keyboards, not reply! - * - * **Note**: This type of button must always be - * the first button in the first row. Related - * invoice is inferred from {@link InputMedia.invoice}, - * thus this button should only be used with it. - */ - export function pay(text: string): tl.RawKeyboardButtonBuy { - return { _: 'keyboardButtonBuy', text } - } - - /** - * Button to authorize a user - * - * Used for inline keyboards, not reply! - * - * @param text Button label - * @param url Authorization URL (see {@link tl.RawInputKeyboardButtonUrlAuth}) - * @param params - */ - export function urlAuth( - text: string, - url: string, - params: { - /** - * Button label when forwarded - */ - fwdText?: string - - /** - * Whether to request the permission for - * your bot to send messages to the user - */ - requestWriteAccess?: boolean - - /** - * Bot, which will be used for user authorization. - * `url` domain must be the same as the domain linked - * with the bot. - * - * @default current bot - */ - bot?: tl.TypeInputUser - } = {}, - ): tl.RawInputKeyboardButtonUrlAuth { - return { - _: 'inputKeyboardButtonUrlAuth', - text, - url, - bot: params.bot ?? { - _: 'inputUserSelf', - }, - fwdText: params.fwdText, - requestWriteAccess: params.requestWriteAccess, - } - } - - /** - * Button to open webview - * - * Used for both inline keyboards and reply ones - * - * @param text Button label - * @param url WebView URL - */ - export function webView(text: string, url: string): tl.RawKeyboardButtonWebView { - return { - _: 'keyboardButtonWebView', - text, - url, - } - } - - /** - * Button to open user profile - * - * @param text Text of the button - * @param user User to be opened (use {@link TelegramClient.resolvePeer}) - */ - export function userProfile(text: string, user: tl.TypeInputPeer): tl.RawInputKeyboardButtonUserProfile { - return { - _: 'inputKeyboardButtonUserProfile', - text, - userId: toInputUser(user), - } - } - - /** - * Button to request a peer from the user - * - * @param text Text of the button - * @param buttonId ID of the button that will later be passed to the service message - */ - export function requestPeer( - text: string, - buttonId: number, - params: { - /** - * Peer type, along with filters - */ - peerType: tl.TypeRequestPeerType - - /** - * Maximum number of peers to be selected - * - * @default 1 - */ - count?: number - }, - ): tl.RawKeyboardButtonRequestPeer { - return { - _: 'keyboardButtonRequestPeer', - text, - buttonId, - peerType: params.peerType, - maxQuantity: params.count ?? 1, - } - } - - /** - * Find a button in the keyboard by its text or by predicate - * - * @param buttons Two-dimensional array of buttons - * @param predicate Button text or predicate function - */ - export function findButton( - buttons: tl.TypeKeyboardButton[][], - predicate: string | ((btn: tl.TypeKeyboardButton) => boolean), - ): tl.TypeKeyboardButton | null { - if (typeof predicate === 'string') { - const text = predicate - - predicate = (btn) => { - return 'text' in btn && btn.text === text - } - } - - for (const row of buttons) { - for (const btn of row) { - if (predicate(btn)) { - return btn - } - } - } - - return null - } - - /** @internal */ - export function _rowsTo2d(rows: tl.RawKeyboardButtonRow[]): tl.TypeKeyboardButton[][] { - return rows.map((it) => it.buttons) - } - - /** @internal */ - export function _2dToRows(arr: tl.TypeKeyboardButton[][], inline: boolean): tl.RawKeyboardButtonRow[] { - return arr.map((row) => { - if (!inline) { - // le cringe - row = row.map((btn) => - btn._ === 'keyboardButtonWebView' ? - { - ...btn, - _: 'keyboardButtonSimpleWebView', - } : - btn, - ) - } - - return { - _: 'keyboardButtonRow', - buttons: row, - } - }) - } - - /** @internal */ - export function _convertToTl(obj?: ReplyMarkup): tl.TypeReplyMarkup | undefined { - if (!obj) return obj - if (tl.isAnyReplyMarkup(obj)) return obj - - switch (obj.type) { - case 'reply': - return { - _: 'replyKeyboardMarkup', - resize: obj.resize, - singleUse: obj.singleUse, - selective: obj.selective, - persistent: obj.persistent, - placeholder: obj.placeholder, - rows: _2dToRows(obj.buttons, false), - } - case 'reply_hide': - return { - _: 'replyKeyboardHide', - selective: obj.selective, - } - case 'force_reply': - return { - _: 'replyKeyboardForceReply', - singleUse: obj.singleUse, - selective: obj.selective, - placeholder: obj.placeholder, - } - case 'inline': - return { - _: 'replyInlineMarkup', - rows: _2dToRows(obj.buttons, true), - } - default: - assertNever(obj) - } - } -} diff --git a/packages/core/src/highlevel/types/bots/keyboard-builder.test.ts b/packages/core/src/highlevel/types/bots/keyboards/builder.test.ts similarity index 99% rename from packages/core/src/highlevel/types/bots/keyboard-builder.test.ts rename to packages/core/src/highlevel/types/bots/keyboards/builder.test.ts index 5a7df4b2..ac7988fe 100644 --- a/packages/core/src/highlevel/types/bots/keyboard-builder.test.ts +++ b/packages/core/src/highlevel/types/bots/keyboards/builder.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { BotKeyboardBuilder } from './keyboard-builder.js' +import { BotKeyboardBuilder } from './builder.js' describe('BotKeyboardBuilder', () => { describe('#push', () => { diff --git a/packages/core/src/highlevel/types/bots/keyboard-builder.ts b/packages/core/src/highlevel/types/bots/keyboards/builder.ts similarity index 99% rename from packages/core/src/highlevel/types/bots/keyboard-builder.ts rename to packages/core/src/highlevel/types/bots/keyboards/builder.ts index fe1c00e5..dbf7aae7 100644 --- a/packages/core/src/highlevel/types/bots/keyboard-builder.ts +++ b/packages/core/src/highlevel/types/bots/keyboards/builder.ts @@ -1,6 +1,6 @@ import { tl } from '@mtcute/tl' -import type { InlineKeyboardMarkup, ReplyKeyboardMarkup } from './keyboards.js' +import type { InlineKeyboardMarkup, ReplyKeyboardMarkup } from './types.js' export type ButtonLike = tl.TypeKeyboardButton | false | null | undefined | void diff --git a/packages/core/src/highlevel/types/bots/keyboards/factories.ts b/packages/core/src/highlevel/types/bots/keyboards/factories.ts new file mode 100644 index 00000000..78249f96 --- /dev/null +++ b/packages/core/src/highlevel/types/bots/keyboards/factories.ts @@ -0,0 +1,427 @@ +import { tl } from '@mtcute/tl' + +import { getPlatform } from '../../../../platform.js' +import { assertNever } from '../../../../types/utils.js' +import { toInputUser } from '../../../utils/peer-utils.js' +import { BotKeyboardBuilder } from './builder.js' +import { + InlineKeyboardMarkup, + ReplyKeyboardForceReply, + ReplyKeyboardHide, + ReplyKeyboardMarkup, + ReplyMarkup, +} from './types.js' + +/** Create a keyboard builder */ +export function builder(maxRowWidth?: number | null): BotKeyboardBuilder { + return new BotKeyboardBuilder(maxRowWidth) +} + +/** + * Create an inline keyboard markup + * + * @param buttons Two-dimensional array of buttons + */ +export function inline(buttons: tl.TypeKeyboardButton[][]): InlineKeyboardMarkup { + return { + type: 'inline', + buttons, + } +} + +/** + * Create a reply keyboard markup + * + * @param buttons Two-dimensional array of buttons + * @param params Additional parameters for the keyboard + */ +export function reply( + buttons: tl.TypeKeyboardButton[][], + params: Omit = {}, +): ReplyKeyboardMarkup { + const ret = params as tl.Mutable + ret.type = 'reply' + ret.buttons = buttons + + return ret +} + +/** + * Hide the previously sent reply keyboard + * + * @param selective + * Whether to remove the keyboard for specific users only. Targets: + * - users that are @mentioned in the text of the Message + * - in case this is a reply, sender of the original message + */ +export function hideReply(selective?: boolean): ReplyKeyboardHide { + return { + type: 'reply_hide', + selective, + } +} + +/** + * Force the user to send a reply + */ +export function forceReply(params: Omit = {}): ReplyKeyboardForceReply { + const ret = params as tl.Mutable + ret.type = 'force_reply' + + return ret +} + +/** + * Create a text-only keyboard button. + * + * Used for reply keyboards, not inline! + * + * @param text Button text + */ +export function text(text: string): tl.RawKeyboardButton { + return { + _: 'keyboardButton', + text, + } +} + +/** + * Create a keyboard button requesting for user's contact. + * Available only for private chats. + * + * Used for reply keyboards, not inline! + * + * @param text Button text + */ +export function requestContact(text: string): tl.RawKeyboardButtonRequestPhone { + return { + _: 'keyboardButtonRequestPhone', + text, + } +} + +/** + * Create a keyboard button requesting for user's geo location. + * Available only for private chats. + * + * Used for reply keyboards, not inline! + * + * @param text Button text + */ +export function requestGeo(text: string): tl.RawKeyboardButtonRequestGeoLocation { + return { + _: 'keyboardButtonRequestGeoLocation', + text, + } +} + +/** + * Create a keyboard button requesting the user to create and send a poll. + * Available only for private chats. + * + * Used for reply keyboards, not inline! + * + * @param text Button text + * @param quiz If set, only quiz polls can be sent + */ +export function requestPoll(text: string, quiz?: boolean): tl.RawKeyboardButtonRequestPoll { + return { + _: 'keyboardButtonRequestPoll', + text, + quiz, + } +} + +/** + * Create a keyboard button with a link. + * + * Used for inline keyboards, not reply! + * + * @param text Button text + * @param url URL + */ +export function url(text: string, url: string): tl.RawKeyboardButtonUrl { + return { + _: 'keyboardButtonUrl', + text, + url, + } +} + +/** + * Create a keyboard button with a link. + * + * Used for inline keyboards, not reply! + * + * @param text Button text + * @param data Callback data (1-64 bytes). String will be converted to `Buffer` + * @param requiresPassword + * Whether the user should verify their identity by entering 2FA password. + * See more: {@link tl.RawKeyboardButtonCallback#requiresPassword} + */ +export function callback( + text: string, + data: string | Uint8Array, + requiresPassword?: boolean, +): tl.RawKeyboardButtonCallback { + return { + _: 'keyboardButtonCallback', + text, + requiresPassword, + data: typeof data === 'string' ? getPlatform().utf8Encode(data) : data, + } +} + +/** + * Button to force a user to switch to inline mode. + * + * Pressing the button will prompt the user to select + * one of their chats, open that chat and insert the bot‘s + * username and the specified inline query (if any) in the input field. + * + * Used for inline keyboards, not reply! + * + * @param text Button text + * @param query Inline query (can be empty or omitted) + * @param currentChat + * If set, pressing the button will insert the bot's username + * and the specified inline query in the current chat's input field + */ +export function switchInline(text: string, query = '', currentChat?: boolean): tl.RawKeyboardButtonSwitchInline { + return { + _: 'keyboardButtonSwitchInline', + samePeer: currentChat, + text, + query, + } +} + +/** + * Button to start a game + * + * Used for inline keyboards, not reply! + * + * **Note**: This type of button must always be + * the first button in the first row. ID of the + * game is inferred from {@link InputMedia.game}, + * thus this button should only be used with it. + */ +export function game(text: string): tl.RawKeyboardButtonGame { + return { _: 'keyboardButtonGame', text } +} + +/** + * Button to pay for a product. + * + * Used for inline keyboards, not reply! + * + * **Note**: This type of button must always be + * the first button in the first row. Related + * invoice is inferred from {@link InputMedia.invoice}, + * thus this button should only be used with it. + */ +export function pay(text: string): tl.RawKeyboardButtonBuy { + return { _: 'keyboardButtonBuy', text } +} + +/** + * Button to authorize a user + * + * Used for inline keyboards, not reply! + * + * @param text Button label + * @param url Authorization URL (see {@link tl.RawInputKeyboardButtonUrlAuth}) + * @param params + */ +export function urlAuth( + text: string, + url: string, + params: { + /** + * Button label when forwarded + */ + fwdText?: string + + /** + * Whether to request the permission for + * your bot to send messages to the user + */ + requestWriteAccess?: boolean + + /** + * Bot, which will be used for user authorization. + * `url` domain must be the same as the domain linked + * with the bot. + * + * @default current bot + */ + bot?: tl.TypeInputUser + } = {}, +): tl.RawInputKeyboardButtonUrlAuth { + return { + _: 'inputKeyboardButtonUrlAuth', + text, + url, + bot: params.bot ?? { + _: 'inputUserSelf', + }, + fwdText: params.fwdText, + requestWriteAccess: params.requestWriteAccess, + } +} + +/** + * Button to open webview + * + * Used for both inline keyboards and reply ones + * + * @param text Button label + * @param url WebView URL + */ +export function webView(text: string, url: string): tl.RawKeyboardButtonWebView { + return { + _: 'keyboardButtonWebView', + text, + url, + } +} + +/** + * Button to open user profile + * + * @param text Text of the button + * @param user User to be opened (use {@link TelegramClient.resolvePeer}) + */ +export function userProfile(text: string, user: tl.TypeInputPeer): tl.RawInputKeyboardButtonUserProfile { + return { + _: 'inputKeyboardButtonUserProfile', + text, + userId: toInputUser(user), + } +} + +/** + * Button to request a peer from the user + * + * @param text Text of the button + * @param buttonId ID of the button that will later be passed to the service message + */ +export function requestPeer( + text: string, + buttonId: number, + params: { + /** + * Peer type, along with filters + */ + peerType: tl.TypeRequestPeerType + + /** + * Maximum number of peers to be selected + * + * @default 1 + */ + count?: number + }, +): tl.RawKeyboardButtonRequestPeer { + return { + _: 'keyboardButtonRequestPeer', + text, + buttonId, + peerType: params.peerType, + maxQuantity: params.count ?? 1, + } +} + +/** + * Find a button in the keyboard by its text or by predicate + * + * @param buttons Two-dimensional array of buttons + * @param predicate Button text or predicate function + */ +export function findButton( + buttons: tl.TypeKeyboardButton[][], + predicate: string | ((btn: tl.TypeKeyboardButton) => boolean), +): tl.TypeKeyboardButton | null { + if (typeof predicate === 'string') { + const text = predicate + + predicate = (btn) => { + return 'text' in btn && btn.text === text + } + } + + for (const row of buttons) { + for (const btn of row) { + if (predicate(btn)) { + return btn + } + } + } + + return null +} + +/** @internal */ +export function _rowsTo2d(rows: tl.RawKeyboardButtonRow[]): tl.TypeKeyboardButton[][] { + return rows.map((it) => it.buttons) +} + +/** @internal */ +export function _2dToRows(arr: tl.TypeKeyboardButton[][], inline: boolean): tl.RawKeyboardButtonRow[] { + return arr.map((row) => { + if (!inline) { + // le cringe + row = row.map((btn) => + btn._ === 'keyboardButtonWebView' ? + { + ...btn, + _: 'keyboardButtonSimpleWebView', + } : + btn, + ) + } + + return { + _: 'keyboardButtonRow', + buttons: row, + } + }) +} + +/** @internal */ +export function _convertToTl(obj?: ReplyMarkup): tl.TypeReplyMarkup | undefined { + if (!obj) return obj + if (tl.isAnyReplyMarkup(obj)) return obj + + switch (obj.type) { + case 'reply': + return { + _: 'replyKeyboardMarkup', + resize: obj.resize, + singleUse: obj.singleUse, + selective: obj.selective, + persistent: obj.persistent, + placeholder: obj.placeholder, + rows: _2dToRows(obj.buttons, false), + } + case 'reply_hide': + return { + _: 'replyKeyboardHide', + selective: obj.selective, + } + case 'force_reply': + return { + _: 'replyKeyboardForceReply', + singleUse: obj.singleUse, + selective: obj.selective, + placeholder: obj.placeholder, + } + case 'inline': + return { + _: 'replyInlineMarkup', + rows: _2dToRows(obj.buttons, true), + } + default: + assertNever(obj) + } +} diff --git a/packages/core/src/highlevel/types/bots/keyboards/index.ts b/packages/core/src/highlevel/types/bots/keyboards/index.ts new file mode 100644 index 00000000..6f113112 --- /dev/null +++ b/packages/core/src/highlevel/types/bots/keyboards/index.ts @@ -0,0 +1,16 @@ +export * from './types.js' +import * as BotKeyboard from './factories.js' + +export { + /** + * Convenience methods wrapping TL + * objects creation for bot keyboard buttons. + * + * You can also use the type-discriminated objects directly. + * + * > **Note**: Button creation functions are intended to be used + * > with inline reply markup, unless stated otherwise + * > in the description. + */ + BotKeyboard, +} diff --git a/packages/core/src/highlevel/types/bots/keyboards.test.ts b/packages/core/src/highlevel/types/bots/keyboards/keyboards.test.ts similarity index 99% rename from packages/core/src/highlevel/types/bots/keyboards.test.ts rename to packages/core/src/highlevel/types/bots/keyboards/keyboards.test.ts index bffecd6f..93912cc1 100644 --- a/packages/core/src/highlevel/types/bots/keyboards.test.ts +++ b/packages/core/src/highlevel/types/bots/keyboards/keyboards.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import { tl } from '@mtcute/tl' -import { BotKeyboard } from './keyboards.js' +import { BotKeyboard } from './index.js' describe('findButton', () => { const kb: tl.TypeKeyboardButton[][] = [ diff --git a/packages/core/src/highlevel/types/bots/keyboards/types.ts b/packages/core/src/highlevel/types/bots/keyboards/types.ts new file mode 100644 index 00000000..48f73917 --- /dev/null +++ b/packages/core/src/highlevel/types/bots/keyboards/types.ts @@ -0,0 +1,46 @@ +import { tl } from '@mtcute/tl' + +/** + * Reply keyboard markup + */ +export interface ReplyKeyboardMarkup extends Omit { + readonly type: 'reply' + + /** + * Two-dimensional array of buttons + */ + readonly buttons: tl.TypeKeyboardButton[][] +} + +/** + * Hide previously sent bot keyboard + */ +export interface ReplyKeyboardHide extends Omit { + readonly type: 'reply_hide' +} + +/** + * Force the user to send a reply + */ +export interface ReplyKeyboardForceReply extends Omit { + readonly type: 'force_reply' +} + +/** + * Inline keyboard markup + */ +export interface InlineKeyboardMarkup { + readonly type: 'inline' + + /** + * Two-dimensional array of buttons + */ + readonly buttons: tl.TypeKeyboardButton[][] +} + +export type ReplyMarkup = + | ReplyKeyboardMarkup + | ReplyKeyboardHide + | ReplyKeyboardForceReply + | InlineKeyboardMarkup + | tl.TypeReplyMarkup diff --git a/packages/core/src/highlevel/types/media/index.ts b/packages/core/src/highlevel/types/media/index.ts index addee4ba..649d40ec 100644 --- a/packages/core/src/highlevel/types/media/index.ts +++ b/packages/core/src/highlevel/types/media/index.ts @@ -3,7 +3,7 @@ export * from './contact.js' export * from './dice.js' export * from './document.js' export * from './game.js' -export * from './input-media.js' +export * from './input-media/index.js' export * from './invoice.js' export * from './location.js' export * from './photo.js' diff --git a/packages/core/src/highlevel/types/media/input-media/factories.ts b/packages/core/src/highlevel/types/media/input-media/factories.ts new file mode 100644 index 00000000..53355acd --- /dev/null +++ b/packages/core/src/highlevel/types/media/input-media/factories.ts @@ -0,0 +1,300 @@ +import { tl } from '@mtcute/tl' + +import { InputFileLike } from '../../files/utils.js' +import { + CaptionMixin, + InputMediaAudio, + InputMediaAuto, + InputMediaContact, + InputMediaDice, + InputMediaDocument, + InputMediaGame, + InputMediaGeo, + InputMediaGeoLive, + InputMediaInvoice, + InputMediaLike, + InputMediaPhoto, + InputMediaPoll, + InputMediaQuiz, + InputMediaSticker, + InputMediaStory, + InputMediaVenue, + InputMediaVideo, + InputMediaVoice, + InputMediaWebpage, +} from './types.js' + +/** Omit `type` and `file` from the given type */ +export type OmitTypeAndFile = Omit + +/** + * Create an animation to be sent + * + * @param file Animation + * @param params Additional parameters + */ +export function animation(file: InputFileLike, params: OmitTypeAndFile = {}): InputMediaVideo { + const ret = params as tl.Mutable + ret.type = 'video' + ret.file = file + ret.isAnimated = true + + return ret +} + +/** + * Create an audio to be sent + * + * @param file Audio file + * @param params Additional parameters + */ +export function audio(file: InputFileLike, params: OmitTypeAndFile = {}): InputMediaAudio { + const ret = params as tl.Mutable + ret.type = 'audio' + ret.file = file + + return ret +} + +/** + * Create an document to be sent + * + * @param file Document + * @param params Additional parameters + */ +export function document(file: InputFileLike, params: OmitTypeAndFile = {}): InputMediaDocument { + const ret = params as tl.Mutable + ret.type = 'document' + ret.file = file + + return ret +} + +/** + * Create an photo to be sent + * + * @param file Photo + * @param params Additional parameters + */ +export function photo(file: InputFileLike, params: OmitTypeAndFile = {}): InputMediaPhoto { + const ret = params as tl.Mutable + ret.type = 'photo' + ret.file = file + + return ret +} + +/** + * Create an video to be sent + * + * @param file Video + * @param params Additional parameters + */ +export function video(file: InputFileLike, params: OmitTypeAndFile = {}): InputMediaVideo { + const ret = params as tl.Mutable + ret.type = 'video' + ret.file = file + + return ret +} + +/** + * Create a voice note to be sent + * + * @param file Voice note + * @param params Additional parameters + */ +export function voice(file: InputFileLike, params: OmitTypeAndFile = {}): InputMediaVoice { + const ret = params as tl.Mutable + ret.type = 'voice' + ret.file = file + + return ret +} + +/** + * Create a sticker to be sent + * + * @param file Sticker + * @param params Additional parameters + */ +export function sticker(file: InputFileLike, params: OmitTypeAndFile = {}): InputMediaSticker { + const ret = params as tl.Mutable + ret.type = 'sticker' + ret.file = file + + return ret +} + +/** + * Create a venue to be sent + * + * @param params Venue parameters + */ +export function venue(params: OmitTypeAndFile): InputMediaVenue { + const ret = params as tl.Mutable + ret.type = 'venue' + + return ret +} + +/** + * Create a geolocation to be sent + * + * @param latitude Latitude of the location + * @param longitude Longitude of the location + * @param params Additional parameters + */ +export function geo( + latitude: number, + longitude: number, + params: OmitTypeAndFile = {}, +): InputMediaGeo { + const ret = params as tl.Mutable + ret.type = 'geo' + ret.latitude = latitude + ret.longitude = longitude + + return ret +} + +/** + * Create a live geolocation to be sent + * + * @param latitude Latitude of the current location + * @param longitude Longitude of the current location + * @param params Additional parameters + */ +export function geoLive( + latitude: number, + longitude: number, + params: OmitTypeAndFile = {}, +): InputMediaGeoLive { + const ret = params as tl.Mutable + ret.type = 'geo_live' + ret.latitude = latitude + ret.longitude = longitude + + return ret +} + +/** + * Create a dice to be sent + * + * For convenience, known dice emojis are available + * as static members of {@link Dice}. + * + * @param emoji Emoji representing the dice + * @param params Additional parameters + */ +export function dice(emoji: string, params: CaptionMixin): InputMediaDice { + const ret = params as tl.Mutable + ret.type = 'dice' + + return ret +} + +/** + * Create a contact to be sent + * + * @param params Contact parameters + */ +export function contact(params: OmitTypeAndFile): InputMediaContact { + const ret = params as tl.Mutable + ret.type = 'contact' + + return ret +} + +/** + * Create a game to be sent + * + * @param game Game short name or TL object representing one + */ +export function game(game: string | tl.TypeInputGame): InputMediaGame { + return { + type: 'game', + game, + } +} + +/** + * Create an invoice to be sent + * + * @param params Invoice parameters + */ +export function invoice(params: OmitTypeAndFile): InputMediaInvoice { + const ret = params as tl.Mutable + ret.type = 'invoice' + + return ret +} + +/** + * Create a poll to be sent + * + * @param params Poll parameters + */ +export function poll(params: OmitTypeAndFile): InputMediaPoll { + const ret = params as tl.Mutable + ret.type = 'poll' + + return ret +} + +/** + * Create a quiz to be sent + * + * @param params Quiz parameters + */ +export function quiz(params: OmitTypeAndFile): InputMediaQuiz { + const ret = params as tl.Mutable + ret.type = 'quiz' + + return ret +} + +/** + * Create a story to be sent + * + * @param params Story parameters + */ +export function story(params: OmitTypeAndFile): InputMediaStory { + const ret = params as tl.Mutable + ret.type = 'story' + + return ret +} + +/** + * Create a webpage to be sent + * + * @param url Webpage URL + * @param params Additional parameters + */ +export function webpage(url: string, params: OmitTypeAndFile = {}): InputMediaWebpage { + const ret = params as tl.Mutable + ret.type = 'webpage' + ret.url = url + + return ret +} + +/** + * Create a document to be sent, which subtype + * is inferred automatically by file contents. + * + * Photo type is only inferred for reused files, + * newly uploaded photos with `auto` will be + * uploaded as a document + * + * @param file The media file + * @param params Additional parameters + */ +export function auto(file: InputFileLike, params: OmitTypeAndFile = {}): InputMediaAuto { + const ret = params as tl.Mutable + ret.type = 'auto' + ret.file = file + + return ret +} diff --git a/packages/core/src/highlevel/types/media/input-media/index.ts b/packages/core/src/highlevel/types/media/input-media/index.ts new file mode 100644 index 00000000..e2d20543 --- /dev/null +++ b/packages/core/src/highlevel/types/media/input-media/index.ts @@ -0,0 +1,3 @@ +import * as InputMedia from './factories.js' +export * from './types.js' +export { InputMedia } diff --git a/packages/core/src/highlevel/types/media/input-media.ts b/packages/core/src/highlevel/types/media/input-media/types.ts similarity index 61% rename from packages/core/src/highlevel/types/media/input-media.ts rename to packages/core/src/highlevel/types/media/input-media/types.ts index d4e81c85..d49c51ee 100644 --- a/packages/core/src/highlevel/types/media/input-media.ts +++ b/packages/core/src/highlevel/types/media/input-media/types.ts @@ -1,10 +1,10 @@ import { tl } from '@mtcute/tl' -import { MaybeArray } from '../../../types/utils.js' -import { InputText } from '../../types/misc/entities.js' -import { InputFileLike } from '../files/index.js' -import { InputPeerLike } from '../peers/index.js' -import { VenueSource } from './venue.js' +import { MaybeArray } from '../../../../types/utils.js' +import { InputText } from '../../../types/misc/entities.js' +import { InputFileLike } from '../../files/index.js' +import { InputPeerLike } from '../../peers/index.js' +import { VenueSource } from '../venue.js' export interface CaptionMixin { /** @@ -607,284 +607,3 @@ export type InputMediaLike = | InputMediaStory | InputMediaWebpage | tl.TypeInputMedia - -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace InputMedia { - /** Omit `type` and `file` from the given type */ - export type OmitTypeAndFile = Omit - - /** - * Create an animation to be sent - * - * @param file Animation - * @param params Additional parameters - */ - export function animation(file: InputFileLike, params: OmitTypeAndFile = {}): InputMediaVideo { - const ret = params as tl.Mutable - ret.type = 'video' - ret.file = file - ret.isAnimated = true - - return ret - } - - /** - * Create an audio to be sent - * - * @param file Audio file - * @param params Additional parameters - */ - export function audio(file: InputFileLike, params: OmitTypeAndFile = {}): InputMediaAudio { - const ret = params as tl.Mutable - ret.type = 'audio' - ret.file = file - - return ret - } - - /** - * Create an document to be sent - * - * @param file Document - * @param params Additional parameters - */ - export function document( - file: InputFileLike, - params: OmitTypeAndFile = {}, - ): InputMediaDocument { - const ret = params as tl.Mutable - ret.type = 'document' - ret.file = file - - return ret - } - - /** - * Create an photo to be sent - * - * @param file Photo - * @param params Additional parameters - */ - export function photo(file: InputFileLike, params: OmitTypeAndFile = {}): InputMediaPhoto { - const ret = params as tl.Mutable - ret.type = 'photo' - ret.file = file - - return ret - } - - /** - * Create an video to be sent - * - * @param file Video - * @param params Additional parameters - */ - export function video(file: InputFileLike, params: OmitTypeAndFile = {}): InputMediaVideo { - const ret = params as tl.Mutable - ret.type = 'video' - ret.file = file - - return ret - } - - /** - * Create a voice note to be sent - * - * @param file Voice note - * @param params Additional parameters - */ - export function voice(file: InputFileLike, params: OmitTypeAndFile = {}): InputMediaVoice { - const ret = params as tl.Mutable - ret.type = 'voice' - ret.file = file - - return ret - } - - /** - * Create a sticker to be sent - * - * @param file Sticker - * @param params Additional parameters - */ - export function sticker(file: InputFileLike, params: OmitTypeAndFile = {}): InputMediaSticker { - const ret = params as tl.Mutable - ret.type = 'sticker' - ret.file = file - - return ret - } - - /** - * Create a venue to be sent - * - * @param params Venue parameters - */ - export function venue(params: OmitTypeAndFile): InputMediaVenue { - const ret = params as tl.Mutable - ret.type = 'venue' - - return ret - } - - /** - * Create a geolocation to be sent - * - * @param latitude Latitude of the location - * @param longitude Longitude of the location - * @param params Additional parameters - */ - export function geo( - latitude: number, - longitude: number, - params: OmitTypeAndFile = {}, - ): InputMediaGeo { - const ret = params as tl.Mutable - ret.type = 'geo' - ret.latitude = latitude - ret.longitude = longitude - - return ret - } - - /** - * Create a live geolocation to be sent - * - * @param latitude Latitude of the current location - * @param longitude Longitude of the current location - * @param params Additional parameters - */ - export function geoLive( - latitude: number, - longitude: number, - params: OmitTypeAndFile = {}, - ): InputMediaGeoLive { - const ret = params as tl.Mutable - ret.type = 'geo_live' - ret.latitude = latitude - ret.longitude = longitude - - return ret - } - - /** - * Create a dice to be sent - * - * For convenience, known dice emojis are available - * as static members of {@link Dice}. - * - * @param emoji Emoji representing the dice - * @param params Additional parameters - */ - export function dice(emoji: string, params: CaptionMixin): InputMediaDice { - const ret = params as tl.Mutable - ret.type = 'dice' - - return ret - } - - /** - * Create a contact to be sent - * - * @param params Contact parameters - */ - export function contact(params: OmitTypeAndFile): InputMediaContact { - const ret = params as tl.Mutable - ret.type = 'contact' - - return ret - } - - /** - * Create a game to be sent - * - * @param game Game short name or TL object representing one - */ - export function game(game: string | tl.TypeInputGame): InputMediaGame { - return { - type: 'game', - game, - } - } - - /** - * Create an invoice to be sent - * - * @param params Invoice parameters - */ - export function invoice(params: OmitTypeAndFile): InputMediaInvoice { - const ret = params as tl.Mutable - ret.type = 'invoice' - - return ret - } - - /** - * Create a poll to be sent - * - * @param params Poll parameters - */ - export function poll(params: OmitTypeAndFile): InputMediaPoll { - const ret = params as tl.Mutable - ret.type = 'poll' - - return ret - } - - /** - * Create a quiz to be sent - * - * @param params Quiz parameters - */ - export function quiz(params: OmitTypeAndFile): InputMediaQuiz { - const ret = params as tl.Mutable - ret.type = 'quiz' - - return ret - } - - /** - * Create a story to be sent - * - * @param params Story parameters - */ - export function story(params: OmitTypeAndFile): InputMediaStory { - const ret = params as tl.Mutable - ret.type = 'story' - - return ret - } - - /** - * Create a webpage to be sent - * - * @param url Webpage URL - * @param params Additional parameters - */ - export function webpage(url: string, params: OmitTypeAndFile = {}): InputMediaWebpage { - const ret = params as tl.Mutable - ret.type = 'webpage' - ret.url = url - - return ret - } - - /** - * Create a document to be sent, which subtype - * is inferred automatically by file contents. - * - * Photo type is only inferred for reused files, - * newly uploaded photos with `auto` will be - * uploaded as a document - * - * @param file The media file - * @param params Additional parameters - */ - export function auto(file: InputFileLike, params: OmitTypeAndFile = {}): InputMediaAuto { - const ret = params as tl.Mutable - ret.type = 'auto' - ret.file = file - - return ret - } -} diff --git a/packages/core/src/highlevel/types/messages/message.ts b/packages/core/src/highlevel/types/messages/message.ts index 38d0e516..149755e8 100644 --- a/packages/core/src/highlevel/types/messages/message.ts +++ b/packages/core/src/highlevel/types/messages/message.ts @@ -6,7 +6,7 @@ import { getMarkedPeerId, toggleChannelIdMark } from '../../../utils/peer-utils. import { assertTypeIsNot } from '../../../utils/type-assertions.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' -import { BotKeyboard, ReplyMarkup } from '../bots/keyboards.js' +import { BotKeyboard, ReplyMarkup } from '../bots/keyboards/index.js' import { TextWithEntities } from '../misc/index.js' import { Chat } from '../peers/chat.js' import { parsePeer, Peer } from '../peers/peer.js' diff --git a/packages/core/src/highlevel/types/misc/index.ts b/packages/core/src/highlevel/types/misc/index.ts index 04c676ae..7c2b764f 100644 --- a/packages/core/src/highlevel/types/misc/index.ts +++ b/packages/core/src/highlevel/types/misc/index.ts @@ -1,5 +1,5 @@ export * from './app-config.js' export * from './entities.js' -export * from './input-privacy-rule.js' +export * from './input-privacy-rule/index.js' export * from './sticker-set.js' export * from './takeout-session.js' diff --git a/packages/core/src/highlevel/types/misc/input-privacy-rule.ts b/packages/core/src/highlevel/types/misc/input-privacy-rule.ts deleted file mode 100644 index 952fe85f..00000000 --- a/packages/core/src/highlevel/types/misc/input-privacy-rule.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* eslint-disable @typescript-eslint/no-namespace */ - -import { tl } from '@mtcute/tl' - -import { MaybeArray } from '../../../types/utils.js' -import { InputPeerLike } from '../peers/index.js' - -export interface InputPrivacyRuleUsers { - allow: boolean - users: InputPeerLike[] -} - -export interface InputPrivacyRuleChatParticipants { - allow: boolean - chats: InputPeerLike[] -} - -export type InputPrivacyRule = InputPrivacyRuleChatParticipants | InputPrivacyRuleUsers | tl.TypeInputPrivacyRule - -/** - * Helpers for creating {@link InputPrivacyRule}s - * - * @example - * ```typescript - * const rules = [ - * PrivacyRule.allow.all, - * PrivacyRule.disallow.users([123456789, 'username']), - * ] - * ``` - */ -export namespace PrivacyRule { - export namespace allow { - /** Allow all users */ - export const all: tl.RawInputPrivacyValueAllowAll = { _: 'inputPrivacyValueAllowAll' } - /** Allow only contacts */ - export const contacts: tl.RawInputPrivacyValueAllowContacts = { _: 'inputPrivacyValueAllowContacts' } - /** Allow only "close friends" list */ - export const closeFriends: tl.RawInputPrivacyValueAllowCloseFriends = { - _: 'inputPrivacyValueAllowCloseFriends', - } - - /** - * Allow only users specified in `users` - * - * @param users Users to allow - */ - export function users(users: MaybeArray): InputPrivacyRuleUsers { - return { - allow: true, - users: Array.isArray(users) ? users : [users], - } - } - - /** - * Allow only participants of chats specified in `chats` - * - * @param chats Chats to allow - */ - export function chatParticipants(chats: MaybeArray): InputPrivacyRuleChatParticipants { - return { - allow: true, - chats: Array.isArray(chats) ? chats : [chats], - } - } - } - - export namespace disallow { - /** Disallow all users */ - export const all: tl.RawInputPrivacyValueDisallowAll = { _: 'inputPrivacyValueDisallowAll' } - /** Disallow contacts */ - export const contacts: tl.RawInputPrivacyValueDisallowContacts = { _: 'inputPrivacyValueDisallowContacts' } - - /** - * Disallow users specified in `users` - * - * @param users Users to disallow - */ - export function users(users: MaybeArray): InputPrivacyRuleUsers { - return { - allow: false, - users: Array.isArray(users) ? users : [users], - } - } - - /** - * Disallow participants of chats specified in `chats` - * - * @param chats Chats to disallow - */ - export function chatParticipants(chats: MaybeArray): InputPrivacyRuleChatParticipants { - return { - allow: false, - chats: Array.isArray(chats) ? chats : [chats], - } - } - } -} diff --git a/packages/core/src/highlevel/types/misc/input-privacy-rule/allow.ts b/packages/core/src/highlevel/types/misc/input-privacy-rule/allow.ts new file mode 100644 index 00000000..8b779ebe --- /dev/null +++ b/packages/core/src/highlevel/types/misc/input-privacy-rule/allow.ts @@ -0,0 +1,38 @@ +import { tl } from '@mtcute/tl' + +import { MaybeArray } from '../../../../types/utils.js' +import { InputPeerLike } from '../../peers/peer.js' +import { InputPrivacyRuleChatParticipants, InputPrivacyRuleUsers } from './types.js' + +/** Allow all users */ +export const all: tl.RawInputPrivacyValueAllowAll = { _: 'inputPrivacyValueAllowAll' } +/** Allow only contacts */ +export const contacts: tl.RawInputPrivacyValueAllowContacts = { _: 'inputPrivacyValueAllowContacts' } +/** Allow only "close friends" list */ +export const closeFriends: tl.RawInputPrivacyValueAllowCloseFriends = { + _: 'inputPrivacyValueAllowCloseFriends', +} + +/** + * Allow only users specified in `users` + * + * @param users Users to allow + */ +export function users(users: MaybeArray): InputPrivacyRuleUsers { + return { + allow: true, + users: Array.isArray(users) ? users : [users], + } +} + +/** + * Allow only participants of chats specified in `chats` + * + * @param chats Chats to allow + */ +export function chatParticipants(chats: MaybeArray): InputPrivacyRuleChatParticipants { + return { + allow: true, + chats: Array.isArray(chats) ? chats : [chats], + } +} diff --git a/packages/core/src/highlevel/types/misc/input-privacy-rule/bundle.ts b/packages/core/src/highlevel/types/misc/input-privacy-rule/bundle.ts new file mode 100644 index 00000000..31579066 --- /dev/null +++ b/packages/core/src/highlevel/types/misc/input-privacy-rule/bundle.ts @@ -0,0 +1,4 @@ +import * as allow from './allow.js' +import * as disallow from './disallow.js' + +export { allow, disallow } diff --git a/packages/core/src/highlevel/types/misc/input-privacy-rule/disallow.ts b/packages/core/src/highlevel/types/misc/input-privacy-rule/disallow.ts new file mode 100644 index 00000000..07bbb2ef --- /dev/null +++ b/packages/core/src/highlevel/types/misc/input-privacy-rule/disallow.ts @@ -0,0 +1,34 @@ +import { tl } from '@mtcute/tl' + +import { MaybeArray } from '../../../../types/utils.js' +import { InputPeerLike } from '../../peers/peer.js' +import { InputPrivacyRuleChatParticipants, InputPrivacyRuleUsers } from './types.js' + +/** Disallow all users */ +export const all: tl.RawInputPrivacyValueDisallowAll = { _: 'inputPrivacyValueDisallowAll' } +/** Disallow contacts */ +export const contacts: tl.RawInputPrivacyValueDisallowContacts = { _: 'inputPrivacyValueDisallowContacts' } + +/** + * Disallow users specified in `users` + * + * @param users Users to disallow + */ +export function users(users: MaybeArray): InputPrivacyRuleUsers { + return { + allow: false, + users: Array.isArray(users) ? users : [users], + } +} + +/** + * Disallow participants of chats specified in `chats` + * + * @param chats Chats to disallow + */ +export function chatParticipants(chats: MaybeArray): InputPrivacyRuleChatParticipants { + return { + allow: false, + chats: Array.isArray(chats) ? chats : [chats], + } +} diff --git a/packages/core/src/highlevel/types/misc/input-privacy-rule/index.ts b/packages/core/src/highlevel/types/misc/input-privacy-rule/index.ts new file mode 100644 index 00000000..8288220c --- /dev/null +++ b/packages/core/src/highlevel/types/misc/input-privacy-rule/index.ts @@ -0,0 +1,4 @@ +import * as PrivacyRule from './bundle.js' + +export { PrivacyRule } +export * from './types.js' diff --git a/packages/core/src/highlevel/types/misc/input-privacy-rule/types.ts b/packages/core/src/highlevel/types/misc/input-privacy-rule/types.ts new file mode 100644 index 00000000..4881b30e --- /dev/null +++ b/packages/core/src/highlevel/types/misc/input-privacy-rule/types.ts @@ -0,0 +1,15 @@ +import { tl } from '@mtcute/tl' + +import { InputPeerLike } from '../../peers/peer.js' + +export interface InputPrivacyRuleUsers { + allow: boolean + users: InputPeerLike[] +} + +export interface InputPrivacyRuleChatParticipants { + allow: boolean + chats: InputPeerLike[] +} + +export type InputPrivacyRule = InputPrivacyRuleChatParticipants | InputPrivacyRuleUsers | tl.TypeInputPrivacyRule diff --git a/packages/file-id/src/types-inner.ts b/packages/file-id/src/types-inner.ts new file mode 100644 index 00000000..6d2b5c03 --- /dev/null +++ b/packages/file-id/src/types-inner.ts @@ -0,0 +1,232 @@ +import Long from 'long' + +export const PERSISTENT_ID_VERSION_OLD = 2 +export const PERSISTENT_ID_VERSION = 4 + +export const WEB_LOCATION_FLAG = 1 << 24 +export const FILE_REFERENCE_FLAG = 1 << 25 + +export const CURRENT_VERSION = 48 + +/** + * An error occurred while parsing or serializing a File ID + */ +export class FileIdError extends Error {} + +/** + * A newer version of File ID is provided, which is + * currently not supported by the library. + * + * Feel free to open an issue on Github! + */ +export class UnsupportedError extends FileIdError {} + +/** + * File ID was invalid, meaning that something did not + * add up while parsing the file ID, or the file ID object + * contained invalid data. + */ +export class InvalidFileIdError extends FileIdError {} + +/** + * Provided File ID cannot be converted to that TL object. + */ +export class ConversionError extends FileIdError { + constructor(to: string) { + super(`Cannot convert given File ID to ${to}`) + } +} + +export enum FileType { + Thumbnail, + ProfilePhoto, + Photo, + VoiceNote, + Video, + Document, + Encrypted, + Temp, + Sticker, + Audio, + Animation, + EncryptedThumbnail, + Wallpaper, + VideoNote, + SecureRaw, + Secure, + Background, + DocumentAsFile, + Size, + None, +} + +// naming convention just like in @mtcute/tl + +// additionally, `_` discriminator is used, +// so we can interoperate with normal TL objects +// like InputFile just by checking `_` + +// for nested types, we don't bother with full type name +// for discriminator since it is really only used internally, +// so uniqueness is pretty much guaranteed + +/** + * This photo is a legacy photo that is + * represented simply by a secret number + */ +export interface RawPhotoSizeSourceLegacy { + readonly _: 'legacy' + readonly secret: Long +} + +/** + * This photo is a thumbnail, and its size + * is provided here as a one-letter string + */ +export interface RawPhotoSizeSourceThumbnail { + readonly _: 'thumbnail' + readonly fileType: FileType + readonly thumbnailType: string +} + +/** + * This photo is a profile photo of + * some peer, and their ID and access + * hash are provided here. + */ +export interface RawPhotoSizeSourceDialogPhoto { + readonly _: 'dialogPhoto' + readonly big: boolean + readonly id: number + readonly accessHash: Long +} + +/** + * This photo is a thumbnail for a a sticker set, + * and set's ID and access hash are provided here + */ +export interface RawPhotoSizeSourceStickerSetThumbnail { + readonly _: 'stickerSetThumbnail' + readonly id: Long + readonly accessHash: Long +} + +/** + * This photo is a legacy photo containing + * volume_id, local_id and secret + */ +export interface RawPhotoSizeSourceFullLegacy { + readonly _: 'fullLegacy' + readonly volumeId: Long + readonly localId: number + readonly secret: Long +} + +/** + * This photo is a legacy dialog photo + */ +export interface RawPhotoSizeSourceDialogPhotoLegacy extends Omit { + readonly _: 'dialogPhotoLegacy' + readonly volumeId: Long + readonly localId: number +} + +/** + * This photo is a legacy sticker set thumbnail + */ +export interface RawPhotoSizeSourceStickerSetThumbnailLegacy extends Omit { + readonly _: 'stickerSetThumbnailLegacy' + readonly volumeId: Long + readonly localId: number +} + +/** + * This photo is a legacy sticker set identified by a version + */ +export interface RawPhotoSizeSourceStickerSetThumbnailVersion extends Omit { + readonly _: 'stickerSetThumbnailVersion' + readonly version: number +} + +export type TypePhotoSizeSource = + | RawPhotoSizeSourceLegacy + | RawPhotoSizeSourceThumbnail + | RawPhotoSizeSourceDialogPhoto + | RawPhotoSizeSourceStickerSetThumbnail + | RawPhotoSizeSourceFullLegacy + | RawPhotoSizeSourceDialogPhotoLegacy + | RawPhotoSizeSourceStickerSetThumbnailLegacy + | RawPhotoSizeSourceStickerSetThumbnailVersion + +/** + * An external web file + */ +export interface RawWebRemoteFileLocation { + readonly _: 'web' + readonly url: string + readonly accessHash: Long +} + +/** + * A photo, that, in addition to ID and access + * hash, has its own `source` and detailed + * information about photo location on the + * servers. + */ +export interface RawPhotoRemoteFileLocation { + readonly _: 'photo' + readonly id: Long + readonly accessHash: Long + readonly source: TypePhotoSizeSource +} + +/** + * A common file that is represented as a pair + * of ID and access hash + */ +export interface RawCommonRemoteFileLocation { + readonly _: 'common' + readonly id: Long + readonly accessHash: Long +} + +export type TypeRemoteFileLocation = RawWebRemoteFileLocation | RawPhotoRemoteFileLocation | RawCommonRemoteFileLocation + +/** + * An object representing information about + * file location, that was either parsed from + * TDLib compatible File ID, or will be parsed + * to one. + * + * This type is supposed to be an intermediate step + * between TL objects and string file IDs, + * and if you are using `@mtcute/client`, you don't + * really need to care about this type at all. + */ +export interface RawFullRemoteFileLocation { + readonly _: 'remoteFileLocation' + + /** + * DC ID where this file is located + */ + readonly dcId: number + /** + * Type of the file + */ + readonly type: FileType + /** + * File reference (if any) + */ + readonly fileReference: Uint8Array | null + /** + * Context of the file location + */ + readonly location: TypeRemoteFileLocation +} + +export function isFileIdLike(obj: unknown): obj is string | RawFullRemoteFileLocation { + return ( + typeof obj === 'string' || + (obj !== null && typeof obj === 'object' && (obj as { _: unknown })._ === 'remoteFileLocation') + ) +} diff --git a/packages/file-id/src/types.ts b/packages/file-id/src/types.ts index 924f1e1a..2af0b1b2 100644 --- a/packages/file-id/src/types.ts +++ b/packages/file-id/src/types.ts @@ -1,240 +1,2 @@ -import Long from 'long' - -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace tdFileId { - export const PERSISTENT_ID_VERSION_OLD = 2 - export const PERSISTENT_ID_VERSION = 4 - - export const WEB_LOCATION_FLAG = 1 << 24 - export const FILE_REFERENCE_FLAG = 1 << 25 - - export const CURRENT_VERSION = 48 - - /** - * An error occurred while parsing or serializing a File ID - */ - export class FileIdError extends Error {} - - /** - * A newer version of File ID is provided, which is - * currently not supported by the library. - * - * Feel free to open an issue on Github! - */ - export class UnsupportedError extends FileIdError {} - - /** - * File ID was invalid, meaning that something did not - * add up while parsing the file ID, or the file ID object - * contained invalid data. - */ - export class InvalidFileIdError extends FileIdError {} - - /** - * Provided File ID cannot be converted to that TL object. - */ - export class ConversionError extends FileIdError { - constructor(to: string) { - super(`Cannot convert given File ID to ${to}`) - } - } - - export enum FileType { - Thumbnail, - ProfilePhoto, - Photo, - VoiceNote, - Video, - Document, - Encrypted, - Temp, - Sticker, - Audio, - Animation, - EncryptedThumbnail, - Wallpaper, - VideoNote, - SecureRaw, - Secure, - Background, - DocumentAsFile, - Size, - None, - } - - // naming convention just like in @mtcute/tl - - // additionally, `_` discriminator is used, - // so we can interoperate with normal TL objects - // like InputFile just by checking `_` - - // for nested types, we don't bother with full type name - // for discriminator since it is really only used internally, - // so uniqueness is pretty much guaranteed - - /** - * This photo is a legacy photo that is - * represented simply by a secret number - */ - export interface RawPhotoSizeSourceLegacy { - readonly _: 'legacy' - readonly secret: Long - } - - /** - * This photo is a thumbnail, and its size - * is provided here as a one-letter string - */ - export interface RawPhotoSizeSourceThumbnail { - readonly _: 'thumbnail' - readonly fileType: FileType - readonly thumbnailType: string - } - - /** - * This photo is a profile photo of - * some peer, and their ID and access - * hash are provided here. - */ - export interface RawPhotoSizeSourceDialogPhoto { - readonly _: 'dialogPhoto' - readonly big: boolean - readonly id: number - readonly accessHash: Long - } - - /** - * This photo is a thumbnail for a a sticker set, - * and set's ID and access hash are provided here - */ - export interface RawPhotoSizeSourceStickerSetThumbnail { - readonly _: 'stickerSetThumbnail' - readonly id: Long - readonly accessHash: Long - } - - /** - * This photo is a legacy photo containing - * volume_id, local_id and secret - */ - export interface RawPhotoSizeSourceFullLegacy { - readonly _: 'fullLegacy' - readonly volumeId: Long - readonly localId: number - readonly secret: Long - } - - /** - * This photo is a legacy dialog photo - */ - export interface RawPhotoSizeSourceDialogPhotoLegacy extends Omit { - readonly _: 'dialogPhotoLegacy' - readonly volumeId: Long - readonly localId: number - } - - /** - * This photo is a legacy sticker set thumbnail - */ - export interface RawPhotoSizeSourceStickerSetThumbnailLegacy - extends Omit { - readonly _: 'stickerSetThumbnailLegacy' - readonly volumeId: Long - readonly localId: number - } - - /** - * This photo is a legacy sticker set identified by a version - */ - export interface RawPhotoSizeSourceStickerSetThumbnailVersion - extends Omit { - readonly _: 'stickerSetThumbnailVersion' - readonly version: number - } - - export type TypePhotoSizeSource = - | RawPhotoSizeSourceLegacy - | RawPhotoSizeSourceThumbnail - | RawPhotoSizeSourceDialogPhoto - | RawPhotoSizeSourceStickerSetThumbnail - | RawPhotoSizeSourceFullLegacy - | RawPhotoSizeSourceDialogPhotoLegacy - | RawPhotoSizeSourceStickerSetThumbnailLegacy - | RawPhotoSizeSourceStickerSetThumbnailVersion - - /** - * An external web file - */ - export interface RawWebRemoteFileLocation { - readonly _: 'web' - readonly url: string - readonly accessHash: Long - } - - /** - * A photo, that, in addition to ID and access - * hash, has its own `source` and detailed - * information about photo location on the - * servers. - */ - export interface RawPhotoRemoteFileLocation { - readonly _: 'photo' - readonly id: Long - readonly accessHash: Long - readonly source: TypePhotoSizeSource - } - - /** - * A common file that is represented as a pair - * of ID and access hash - */ - export interface RawCommonRemoteFileLocation { - readonly _: 'common' - readonly id: Long - readonly accessHash: Long - } - - export type TypeRemoteFileLocation = - | RawWebRemoteFileLocation - | RawPhotoRemoteFileLocation - | RawCommonRemoteFileLocation - - /** - * An object representing information about - * file location, that was either parsed from - * TDLib compatible File ID, or will be parsed - * to one. - * - * This type is supposed to be an intermediate step - * between TL objects and string file IDs, - * and if you are using `@mtcute/client`, you don't - * really need to care about this type at all. - */ - export interface RawFullRemoteFileLocation { - readonly _: 'remoteFileLocation' - - /** - * DC ID where this file is located - */ - readonly dcId: number - /** - * Type of the file - */ - readonly type: FileType - /** - * File reference (if any) - */ - readonly fileReference: Uint8Array | null - /** - * Context of the file location - */ - readonly location: TypeRemoteFileLocation - } - - export function isFileIdLike(obj: unknown): obj is string | RawFullRemoteFileLocation { - return ( - typeof obj === 'string' || - (obj !== null && typeof obj === 'object' && (obj as { _: unknown })._ === 'remoteFileLocation') - ) - } -} +import * as tdFileId from './types-inner.js' +export { tdFileId }