@effect-ak/tg-bot-client 1.3.4 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -2,20 +2,25 @@
2
2
  "name": "@effect-ak/tg-bot-client",
3
3
  "type": "module",
4
4
  "description": "Type-safe HTTP client for Telegram Bot API",
5
- "version": "1.3.4",
5
+ "version": "1.4.1",
6
6
  "license": "MIT",
7
+ "keywords": [
8
+ "telegram",
9
+ "telegram-bot",
10
+ "telegram-bot-api",
11
+ "http-client",
12
+ "typescript"
13
+ ],
7
14
  "author": {
8
15
  "name": "Aleksandr Kondaurov",
9
16
  "email": "kondaurov.dev@gmail.com"
10
17
  },
18
+ "homepage": "https://tg-bot-sdk.website",
11
19
  "repository": {
12
20
  "type": "git",
13
- "url": "https://github.com/kondaurovDev/tg-bot-client",
21
+ "url": "https://github.com/kondaurovDev/tg-bot-sdk",
14
22
  "directory": "packages/client"
15
23
  },
16
- "bugs": {
17
- "url": "https://github.com/effect-ak/tg-bot-client/issues"
18
- },
19
24
  "publishConfig": {
20
25
  "access": "public"
21
26
  },
@@ -27,10 +32,11 @@
27
32
  "types": "./dist/index.d.ts",
28
33
  "import": "./dist/index.js",
29
34
  "require": "./dist/index.cjs"
30
- }
35
+ },
36
+ "./dist/*": "./dist/*"
31
37
  },
32
38
  "dependencies": {
33
- "@effect-ak/tg-bot-api": "^1.3.0"
39
+ "@effect-ak/tg-bot-api": "^1.3.1"
34
40
  },
