@effect-ak/tg-bot-client 1.0.0 → 1.2.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 CHANGED
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,52 +15,42 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
 
30
20
  // src/index.ts
31
21
  var index_exports = {};
32
22
  __export(index_exports, {
33
- ClientFileService: () => ClientFileService,
34
23
  MESSAGE_EFFECTS: () => MESSAGE_EFFECTS,
35
24
  TG_BOT_API_URL: () => TG_BOT_API_URL,
36
- TgBotApiBaseUrl: () => TgBotApiBaseUrl,
37
- TgBotApiToken: () => TgBotApiToken,
38
25
  TgBotClientError: () => TgBotClientError,
39
26
  executeTgBotMethod: () => executeTgBotMethod,
27
+ getBaseUrl: () => getBaseUrl,
28
+ getFile: () => getFile,
29
+ getFileBytes: () => getFileBytes,
40
30
  isFileContent: () => isFileContent,
41
31
  isMessageEffect: () => isMessageEffect,
42
32
  isTgBotApiResponse: () => isTgBotApiResponse,
43
- isTgBotApiUpdate: () => isTgBotApiUpdate,
44
33
  makePayload: () => makePayload,
45
34
  makeTgBotClient: () => makeTgBotClient,
46
- messageEffectIdCodes: () => messageEffectIdCodes
35
+ messageEffectIdCodes: () => messageEffectIdCodes,
36
+ snakeToCamel: () => snakeToCamel
47
37
  });
48
38
  module.exports = __toCommonJS(index_exports);
49
39
 
50
- // src/execute.ts
51
- var String = __toESM(require("effect/String"), 1);
52
- var Micro = __toESM(require("effect/Micro"), 1);
53
-
54
40
  // src/errors.ts
55
- var Data = __toESM(require("effect/Data"), 1);
56
- var TgBotClientError = class extends Data.TaggedError("TgBotClientError") {
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
+ }
57
49
  };
58
50
 
59
51
  // src/guards.ts
60
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";
61
53
  var isTgBotApiResponse = (input) => typeof input == "object" && input != null && "ok" in input && typeof input.ok == "boolean";
62
- var isTgBotApiUpdate = (input) => typeof input == "object" && input != null && "update_id" in input && typeof input.update_id == "number";
63
-
64
- // src/config.ts
65
- var Context = __toESM(require("effect/Context"), 1);
66
54
 
67
55
  // src/const.ts
68
56
  var TG_BOT_API_URL = "https://api.telegram.org";
@@ -82,53 +70,58 @@ var isMessageEffect = (input) => {
82
70
  };
83
71
 
84
72
  // src/config.ts
