@effect-ak/tg-bot-client 0.6.4 → 1.1.0

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/README.md ADDED
@@ -0,0 +1,294 @@
1
+ # @effect-ak/tg-bot-client
2
+
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)
7
+
8
+ Type-safe HTTP client for Telegram Bot API with complete TypeScript support.
9
+
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
+ ## Installation
49
+
50
+ ```bash
51
+ npm install @effect-ak/tg-bot-client
52
+ ```
53
+
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
+ ## Quick Start
63
+
64
+ ```typescript
65
+ import { makeTgBotClient } from "@effect-ak/tg-bot-client"
66
+
67
+ const client = makeTgBotClient({
68
+ bot_token: "YOUR_BOT_TOKEN" // Token from @BotFather
69
+ })
70
+
71
+ // Send a message
72
+ await client.execute("send_message", {
73
+ chat_id: "123456789",
74
+ text: "Hello, World!"
75
+ })
76
+ ```
77
+
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:
283
+
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
287
+
288
+ ## Contributing
289
+
290
+ Contributions are welcome! Please check out the [issues](https://github.com/effect-ak/tg-bot-client/issues) or submit a pull request.
291
+
292
+ ## License
293
+
294
+ MIT © [Aleksandr Kondaurov](https://github.com/effect-ak)
package/dist/index.cjs ADDED
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ MESSAGE_EFFECTS: () => MESSAGE_EFFECTS,
24
+ TG_BOT_API_URL: () => TG_BOT_API_URL,
25
+ TgBotClientError: () => TgBotClientError,
26
+ executeTgBotMethod: () => executeTgBotMethod,
27
+ getBaseUrl: () => getBaseUrl,
28
+ getFile: () => getFile,
29
+ getFileBytes: () => getFileBytes,
30
+ isFileContent: () => isFileContent,
31
+ isMessageEffect: () => isMessageEffect,
32
+ isTgBotApiResponse: () => isTgBotApiResponse,
33
+ makePayload: () => makePayload,
34
+ makeTgBotClient: () => makeTgBotClient,
35
+ messageEffectIdCodes: () => messageEffectIdCodes,
36
+ snakeToCamel: () => snakeToCamel
37
+ });
38
+ module.exports = __toCommonJS(index_exports);
39
+
40
+ // src/errors.ts
41
+ var TgBotClientError = class extends Error {
42
+ _tag = "TgBotClientError";
43
+ cause;
44
+ constructor(options) {
45
+ super(`TgBotClientError: ${options.cause._tag}`);
46
+ this.cause = options.cause;
47
+ this.name = "TgBotClientError";
48
+ }
49
+ };
50
+
51
+ // src/guards.ts
52
+ var isFileContent = (input) => typeof input == "object" && input != null && "file_content" in input && input.file_content instanceof Uint8Array && "file_name" in input && typeof input.file_name == "string";
53
+ var isTgBotApiResponse = (input) => typeof input == "object" && input != null && "ok" in input && typeof input.ok == "boolean";
54
+
55
+ // src/const.ts
56
+ var TG_BOT_API_URL = "https://api.telegram.org";
57
+ var MESSAGE_EFFECTS = {
58
+ "\u{1F525}": "5104841245755180586",
59
+ "\u{1F44D}": "5107584321108051014",
60
+ "\u{1F44E}": "5104858069142078462",
61
+ "\u2764\uFE0F": "5159385139981059251",
62
+ "\u{1F389}": "5046509860389126442",
63
+ "\u{1F4A9}": "5046589136895476101"
64
+ };
65
+ var messageEffectIdCodes = Object.keys(
66
+ MESSAGE_EFFECTS
67
+ );
68
+ var isMessageEffect = (input) => {
69
+ return typeof input === "string" && input in MESSAGE_EFFECTS;
70
+ };
71
+
72
+ // src/config.ts
73
+ var getBaseUrl = (config) => {
74
+ return config?.baseUrl ?? TG_BOT_API_URL;
75
+ };
76
+
77
+ // src/utils.ts
78
+ var snakeToCamel = (str) => {
79
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
80
+ };
81
+
82
+ // src/execute.ts
83
+ async function executeTgBotMethod(params) {
84
+ const { config, method, input } = params;
85
+ const baseUrl = getBaseUrl(config);
86
+ const botToken = config.botToken;
87
+ let httpResponse;
88
+ try {
89
+ httpResponse = await fetch(
90
+ `${baseUrl}/bot${botToken}/${snakeToCamel(method)}`,
91
+ {
92
+ body: makePayload(input) ?? null,
93
+ method: "POST"
94
+ }
95
+ );
96
+ } catch (cause) {
97
+ throw new TgBotClientError({
98
+ cause: { _tag: "ClientInternalError", cause }
99
+ });
100
+ }
101
+ let response;
102
+ try {
103
+ response = await httpResponse.json();
104
+ } catch {
105
+ throw new TgBotClientError({
106
+ cause: { _tag: "NotJsonResponse", response: httpResponse }
107
+ });
108
+ }
109
+ if (!isTgBotApiResponse(response)) {
110
+ throw new TgBotClientError({
111
+ cause: { _tag: "UnexpectedResponse", response }
112
+ });
113
+ }
114
+ if (!httpResponse.ok) {
115
+ throw new TgBotClientError({
116
+ cause: {
117
+ _tag: "NotOkResponse",
118
+ ...response.error_code ? { errorCode: response.error_code } : {},
119
+ ...response.description ? { details: response.description } : {}
120
+ }
121
+ });
122
+ }
123
+ return response.result;
124
+ }
125
+ var makePayload = (body) => {
126
+ const entries = Object.entries(body);
127
+ if (entries.length == 0) return void 0;
128
+ const result = new FormData();
129
+ for (const [key, value] of entries) {
130
+ if (!value) continue;
131
+ if (typeof value != "object") {
132
+ result.append(key, `${value}`);
133
+ } else if (isFileContent(value)) {
134
+ result.append(key, new Blob([value.file_content]), value.file_name);
135
+ } else {
136
+ result.append(key, JSON.stringify(value));
137
+ }
138
+ }
139
+ return result;
140
+ };
141
+
142
+ // src/client-file.ts
143
+ var getFileBytes = async (fileId, context) => {
144
+ const { config, execute } = context;
145
+ const response = await execute("get_file", { file_id: fileId });
146
+ const file_path = response.file_path;
147
+ if (!file_path || file_path.length === 0) {
148
+ throw new TgBotClientError({
149
+ cause: {
150
+ _tag: "UnableToGetFile",
151
+ cause: "File path not defined"
152
+ }
153
+ });
154
+ }
155
+ const file_name = file_path.replaceAll("/", "-");
156
+ const baseUrl = getBaseUrl(config);
157
+ const botToken = config.botToken;
158
+ const url = `${baseUrl}/file/bot${botToken}/${file_path}`;
159
+ let content;
160
+ try {
161
+ content = await fetch(url).then((_) => _.arrayBuffer());
162
+ } catch (cause) {
163
+ throw new TgBotClientError({
164
+ cause: { _tag: "UnableToGetFile", cause }
165
+ });
166
+ }
167
+ const base64String = () => Buffer.from(content).toString("base64");
168
+ return {
169
+ content,
170
+ file_name,
171
+ base64String
172
+ };
173
+ };
174
+ var getFile = async (input, context) => {
175
+ const { content, file_name } = await getFileBytes(input.fileId, context);
176
+ return new File([content], file_name, {
177
+ ...input.type ? { type: input.type } : {}
178
+ });
179
+ };
180
+
181
+ // src/client.ts
182
+ function makeTgBotClient(config) {
183
+ const tgConfig = {
184
+ botToken: config.bot_token,
185
+ ...config.base_url ? { baseUrl: config.base_url } : {}
186
+ };
187
+ const execute = (method, input) => executeTgBotMethod({ config: tgConfig, method, input });
188
+ return {
189
+ execute,
190
+ getFile: (input) => getFile(input, { config: tgConfig, execute })
191
+ };
192
+ }
193
+ // Annotate the CommonJS export names for ESM import in node:
194
+ 0 && (module.exports = {
195
+ MESSAGE_EFFECTS,
196
+ TG_BOT_API_URL,
197
+ TgBotClientError,
198
+ executeTgBotMethod,
199
+ getBaseUrl,
200
+ getFile,
201
+ getFileBytes,
202
+ isFileContent,
203
+ isMessageEffect,
204
+ isTgBotApiResponse,
205
+ makePayload,
206
+ makeTgBotClient,
207
+ messageEffectIdCodes,
208
+ snakeToCamel
209
+ });
@@ -0,0 +1,98 @@
1
+ import { Api } from '@effect-ak/tg-bot-api';
2
+
3
+ interface TgBotConfig {
4
+ botToken: string;
5
+ baseUrl?: string;
6
+ }
7
+ declare const getBaseUrl: (config?: Pick<TgBotConfig, "baseUrl">) => string;
8
+
9
+ declare function executeTgBotMethod<M extends keyof Api>(params: {
10
+ config: TgBotConfig;
11
+ method: M;
12
+ input: Parameters<Api[M]>[0];
13
+ }): Promise<ReturnType<Api[M]>>;
14
+ declare const makePayload: (body: object) => FormData | undefined;
15
+
16
+ interface GetFile {
17
+ fileId: string;
18
+ type?: string;
19
+ }
20
+ interface FileBytes {
21
+ content: ArrayBuffer;
22
+ file_name: string;
23
+ base64String: () => string;
24
+ }
25
+ interface FileContext {
26
+ config: TgBotConfig;
27
+ execute: <M extends keyof Api>(method: M, input: Parameters<Api[M]>[0]) => Promise<ReturnType<Api[M]>>;
28
+ }
29
+ declare const getFileBytes: (fileId: string, context: FileContext) => Promise<FileBytes>;
30
+ declare const getFile: (input: GetFile, context: FileContext) => Promise<File>;
31
+
32
+ interface TgBotClient {
33
+ readonly execute: <M extends keyof Api>(method: M, input: Parameters<Api[M]>[0]) => Promise<ReturnType<Api[M]>>;
34
+ readonly getFile: (input: GetFile) => Promise<File>;
35
+ }
36
+ interface MakeTgClient {
37
+ bot_token: string;
38
+ base_url?: string;
39
+ }
40
+ declare function makeTgBotClient(config: MakeTgClient): TgBotClient;
41
+
42
+ type ErrorReason = {
43
+ _tag: "NotOkResponse";
44
+ errorCode?: number;
45
+ details?: string;
46
+ } | {
47
+ _tag: "UnexpectedResponse";
48
+ response: unknown;
49
+ } | {
50
+ _tag: "ClientInternalError";
51
+ cause: unknown;
52
+ } | {
53
+ _tag: "UnableToGetFile";
54
+ cause: unknown;
55
+ } | {
56
+ _tag: "BotHandlerError";
57
+ cause: unknown;
58
+ } | {
59
+ _tag: "NotJsonResponse";
60
+ response: unknown;
61
+ };
62
+ declare class TgBotClientError extends Error {
63
+ readonly _tag = "TgBotClientError";
64
+ readonly cause: ErrorReason;
65
+ constructor(options: {
66
+ cause: ErrorReason;
67
+ });
68
+ }
69
+
70
+ interface FileContent {
71
+ file_content: Uint8Array<ArrayBuffer>;
72
+ file_name: string;
73
+ }
74
+ declare const isFileContent: (input: unknown) => input is FileContent;
75
+ interface TgBotApiResponseSchema {
76
+ ok: boolean;
77
+ error_code?: number;
78
+ description?: string;
79
+ result?: unknown;
80
+ }
81
+ declare const isTgBotApiResponse: (input: unknown) => input is TgBotApiResponseSchema;
82
+
83
+ declare const TG_BOT_API_URL = "https://api.telegram.org";
84
+ declare const MESSAGE_EFFECTS: {
85
+ readonly "\uD83D\uDD25": "5104841245755180586";
86
+ readonly "\uD83D\uDC4D": "5107584321108051014";
87
+ readonly "\uD83D\uDC4E": "5104858069142078462";
88
+ readonly "\u2764\uFE0F": "5159385139981059251";
89
+ readonly "\uD83C\uDF89": "5046509860389126442";
90
+ readonly "\uD83D\uDCA9": "5046589136895476101";
91
+ };
92
+ type MessageEffect = keyof typeof MESSAGE_EFFECTS;
93
+ declare const messageEffectIdCodes: MessageEffect[];
94
+ declare const isMessageEffect: (input: unknown) => input is MessageEffect;
95
+
96
+ declare const snakeToCamel: (str: string) => string;
97
+
98
+ export { type FileBytes, type FileContent, type GetFile, MESSAGE_EFFECTS, type MakeTgClient, type MessageEffect, TG_BOT_API_URL, type TgBotApiResponseSchema, type TgBotClient, TgBotClientError, type TgBotConfig, executeTgBotMethod, getBaseUrl, getFile, getFileBytes, isFileContent, isMessageEffect, isTgBotApiResponse, makePayload, makeTgBotClient, messageEffectIdCodes, snakeToCamel };