From 0cee1b8037b4d7cfea8fb8d97877207f6dff93d6 Mon Sep 17 00:00:00 2001
From: Alina Sireneva
Date: Thu, 7 Mar 2024 13:07:08 +0300
Subject: [PATCH 1/2] docs: update readme
---
README.md | 25 ++++++++++++++++++++-----
1 file changed, 20 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 91981165..a36ee97d 100644
--- a/README.md
+++ b/README.md
@@ -73,11 +73,11 @@ learn more: [guide](https://mtcute.dev/guide/)
## features
-- 🍰 **simple**: mtcute hides all the complexity and provides a clean and modern API
-- ✨ **compatible**: mtcute supports almost everything Bot API does, and even more!
-- 🍡 **lightweight**: Running instance uses less than 50 MB of RAM.
-- 🛡️ **type-safe**: Most of the APIs (including MTProto) are strictly typed to help your workflow
-- ⚙️ **hackable**: Almost every aspect of the library is customizable, including networking and storage
+- 🍰 **simple**: mtcute hides all the complexity and provides a clean and modern api
+- ✨ **compatible**: mtcute supports almost everything bot api does, and even more!
+- 🍡 **lightweight**: running instance uses less than 50 mb of ram
+- 🛡️ **type-safe**: most of the apis (including mtproto) are strictly typed to help your workflow
+- ⚙️ **hackable**: almost every aspect of the library is customizable, including networking and storage
- 🕙 **up-to-date**: mtcute uses the latest TL schema to provide the newest features as soon as possible
## cat in the readme 🐈
@@ -86,6 +86,21 @@ learn more: [guide](https://mtcute.dev/guide/)
+## project goals
+
+mtcute strives to:
+- be customizable enough to fit most of the possible use-cases
+- be lightweight, both in terms of runtime and bundle size
+- support (theoretically) any environment without much hassle
+- provide a solid foundation for all kinds of applications on telegram platform
+- provide a convenient high-level api for the most commonly used features
+
+mtcute is **NOT** and will never be:
+- a library for spam/flood or otherwise malicious activities
+- a fully feature-complete library - highlevel apis will never cover the entirety of the apis. feel free to contribute, though!
+- a drop-in replacement for (insert library name)
+- a teapot
+
## setting up for development
```bash
--
2.45.2
From a0b3e9cc6e81b922bad57b64abdcd8163a5dacec Mon Sep 17 00:00:00 2001
From: Alina Sireneva
Date: Thu, 7 Mar 2024 18:11:02 +0300
Subject: [PATCH 2/2] 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 }
--
2.45.2