85
- var TgBotApiBaseUrl = class extends Context.Reference()(
86
- "TgBotApiBaseUrl",
87
- { defaultValue: () => TG_BOT_API_URL }
88
- ) {
73
+ var getBaseUrl = (config) => {
74
+ return config?.baseUrl ?? TG_BOT_API_URL;
89
75
  };
90
- var TgBotApiToken = class extends Context.Tag("TgBotApiToken")() {
76
+
77
+ // src/utils.ts
78
+ var snakeToCamel = (str) => {
79
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
91
80
  };
92
81
 
93
82
  // src/execute.ts
94
- var executeTgBotMethod = (method, input) => Micro.gen(function* () {
95
- const botToken = yield* Micro.service(TgBotApiToken);
96
- const baseUrl = yield* Micro.service(TgBotApiBaseUrl);
97
- const httpResponse = yield* Micro.tryPromise({
98
- try: () => fetch(`${baseUrl}/bot${botToken}/${String.snakeToCamel(method)}`, {
99
- body: makePayload(input) ?? null,
100
- method: "POST"
101
- }),
102
- catch: (cause) => new TgBotClientError({
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({
103
98
  cause: { _tag: "ClientInternalError", cause }
104
- })
105
- });
106
- const response = yield* Micro.tryPromise({
107
- try: () => httpResponse.json(),
108
- catch: () => new TgBotClientError({
99
+ });
100
+ }
101
+ let response;
102
+ try {
103
+ response = await httpResponse.json();
104
+ } catch {
105
+ throw new TgBotClientError({
109
106
  cause: { _tag: "NotJsonResponse", response: httpResponse }
110
- })
111
- });
107
+ });
108
+ }
112
109
  if (!isTgBotApiResponse(response)) {
113
- return yield* Micro.fail(
114
- new TgBotClientError({
115
- cause: { _tag: "UnexpectedResponse", response }
116
- })
117
- );
110
+ throw new TgBotClientError({
111
+ cause: { _tag: "UnexpectedResponse", response }
112
+ });
118
113
  }
119
114
  if (!httpResponse.ok) {
120
- return yield* Micro.fail(
121
- new TgBotClientError({
122
- cause: {
123
- _tag: "NotOkResponse",
124
- ...response.error_code ? { errorCode: response.error_code } : void 0,
125
- ...response.description ? { details: response.description } : void 0
126
- }
127
- })
128
- );
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
+ });
129
122
  }
130
123
  return response.result;
131
- });
124
+ }
132
125
  var makePayload = (body) => {
133
126
  const entries = Object.entries(body);
134
127
  if (entries.length == 0) return void 0;
@@ -147,84 +140,70 @@ var makePayload = (body) => {
147
140
  };
148
141
 
149
142
  // src/client-file.ts
150
- var Micro2 = __toESM(require("effect/Micro"), 1);
151
- var Context2 = __toESM(require("effect/Context"), 1);
152
- var ClientFileService = class _ClientFileService extends Context2.Tag("ClientFileService")() {
153
- static live = () => {
154
- return _ClientFileService.context({
155
- getFile
156
- });
157
- };
158
- };
159
- var getFile = ({ fileId, type }) => getFileBytes(fileId).pipe(
160
- Micro2.andThen(
161
- ({ content, file_name }) => new File([content], file_name, {
162
- ...type ? { type } : void 0
163
- })
164
- )
165
- );
166
- var getFileBytes = (fileId) => Micro2.gen(function* () {
167
- const response = yield* executeTgBotMethod("get_file", { file_id: fileId });
143
+ var getFileBytes = async (fileId, context) => {
144
+ const { config, execute } = context;
145
+ const response = await execute("get_file", { file_id: fileId });
168
146
  const file_path = response.file_path;
169
- if (!file_path || file_path.length == 0) {
170
- return yield* Micro2.fail(
171
- new TgBotClientError({
172
- cause: {
173
- _tag: "UnableToGetFile",
174
- cause: "File path not defined"
175
- }
176
- })
177
- );
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
+ });
178
154
  }
179
155
  const file_name = file_path.replaceAll("/", "-");
180
- const baseUrl = yield* Micro2.service(TgBotApiBaseUrl);
181
- const botToken = yield* Micro2.service(TgBotApiToken);
156
+ const baseUrl = getBaseUrl(config);
157
+ const botToken = config.botToken;
182
158
  const url = `${baseUrl}/file/bot${botToken}/${file_path}`;
183
- const content = yield* Micro2.tryPromise({
184
- try: () => fetch(url).then((_) => _.arrayBuffer()),
185
- catch: (cause) => new TgBotClientError({
159
+ let content;
160
+ try {
161
+ content = await fetch(url).then((_) => _.arrayBuffer());
162
+ } catch (cause) {
163
+ throw new TgBotClientError({
186
164
  cause: { _tag: "UnableToGetFile", cause }
187
- })
188
- });
165
+ });
166
+ }
167
+ const base64String = () => Buffer.from(content).toString("base64");
189
168
  return {
190
169
  content,
191
- file_name
170
+ file_name,
171
+ base64String
192
172
  };
193
- });
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
+ };
194
180
 
195
181
  // src/client.ts
196
- var Micro3 = __toESM(require("effect/Micro"), 1);
197
- var Context3 = __toESM(require("effect/Context"), 1);
198
182
  function makeTgBotClient(config) {
199
- return createEffect(config).pipe(Micro3.runSync);
200
- }
201
- var createEffect = ({ bot_token }) => Micro3.gen(function* () {
202
- const file = yield* Micro3.service(ClientFileService);
203
- const context = Context3.make(TgBotApiToken, bot_token);
204
- const execute = (method, input) => executeTgBotMethod(method, input).pipe(
205
- Micro3.provideContext(context),
206
- Micro3.runPromise
207
- );
208
- const getFile2 = (input) => file.getFile(input).pipe(Micro3.provideContext(context), Micro3.runPromise);
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 });
209
188
  return {
210
189
  execute,
211
- getFile: getFile2
190
+ getFile: (input) => getFile(input, { config: tgConfig, execute })
212
191
  };
213
- }).pipe(Micro3.provideContext(ClientFileService.live()));
192
+ }
214
193
  // Annotate the CommonJS export names for ESM import in node:
215
194
  0 && (module.exports = {
216
- ClientFileService,
217
195
  MESSAGE_EFFECTS,
218
196
  TG_BOT_API_URL,
219
- TgBotApiBaseUrl,
220
- TgBotApiToken,
221
197
  TgBotClientError,
222
198
  executeTgBotMethod,
199
+ getBaseUrl,
200
+ getFile,
201
+ getFileBytes,
223
202
  isFileContent,
224
203
  isMessageEffect,
225
204
  isTgBotApiResponse,
226
- isTgBotApiUpdate,
227
205
  makePayload,
228
206
  makeTgBotClient,
229
- messageEffectIdCodes
207
+ messageEffectIdCodes,
208
+ snakeToCamel
230
209
  });