35
41
  "scripts": {
36
42
  "build": "tsup",
package/readme.md CHANGED
@@ -1,294 +1,43 @@
1
1
  # @effect-ak/tg-bot-client
2
2
 
3
3
  [![NPM Version](https://img.shields.io/npm/v/%40effect-ak%2Ftg-bot-client)](https://www.npmjs.com/package/@effect-ak/tg-bot-client)
4
- ![NPM Unpacked Size](https://img.shields.io/npm/unpacked-size/%40effect-ak%2Ftg-bot-client?link=)
5
- ![NPM Downloads](https://img.shields.io/npm/dw/%40effect-ak%2Ftg-bot-client?link=)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+ ![NPM Downloads](https://img.shields.io/npm/dw/%40effect-ak%2Ftg-bot-client)
7
5
 
8
6
  Type-safe HTTP client for Telegram Bot API with complete TypeScript support.
9
7
 
10
- ## Table of Contents
11
-
12
- - [Motivation](#motivation)
13
- - [Features](#features)
14
- - [Installation](#installation)
15
- - [Quick Start](#quick-start)
16
- - [Usage Examples](#usage-examples)
17
- - [Sending Messages](#sending-messages)
18
- - [Sending Files](#sending-files)
19
- - [Downloading Files](#downloading-files)
20
- - [Using Message Effects](#using-message-effects)
21
- - [Error Handling](#error-handling)
22
- - [API Reference](#api-reference)
23
- - [Configuration](#configuration)
24
- - [Related Packages](#related-packages)
25
- - [Contributing](#contributing)
26
- - [License](#license)
27
-
28
- ## Motivation
29
-
30
- **Telegram** does not offer an official TypeScript **SDK** for their **API** but they provide documentation in HTML format.
31
-
32
- This package provides a lightweight, type-safe HTTP client that uses types generated from the [official Telegram Bot API documentation](https://core.telegram.org/bots/api), ensuring it stays in sync with the latest API updates.
33
-
34
- ## Features
35
-
36
- - **Type-Safe**: Full TypeScript support with types generated from official documentation
37
- - **Lightweight**: Minimal dependencies
38
- - **Complete API Coverage**: All Bot API methods are supported
39
- - **Smart Type Conversion**: Automatic mapping of Telegram types to TypeScript:
40
- - `Integer` → `number`
41
- - `True` → `boolean`
42
- - `String or Number` → `string | number`
43
- - Enumerated types → Union of literal types (e.g., `"private" | "group" | "supergroup" | "channel"`)
44
- - **File Handling**: Simplified file upload and download
45
- - **Message Effects**: Built-in constants for message effects
46
- - **Custom Base URL**: Support for custom Telegram Bot API servers
47
-
48
8
  ## Installation
49
9
 
50
10
  ```bash
51
11
  npm install @effect-ak/tg-bot-client
52
12
  ```
53
13
 
54
- ```bash
55
- pnpm add @effect-ak/tg-bot-client
56
- ```
57
-
58
- ```bash
59
- yarn add @effect-ak/tg-bot-client
60
- ```
61
-
62
14
  ## Quick Start
63
15
 
64
16
  ```typescript
65
17
  import { makeTgBotClient } from "@effect-ak/tg-bot-client"
66
18
 
67
19
  const client = makeTgBotClient({
68
- bot_token: "YOUR_BOT_TOKEN" // Token from @BotFather
20
+ bot_token: "YOUR_BOT_TOKEN"
69
21
  })
70
22
 
71
- // Send a message
72
23
  await client.execute("send_message", {
73
24
  chat_id: "123456789",
74
25
  text: "Hello, World!"
75
26
  })
76
27
  ```
77
28
 
78
- ## Usage Examples
79
-
80
- ### Sending Messages
81
-
82
- #### Basic Text Message
83
-
84
- ```typescript
85
- await client.execute("send_message", {
86
- chat_id: "123456789",
87
- text: "Hello from TypeScript!"
88
- })
89
- ```
90
-
91
- #### Message with Formatting
92
-
93
- ```typescript
94
- await client.execute("send_message", {
95
- chat_id: "123456789",
96
- text: "*Bold* _italic_ `code`",
97
- parse_mode: "Markdown"
98
- })
99
- ```
100
-
101
- #### Message with Inline Keyboard
102
-
103
- ```typescript
104
- await client.execute("send_message", {
105
- chat_id: "123456789",
106
- text: "Choose an option:",
107
- reply_markup: {
108
- inline_keyboard: [
109
- [
110
- { text: "Option 1", callback_data: "opt_1" },
111
- { text: "Option 2", callback_data: "opt_2" }
112
- ]
113
- ]
114
- }
115
- })
116
- ```
117
-
118
- ### Sending Files
119
-
120
- #### Sending a Dice
121
-
122
- ```typescript
123
- await client.execute("send_dice", {
124
- chat_id: "123456789",
125
- emoji: "🎲"
126
- })
127
- ```
128
-
129
- #### Sending a Document
130
-
131
- ```typescript
132
- await client.execute("send_document", {
133
- chat_id: "123456789",
134
- document: {
135
- file_content: new TextEncoder().encode("Hello from file!"),
136
- file_name: "hello.txt"
137
- },
138
- caption: "Simple text file"
139
- })
140
- ```
141
-
142
- #### Sending a Photo
143
-
144
- ```typescript
145
- await client.execute("send_photo", {
146
- chat_id: "123456789",
147
- photo: {
148
- file_content: photoBuffer,
149
- file_name: "image.jpg"
150
- },
151
- caption: "Check out this photo!"
152
- })
153
- ```
154
-
155
- ### Downloading Files
156
-
157
- To download a file from Telegram servers, use the `getFile` method. It handles both the API call and file download in one step:
158
-
159
- ```typescript
160
- // Get file by file_id (from a message, for example)
161
- const file = await client.getFile({
162
- file_id: "AgACAgIAAxkBAAI..."
163
- })
164
-
165
- // file is a standard File object
166
- console.log(file.name) // filename.jpg
167
- console.log(file.size) // file size in bytes
168
-
169
- // Read file contents
170
- const arrayBuffer = await file.arrayBuffer()
171
- const text = await file.text()
172
- const blob = await file.blob()
173
- ```
174
-
175
- ### Using Message Effects
176
-
177
- The library includes built-in constants for message effects:
178
-
179
- ```typescript
180
- import { MESSAGE_EFFECTS } from "@effect-ak/tg-bot-client"
181
-
182
- await client.execute("send_message", {
183
- chat_id: "123456789",
184
- text: "Message with fire effect!",
185
- message_effect_id: MESSAGE_EFFECTS["🔥"]
186
- })
187
- ```
188
-
189
- Available effects:
190
- - `MESSAGE_EFFECTS["🔥"]` - Fire
191
- - `MESSAGE_EFFECTS["👍"]` - Thumbs up
192
- - `MESSAGE_EFFECTS["👎"]` - Thumbs down
193
- - `MESSAGE_EFFECTS["❤️"]` - Heart
194
- - `MESSAGE_EFFECTS["🎉"]` - Party
195
- - `MESSAGE_EFFECTS["💩"]` - Poop
196
-
197
- ## Error Handling
198
-
199
- The client throws `TgBotClientError` for all errors:
200
-
201
- ```typescript
202
- import { TgBotClientError } from "@effect-ak/tg-bot-client"
203
-
204
- try {
205
- await client.execute("send_message", {
206
- chat_id: "invalid",
207
- text: "Test"
208
- })
209
- } catch (error) {
210
- if (error instanceof TgBotClientError) {
211
- console.error("Error type:", error.cause._tag)
212
-
213
- switch (error.cause._tag) {
214
- case "NotOkResponse":
215
- console.error("API error:", error.cause.errorCode, error.cause.details)
216
- break
217
- case "UnexpectedResponse":
218
- console.error("Unexpected response:", error.cause.response)
219
- break
220
- case "ClientInternalError":
221
- console.error("Internal error:", error.cause.cause)
222
- break
223
- case "UnableToGetFile":
224
- console.error("File download error:", error.cause.cause)
225
- break
226
- case "NotJsonResponse":
227
- console.error("Invalid JSON response:", error.cause.response)
228
- break
229
- }
230
- }
231
- }
232
- ```
233
-
234
- ## API Reference
235
-
236
- ### `makeTgBotClient(config)`
237
-
238
- Creates a new Telegram Bot API client.
239
-
240
- **Parameters:**
241
- - `config.bot_token` (string, required): Your bot token from @BotFather
242
- - `config.base_url` (string, optional): Custom API base URL (default: `https://api.telegram.org`)
243
-
244
- **Returns:** `TgBotClient`
245
-
246
- ### `client.execute(method, input)`
247
-
248
- Executes a Telegram Bot API method.
249
-
250
- **Parameters:**
251
- - `method` (string): API method name in snake_case (e.g., `"send_message"`, `"get_updates"`)
252
- - `input` (object): Method parameters as defined in Telegram Bot API
253
-
254
- **Returns:** `Promise<ApiResponse>` - Typed response based on the method
255
-
256
- **Note:** Method names use snake_case (e.g., `send_message`) instead of camelCase for better readability and consistency with the Telegram API documentation.
257
-
258
- ### `client.getFile(input)`
259
-
260
- Downloads a file from Telegram servers.
261
-
262
- **Parameters:**
263
- - `input.file_id` (string): File identifier from a message
264
-
265
- **Returns:** `Promise<File>` - Standard [File](https://developer.mozilla.org/en-US/docs/Web/API/File) object
266
-
267
- ## Configuration
268
-
269
- ### Custom Base URL
270
-
271
- If you're running your own Telegram Bot API server:
272
-
273
- ```typescript
274
- const client = makeTgBotClient({
275
- bot_token: "YOUR_BOT_TOKEN",
276
- base_url: "https://your-custom-server.com"
277
- })
278
- ```
279
-
280
- ## Related Packages
281
-
282
- This package is part of the `tg-bot-client` monorepo:
29
+ ## Features
283
30
 
284
- - **[@effect-ak/tg-bot-api](../api)** - TypeScript types for Telegram Bot API and Mini Apps
285
- - **[@effect-ak/tg-bot](../bot)** - Effect-based bot runner for building Telegram bots
286
- - **[@effect-ak/tg-bot-codegen](../codegen)** - Code generator that parses official documentation
31
+ - **Type-Safe** full TypeScript support with types from official documentation
32
+ - **Lightweight** minimal dependencies
33
+ - **Complete API Coverage** all Bot API methods supported
34
+ - **File Handling** — simplified upload and download
35
+ - **Message Effects** — built-in constants
287
36
 
288
- ## Contributing
37
+ ## Documentation
289
38
 
290
- Contributions are welcome! Please check out the [issues](https://github.com/effect-ak/tg-bot-client/issues) or submit a pull request.
39
+ Full documentation, usage examples, and error handling: **[tg-bot-sdk.website](https://tg-bot-sdk.website)**
291
40
 
292
41
  ## License
293
42
 
294
- MIT © [Aleksandr Kondaurov](https://github.com/effect-ak)
43
+ MIT
package/src/client.ts CHANGED
@@ -1,35 +1,75 @@
1
- import type { Api } from "@effect-ak/tg-bot-api"
2
- import { executeTgBotMethod, type ExecuteMethod } from "./execute"
3
- import { getFile as getFileImpl, type TgFile } from "./client-file"
4
- import { TG_BOT_API_URL } from "./const"
1
+ import { executeTgBotMethod, type ExecuteMethod, type ClientResult } from "./execute"
5
2
 
6
- export interface TgBotClient {
7
- readonly config: Required<TgClientConfig>
8
- readonly execute: ExecuteMethod
9
- readonly getFile: (input: { fileId: string; type?: string }) => Promise<TgFile>
10
- }
3
+ const TG_BOT_API_URL = "https://api.telegram.org"
11
4
 
12
5
  export interface TgClientConfig {
13
6
  bot_token: string
14
7
  base_url?: string
15
8
  }
16
9
 
10
+ export interface TgFile {
11
+ readonly content: ArrayBuffer
12
+ readonly file_name: string
13
+ readonly base64String: () => string
14
+ readonly file: () => File
15
+ }
16
+
17
+ export interface TgBotClient {
18
+ readonly config: Required<TgClientConfig>
19
+ readonly execute: ExecuteMethod
20
+ readonly getFile: (input: { fileId: string; type?: string }) => Promise<ClientResult<TgFile>>
21
+ }
22
+
17
23
  export function makeTgBotClient(config: TgClientConfig): TgBotClient {
18
24
  const tgConfig = {
19
25
  bot_token: config.bot_token,
20
26
  base_url: config.base_url ?? TG_BOT_API_URL
21
27
  }
22
28
 
23
- const execute = <M extends keyof Api>(
24
- method: M,
25
- input: Parameters<Api[M]>[0]
26
- ) => executeTgBotMethod({
27
- config: tgConfig, method, input
28
- })
29
+ const execute: ExecuteMethod = (method, input) =>
30
+ executeTgBotMethod({
31
+ config: tgConfig, method, input: input as any
32
+ })
33
+
34
+ const getFile = async (params: { fileId: string; type?: string }): Promise<ClientResult<TgFile>> => {
35
+ const response = await execute("get_file", { file_id: params.fileId })
36
+
37
+ if (!response.ok) return response
38
+
39
+ const file_path = response.data.file_path
40
+
41
+ if (!file_path || file_path.length === 0) {
42
+ return {
43
+ ok: false,
44
+ error: { _tag: "UnableToGetFile", cause: "File path not defined" }
45
+ }
46
+ }
47
+
48
+ const file_name = file_path.replaceAll("/", "-")
49
+ const url = `${tgConfig.base_url}/file/bot${tgConfig.bot_token}/${file_path}`
50
+
51
+ let content: ArrayBuffer
52
+ try {
53
+ content = await fetch(url).then((_) => _.arrayBuffer())
54
+ } catch (cause) {
55
+ return { ok: false, error: { _tag: "UnableToGetFile", cause } }
56
+ }
57
+
58
+ const base64String = () => Buffer.from(content).toString("base64")
59
+ const file = () =>
60
+ new File([content], file_name, {
61
+ ...(params.type ? { type: params.type } : {})
62
+ })
63
+
64
+ return {
65
+ ok: true,
66
+ data: { content, file_name, base64String, file }
67
+ }
68
+ }
29
69
 
30
70
  return {
31
71
  config: tgConfig,
32
72
  execute,
33
- getFile: (input) => getFileImpl({ ...input, config: tgConfig, execute })
73
+ getFile
34
74
  }
35
75
  }
package/src/execute.ts CHANGED
@@ -1,14 +1,87 @@
1
1
  import type { Api } from "@effect-ak/tg-bot-api"
2
2
 
3
- import { TgBotClientError } from "./errors"
4
- import { isFileContent, isTgBotApiResponse } from "./guards"
5
- import { snakeToCamel } from "./utils"
6
3
  import type { TgClientConfig } from "./client"
7
4
 
5
+ // --- result ---
6
+
7
+ export type ClientErrorReason =
8
+ | { _tag: "NotOkResponse"; errorCode?: number; details?: string }
9
+ | { _tag: "UnexpectedResponse"; response: unknown }
10
+ | { _tag: "ClientInternalError"; cause: unknown }
11
+ | { _tag: "UnableToGetFile"; cause: unknown }
12
+ | { _tag: "BotHandlerError"; cause: unknown }
13
+ | { _tag: "NotJsonResponse"; response: unknown }
14
+
15
+ export type ClientResult<T> =
16
+ | { readonly ok: true; readonly data: T }
17
+ | { readonly ok: false; readonly error: ClientErrorReason }
18
+
19
+ // --- guards ---
20
+
21
+ export interface FileContent {
22
+ file_content: Uint8Array<ArrayBuffer>
23
+ file_name: string
24
+ }
25
+
26
+ const isFileContent = (input: unknown): input is FileContent =>
27
+ typeof input == "object" &&
28
+ input != null &&
29
+ "file_content" in input &&
30
+ input.file_content instanceof Uint8Array &&
31
+ "file_name" in input &&
32
+ typeof input.file_name == "string"
33
+
34
+ interface TgBotApiResponseSchema {
35
+ ok: boolean
36
+ error_code?: number
37
+ description?: string
38
+ result?: unknown
39
+ }
40
+
41
+ const isTgBotApiResponse = (
42
+ input: unknown
43
+ ): input is TgBotApiResponseSchema =>
44
+ typeof input == "object" &&
45
+ input != null &&
46
+ "ok" in input &&
47
+ typeof input.ok == "boolean"
48
+
49
+ // --- message effects ---
50
+
51
+ export const MESSAGE_EFFECTS = {
52
+ "🔥": "5104841245755180586",
53
+ "👍": "5107584321108051014",
54
+ "👎": "5104858069142078462",
55
+ "❤️": "5159385139981059251",
56
+ "🎉": "5046509860389126442",
57
+ "💩": "5046589136895476101"
58
+ } as const
59
+
60
+ export type MessageEffect = keyof typeof MESSAGE_EFFECTS
61
+
62
+ export const messageEffectIdCodes = Object.keys(
63
+ MESSAGE_EFFECTS
64
+ ) as MessageEffect[]
65
+
66
+ const isMessageEffect = (input: unknown): input is MessageEffect => {
67
+ return typeof input === "string" && input in MESSAGE_EFFECTS
68
+ }
69
+
70
+ // --- execute ---
71
+
72
+ const snakeToCamel = (str: string): string => {
73
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
74
+ }
75
+
76
+ type WithMessageEffect<T> =
77
+ T extends { message_effect_id?: string }
78
+ ? Omit<T, "message_effect_id"> & { message_effect_id?: MessageEffect | (string & {}) }
79
+ : T
80
+
8
81
  export type ExecuteMethod = <M extends keyof Api>(
9
82
  method: M,
10
- input: Parameters<Api[M]>[0]
11
- ) => Promise<ReturnType<Api[M]>>
83
+ input: WithMessageEffect<Parameters<Api[M]>[0]>
84
+ ) => Promise<ClientResult<ReturnType<Api[M]>>>
12
85
 
13
86
  export async function executeTgBotMethod<M extends keyof Api>(
14
87
  params: {
@@ -16,8 +89,13 @@ export async function executeTgBotMethod<M extends keyof Api>(
16
89
  method: M
17
90
  input: Parameters<Api[M]>[0]
18
91
  }
19
- ): Promise<ReturnType<Api[M]>> {
20
- const { config, method, input } = params
92
+ ): Promise<ClientResult<ReturnType<Api[M]>>> {
93
+ const { config, method } = params
94
+ let { input } = params
95
+
96
+ if ("message_effect_id" in input && isMessageEffect(input.message_effect_id)) {
97
+ input = { ...input, message_effect_id: MESSAGE_EFFECTS[input.message_effect_id] }
98
+ }
21
99
 
22
100
  let httpResponse: Response
23
101
  try {
@@ -29,37 +107,32 @@ export async function executeTgBotMethod<M extends keyof Api>(
29
107
  }
30
108
  )
31
109
  } catch (cause) {
32
- throw new TgBotClientError({
33
- cause: { _tag: "ClientInternalError", cause }
34
- })
110
+ return { ok: false, error: { _tag: "ClientInternalError", cause } }
35
111
  }
36
112
 
37
113
  let response: unknown
38
114
  try {
39
115
  response = await httpResponse.json()
40
116
  } catch {
41
- throw new TgBotClientError({
42
- cause: { _tag: "NotJsonResponse", response: httpResponse }
43
- })
117
+ return { ok: false, error: { _tag: "NotJsonResponse", response: httpResponse } }
44
118
  }
45
119
 
46
120
  if (!isTgBotApiResponse(response)) {
47
- throw new TgBotClientError({
48
- cause: { _tag: "UnexpectedResponse", response }
49
- })
121
+ return { ok: false, error: { _tag: "UnexpectedResponse", response } }
50
122
  }
51
123
 
52
124
  if (!httpResponse.ok) {
53
- throw new TgBotClientError({
54
- cause: {
125
+ return {
126
+ ok: false,
127
+ error: {
55
128
  _tag: "NotOkResponse",
56
129
  ...(response.error_code ? { errorCode: response.error_code } : {}),
57
130
  ...(response.description ? { details: response.description } : {})
58
131
  }
59
- })
132
+ }
60
133
  }
61
134
 
62
- return response.result as ReturnType<Api[M]>
135
+ return { ok: true, data: response.result as ReturnType<Api[M]> }
63
136
  }
64
137
 
65
138
  export const makePayload = (body: object): FormData | undefined => {
package/src/index.ts CHANGED
@@ -1,7 +1,24 @@
1
- export * from "./execute"
2
- export * from "./client-file"
3
- export * from "./client"
4
- export * from "./errors"
5
- export * from "./guards"
6
- export * from "./const"
7
- export * from "./utils"
1
+ export {
2
+ MESSAGE_EFFECTS,
3
+ messageEffectIdCodes,
4
+ makePayload,
5
+ executeTgBotMethod
6
+ } from "./execute"
7
+
8
+ export type {
9
+ ClientResult,
10
+ ClientErrorReason,
11
+ FileContent,
12
+ MessageEffect,
13
+ ExecuteMethod
14
+ } from "./execute"
15
+
16
+ export {
17
+ makeTgBotClient
18
+ } from "./client"
19
+
20
+ export type {
21
+ TgBotClient,
22
+ TgClientConfig,
23
+ TgFile
24
+ } from "./client"