@effect-ak/tg-bot-client 0.1.0 → 0.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.
Files changed (82) hide show
  1. package/dist/cjs/bot/factory/_service.js +20 -0
  2. package/dist/cjs/bot/factory/client-config.js +29 -0
  3. package/dist/cjs/bot/factory/make-bot.js +20 -0
  4. package/dist/cjs/bot/message-handler/_service.js +12 -0
  5. package/dist/cjs/bot/message-handler/types.js +5 -0
  6. package/dist/cjs/bot/message-handler/utils.js +19 -0
  7. package/dist/cjs/bot/run.js +10 -0
  8. package/dist/cjs/bot/update-poller/_service.js +39 -0
  9. package/dist/cjs/bot/update-poller/errors.js +5 -0
  10. package/dist/cjs/bot/update-poller/fetch-updates.js +70 -0
  11. package/dist/cjs/bot/update-poller/poll-and-handle.js +50 -0
  12. package/dist/cjs/bot/update-poller/settings.js +23 -0
  13. package/dist/cjs/client/_client.js +17 -14
  14. package/dist/cjs/client/config.js +17 -0
  15. package/dist/cjs/client/errors.js +8 -9
  16. package/dist/cjs/client/execute-request/_service.js +20 -0
  17. package/dist/cjs/client/execute-request/execute.js +59 -0
  18. package/dist/cjs/client/{request.js → execute-request/payload.js} +2 -2
  19. package/dist/cjs/client/file/_service.js +22 -0
  20. package/dist/cjs/client/file/get-file.js +38 -0
  21. package/dist/cjs/client/guards.js +12 -0
  22. package/dist/cjs/index.js +41 -8
  23. package/dist/dts/bot/factory/_service.d.ts +37 -0
  24. package/dist/dts/bot/factory/client-config.d.ts +3 -0
  25. package/dist/dts/bot/factory/make-bot.d.ts +7 -0
  26. package/dist/dts/bot/message-handler/_service.d.ts +6 -0
  27. package/dist/dts/bot/message-handler/types.d.ts +16 -0
  28. package/dist/dts/bot/message-handler/utils.d.ts +6 -0
  29. package/dist/dts/bot/run.d.ts +7 -0
  30. package/dist/dts/bot/update-poller/_service.d.ts +21 -0
  31. package/dist/dts/bot/update-poller/errors.d.ts +1 -0
  32. package/dist/dts/bot/update-poller/fetch-updates.d.ts +17 -0
  33. package/dist/dts/bot/update-poller/poll-and-handle.d.ts +16 -0
  34. package/dist/dts/bot/update-poller/settings.d.ts +6 -0
  35. package/dist/dts/client/_client.d.ts +6 -17
  36. package/dist/dts/client/config.d.ts +8 -0
  37. package/dist/dts/client/errors.d.ts +6 -4
  38. package/dist/dts/client/execute-request/_service.d.ts +15 -0
  39. package/dist/dts/client/execute-request/execute.d.ts +5 -0
  40. package/dist/dts/client/file/_service.d.ts +16 -0
  41. package/dist/dts/client/file/get-file.d.ts +5 -0
  42. package/dist/dts/client/guards.d.ts +17 -0
  43. package/dist/dts/index.d.ts +4 -1
  44. package/dist/dts/specification/api.d.ts +14 -14
  45. package/dist/dts/specification/types.d.ts +77 -77
  46. package/dist/esm/bot/factory/_service.js +12 -0
  47. package/dist/esm/bot/factory/client-config.js +20 -0
  48. package/dist/esm/bot/factory/make-bot.js +9 -0
  49. package/dist/esm/bot/message-handler/_service.js +4 -0
  50. package/dist/esm/bot/message-handler/types.js +1 -0
  51. package/dist/esm/bot/message-handler/utils.js +12 -0
  52. package/dist/esm/bot/run.js +6 -0
  53. package/dist/esm/bot/update-poller/_service.js +31 -0
  54. package/dist/esm/bot/update-poller/errors.js +1 -0
  55. package/dist/esm/bot/update-poller/fetch-updates.js +49 -0
  56. package/dist/esm/bot/update-poller/poll-and-handle.js +37 -0
  57. package/dist/esm/bot/update-poller/settings.js +15 -0
  58. package/dist/esm/client/_client.js +15 -14
  59. package/dist/esm/client/config.js +8 -0
  60. package/dist/esm/client/errors.js +6 -9
  61. package/dist/esm/client/execute-request/_service.js +12 -0
  62. package/dist/esm/client/execute-request/execute.js +37 -0
  63. package/dist/esm/client/{request.js → execute-request/payload.js} +2 -4
  64. package/dist/esm/client/file/_service.js +14 -0
  65. package/dist/esm/client/file/get-file.js +24 -0
  66. package/dist/esm/client/guards.js +7 -0
  67. package/dist/esm/index.js +4 -3
  68. package/package.json +13 -8
  69. package/readme.md +75 -25
  70. package/dist/cjs/client/download-file.js +0 -44
  71. package/dist/cjs/client/execute-request.js +0 -69
  72. package/dist/cjs/client/response.js +0 -22
  73. package/dist/dts/client/download-file.d.ts +0 -13
  74. package/dist/dts/client/execute-request.d.ts +0 -13
  75. package/dist/dts/client/response.d.ts +0 -7
  76. package/dist/esm/client/download-file.js +0 -36
  77. package/dist/esm/client/execute-request.js +0 -62
  78. package/dist/esm/client/response.js +0 -15
  79. /package/dist/cjs/{client/const.js → const.js} +0 -0
  80. /package/dist/dts/client/{request.d.ts → execute-request/payload.d.ts} +0 -0
  81. /package/dist/dts/{client/const.d.ts → const.d.ts} +0 -0
  82. /package/dist/esm/{client/const.js → const.js} +0 -0
@@ -0,0 +1,49 @@
1
+ import * as Micro from "effect/Micro";
2
+ import { extractUpdate } from "../../bot/message-handler/utils.js";
3
+ export const fetchUpdates = ({ state, settings, execute, handlers }) => Micro.gen(function* () {
4
+ const updateId = state.lastUpdateId;
5
+ console.info("getting updates", state);
6
+ const updates = yield* execute("get_updates", {
7
+ ...settings,
8
+ ...(updateId ? { offset: updateId } : undefined)
9
+ }).pipe(Micro.andThen(_ => _.sort(_ => _.update_id)));
10
+ let lastSuccessId = undefined;
11
+ let hasError = false;
12
+ for (const updateObject of updates) {
13
+ const update = extractUpdate(updateObject);
14
+ if (!update) {
15
+ console.warn("Unknown update", update);
16
+ hasError = true;
17
+ break;
18
+ }
19
+ const handler = handlers[`on_${update.type}`];
20
+ if (!handler) {
21
+ console.warn("Handler for update not defined", update);
22
+ hasError = true;
23
+ break;
24
+ }
25
+ const handleResult = handler(update);
26
+ if ("chat" in update) {
27
+ const response = yield* execute(`send_${handleResult.type}`, {
28
+ ...handleResult,
29
+ chat_id: update.chat.id
30
+ });
31
+ console.log("bot response", response);
32
+ }
33
+ if (!handleResult) {
34
+ hasError = true;
35
+ console.log(handleResult);
36
+ break;
37
+ }
38
+ ;
39
+ lastSuccessId = updateObject.update_id;
40
+ }
41
+ if (hasError && lastSuccessId) {
42
+ const resp = //commit successfully handled messages
43
+ yield* execute("get_updates", {
44
+ offset: lastSuccessId,
45
+ limit: 0
46
+ });
47
+ }
48
+ return { updates, lastSuccessId, hasError };
49
+ });
@@ -0,0 +1,37 @@
1
+ import * as Micro from "effect/Micro";
2
+ import { makeSettingsFrom } from "./settings.js";
3
+ import { fetchUpdates } from "./fetch-updates.js";
4
+ export const pollAndHandle = (input) => {
5
+ const state = {
6
+ lastUpdateId: undefined,
7
+ emptyResponses: 0
8
+ };
9
+ const settings = makeSettingsFrom(input.settings);
10
+ return Micro.delay(1000)(fetchUpdates({
11
+ state, settings,
12
+ execute: input.execute,
13
+ handlers: input.settings,
14
+ })).pipe(Micro.repeat({
15
+ while: ({ updates, lastSuccessId, hasError }) => {
16
+ if (hasError) {
17
+ console.warn("error in handler, quitting");
18
+ return false;
19
+ }
20
+ if (updates.length == 0) {
21
+ state.emptyResponses += 1;
22
+ if (state.emptyResponses > 200) {
23
+ console.info("too many empty responses, quitting");
24
+ return false;
25
+ }
26
+ }
27
+ else {
28
+ state.emptyResponses = 0;
29
+ }
30
+ ;
31
+ if (lastSuccessId) {
32
+ state.lastUpdateId = lastSuccessId + 1;
33
+ }
34
+ return true;
35
+ }
36
+ }));
37
+ };
@@ -0,0 +1,15 @@
1
+ export const makeSettingsFrom = (input) => {
2
+ let limit = input.batch_size ?? 10;
3
+ let timeout = input.timeout ?? 10;
4
+ if (limit < 10 || limit > 100) {
5
+ console.warn("Wrong limit, must be in [10..100], using 10 instead");
6
+ limit = 10;
7
+ }
8
+ if (timeout < 2 || timeout > 10) {
9
+ console.warn("Wrong timeout, must be in [2..10], using 2 instead");
10
+ limit = 10;
11
+ }
12
+ return {
13
+ limit, timeout
14
+ };
15
+ };
@@ -1,15 +1,16 @@
1
- import { makeExecute } from "./execute-request.js";
2
- import { makeDownloadFile } from "./download-file.js";
3
- import { defaultBaseUrl } from "./const.js";
4
- export const makeTgBotClient = (inputConfig) => {
5
- const config = {
6
- ...inputConfig,
7
- baseUrl: inputConfig.baseUrl ?? defaultBaseUrl
8
- };
9
- const execute = makeExecute(config);
10
- const file = makeDownloadFile(config, execute);
11
- return {
12
- ...execute,
13
- ...file
14
- };
1
+ import * as Micro from "effect/Micro";
2
+ import { makeTgBotClientConfig, TgBotClientConfig } from "./config.js";
3
+ import { ClientExecuteRequestService, ClientExecuteRequestServiceDefault } from "./execute-request/_service.js";
4
+ import { ClientFileService, ClientFileServiceDefault } from "./file/_service.js";
5
+ export const makeTgBotClient = (input) => {
6
+ const config = makeTgBotClientConfig(input);
7
+ const client = Micro.gen(function* () {
8
+ const execute = yield* Micro.service(ClientExecuteRequestService);
9
+ const file = yield* Micro.service(ClientFileService);
10
+ return {
11
+ execute: (method, input) => execute.execute(method, input).pipe(Micro.runPromise),
12
+ getFile: (input) => file.getFile(input).pipe(Micro.runPromise)
13
+ };
14
+ }).pipe(Micro.provideServiceEffect(ClientExecuteRequestService, ClientExecuteRequestServiceDefault), Micro.provideServiceEffect(ClientFileService, ClientFileServiceDefault), Micro.provideService(TgBotClientConfig, config), Micro.runSync);
15
+ return client;
15
16
  };
@@ -0,0 +1,8 @@
1
+ import * as Context from "effect/Context";
2
+ import { defaultBaseUrl } from "../const.js";
3
+ export const makeTgBotClientConfig = (input) => TgBotClientConfig.of({
4
+ ...input,
5
+ ["base-url"]: input["base-url"] ?? defaultBaseUrl
6
+ });
7
+ export class TgBotClientConfig extends Context.Tag("TgBotClientConfig")() {
8
+ }
@@ -1,12 +1,9 @@
1
- export class TgBotClientError extends Error {
2
- reason;
3
- _tag = "TgBotClientError";
4
- constructor(reason) {
5
- super();
6
- this.reason = reason;
7
- }
1
+ import * as Data from "effect/Data";
2
+ export class TgBotClientError extends Data.TaggedError("TgBotClientError") {
8
3
  static missingSuccess = new TgBotClientError({
9
- type: "ClientInternalError",
10
- cause: "Expected 'success' to be defined"
4
+ reason: {
5
+ type: "ClientInternalError",
6
+ cause: "Expected 'success' to be defined"
7
+ },
11
8
  });
12
9
  }
@@ -0,0 +1,12 @@
1
+ import * as Micro from "effect/Micro";
2
+ import * as Context from "effect/Context";
3
+ import { TgBotClientConfig } from "../config.js";
4
+ import { execute } from "./execute.js";
5
+ export class ClientExecuteRequestService extends Context.Tag("ClientExecuteRequestService")() {
6
+ }
7
+ export const ClientExecuteRequestServiceDefault = Micro.gen(function* () {
8
+ const config = yield* Micro.service(TgBotClientConfig);
9
+ return {
10
+ execute: (method, input) => execute(config, method, input)
11
+ };
12
+ });
@@ -0,0 +1,37 @@
1
+ import * as Micro from "effect/Micro";
2
+ import * as String from "effect/String";
3
+ import { TgBotClientError } from "../errors.js";
4
+ import { makePayload } from "./payload.js";
5
+ import { isTgBotApiResponse } from "../guards.js";
6
+ export const execute = (config, method, input) => Micro.gen(function* () {
7
+ const httpResponse = yield* Micro.tryPromise({
8
+ try: () => fetch(`${config["base-url"]}/bot${config["bot-token"]}/${String.snakeToCamel(method)}`, {
9
+ body: makePayload(input) ?? null,
10
+ method: "POST",
11
+ }),
12
+ catch: cause => new TgBotClientError({
13
+ reason: { type: "ClientInternalError", cause }
14
+ })
15
+ });
16
+ const response = yield* Micro.tryPromise({
17
+ try: () => httpResponse.json(),
18
+ catch: () => new TgBotClientError({
19
+ reason: { type: "UnexpectedResponse", response: httpResponse }
20
+ })
21
+ });
22
+ if (!isTgBotApiResponse(response)) {
23
+ return yield* Micro.fail(new TgBotClientError({
24
+ reason: { type: "UnexpectedResponse", response }
25
+ }));
26
+ }
27
+ if (!httpResponse.ok) {
28
+ return yield* Micro.fail(new TgBotClientError({
29
+ reason: {
30
+ type: "NotOkResponse",
31
+ ...(response.error_code ? { errorCode: response.error_code } : undefined),
32
+ ...(response.description ? { details: response.description } : undefined)
33
+ }
34
+ }));
35
+ }
36
+ return response.result;
37
+ });
@@ -1,6 +1,4 @@
1
- const hasFileContent = (input) => (typeof input == "object" && input != null) &&
2
- ("file_content" in input && input.file_content instanceof Uint8Array) &&
3
- ("file_name" in input && typeof input.file_name === "string" && input.file_name.length > 0);
1
+ import { isFileContent } from "../guards.js";
4
2
  export const makePayload = (body) => {
5
3
  const entries = Object.entries(body);
6
4
  if (entries.length == 0)
@@ -12,7 +10,7 @@ export const makePayload = (body) => {
12
10
  if (typeof value != "object") {
13
11
  result.append(key, `${value}`);
14
12
  }
15
- else if (hasFileContent(value)) {
13
+ else if (isFileContent(value)) {
16
14
  result.append(key, new Blob([value.file_content]), value.file_name);
17
15
  }
18
16
  else {
@@ -0,0 +1,14 @@
1
+ import * as Micro from "effect/Micro";
2
+ import * as Context from "effect/Context";
3
+ import { TgBotClientConfig } from "../config.js";
4
+ import { ClientExecuteRequestService, ClientExecuteRequestServiceDefault } from "../execute-request/_service.js";
5
+ import { getFile } from "./get-file.js";
6
+ export class ClientFileService extends Context.Tag("ClientFileService")() {
7
+ }
8
+ export const ClientFileServiceDefault = Micro.gen(function* () {
9
+ const config = yield* Micro.service(TgBotClientConfig);
10
+ const execute = yield* Micro.service(ClientExecuteRequestService);
11
+ return {
12
+ getFile: (input) => getFile(input.file_id, config, execute)
13
+ };
14
+ }).pipe(Micro.provideServiceEffect(ClientExecuteRequestService, ClientExecuteRequestServiceDefault));
@@ -0,0 +1,24 @@
1
+ import * as Micro from "effect/Micro";
2
+ import { TgBotClientError } from "../errors.js";
3
+ export const getFile = (fileId, config, execute) => Micro.gen(function* () {
4
+ const response = yield* execute.execute("get_file", { file_id: fileId });
5
+ const file_path = response.file_path;
6
+ if (!file_path || file_path.length == 0) {
7
+ return yield* Micro.fail(new TgBotClientError({
8
+ reason: {
9
+ type: "UnableToGetFile",
10
+ cause: "File path not defined"
11
+ }
12
+ }));
13
+ }
14
+ const file_name = file_path.replaceAll("/", "-");
15
+ const url = `${config["base-url"]}/file/bot${config["bot-token"]}/${file_path}`;
16
+ const fileContent = yield* Micro.tryPromise({
17
+ try: () => fetch(url).then(_ => _.arrayBuffer()),
18
+ catch: cause => new TgBotClientError({
19
+ reason: { type: "UnableToGetFile", cause }
20
+ })
21
+ });
22
+ const file = new File([new Uint8Array(fileContent)], file_name);
23
+ return file;
24
+ });
@@ -0,0 +1,7 @@
1
+ export const isFileContent = (input) => (typeof input == "object" && input != null) &&
2
+ ("file_content" in input && input.file_content instanceof Uint8Array) &&
3
+ ("file_name" in input && typeof input.file_name == "string");
4
+ export const isTgBotApiResponse = (input) => (typeof input == "object" && input != null) &&
5
+ ("ok" in input && typeof input.ok == "boolean");
6
+ export const isTgBotClientSettingsInput = (input) => (typeof input == "object" && input != null) &&
7
+ ("bot-token" in input && typeof input["bot-token"] == "string");
package/dist/esm/index.js CHANGED
@@ -1,4 +1,5 @@
1
- export * from "./client/const.js";
2
1
  export * from "./client/_client.js";
3
- // export * from "./specification/api.js"
4
- // export * from "./specification/types.js"
2
+ export * from "./bot/run.js";
3
+ export * from "./specification/api.js";
4
+ export * from "./specification/types.js";
5
+ export * from "./const.js";
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@effect-ak/tg-bot-client",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
+ "homepage": "https://effect-ak.github.io/telegram-bot-client",
5
6
  "author": {
6
7
  "name": "Aleksandr Kondaurov",
7
8
  "email": "kondaurov.dev@gmail.com"
@@ -30,22 +31,26 @@
30
31
  "@babel/plugin-transform-export-namespace-from": "^7.25.9",
31
32
  "@babel/plugin-transform-modules-commonjs": "^7.26.3",
32
33
  "@types/node": "^22.10.1",
33
- "effect": "^3.11.4",
34
+ "@types/js-yaml": "^4.0.9",
35
+ "js-yaml": "^4.1.0",
34
36
  "node-html-parser": "^6.1.13",
35
37
  "openapi-types": "^12.1.3",
36
38
  "ts-morph": "^24.0.0",
37
39
  "tsc-alias": "^1.8.10",
38
- "type-fest": "^4.30.0",
40
+ "tsx": "^4.19.2",
39
41
  "typescript": "^5.7.2",
40
42
  "vite-tsconfig-paths": "^5.1.4",
41
- "vitest": "^2.1.8",
42
- "@redocly/cli": "^1.26.0"
43
+ "vitest": "^2.1.8"
44
+ },
45
+ "dependencies": {
46
+ "effect": "^3.4.0"
43
47
  },
44
48
  "scripts": {
45
- "gen": "bun run ./codegen/main",
49
+ "docgen": "docgen",
50
+ "gen": "tsx ./codegen/main",
51
+ "run-bot": "tsx ./src/bot/run",
46
52
  "build": "pnpm build-esm && pnpm build-cjs",
47
53
  "build-esm": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
48
- "build-cjs": "babel dist/esm --out-dir dist/cjs",
49
- "gen-html": "redocly build-docs --output ../effect-ak.github.io/telegram-bot-api/index.html"
54
+ "build-cjs": "babel dist/esm --out-dir dist/cjs"
50
55
  }
51
56
  }
package/readme.md CHANGED
@@ -1,34 +1,29 @@
1
- ![NPM Version](https://img.shields.io/npm/v/%40effect-ak%2Ftg-bot-client)
1
+ [![NPM Version](https://img.shields.io/npm/v/%40effect-ak%2Ftg-bot-client)](https://www.npmjs.com/package/@effect-ak/tg-bot-client)
2
+ [![OpenAPI](https://img.shields.io/badge/OpenAPI-3.1-blue.svg)](https://effect-ak.github.io/telegram-bot-api/)
3
+ ![NPM Unpacked Size](https://img.shields.io/npm/unpacked-size/%40effect-ak%2Ftg-bot-client)
4
+ ![NPM Downloads](https://img.shields.io/npm/dw/%40effect-ak%2Ftg-bot-client)
2
5
 
3
- [OpenApi Specification](https://effect-ak.github.io/telegram-bot-api/)
4
6
 
5
- ### What is it?
6
7
 
7
- This is a client for interacting with the Telegram Bot API.
8
- The main reason for creating this package is that Telegram does not provide an SDK for working with their API.
8
+ ### Motivation
9
9
 
10
- They only provide documentation in the form of a massive HTML page, which is very inconvenient for navigating and understanding what the Telegram Bot API offers.
10
+ The official documentation is available as a comprehensive [HTML page](https://core.telegram.org/bots/api), providing basic navigation. While functional, relying solely on this format can be somewhat inconvenient during bot development.
11
11
 
12
- ## Features:
13
- - **Pure TypeScript Client**: This is a clean client written in TypeScript with no abstractions.
14
- - **Complete**: The entire API is generated from the official documentation [https://core.telegram.org/bots/api](https://core.telegram.org/bots/api) using a [code generator](./codegen/main.ts).
15
- - ~~**Inline Documentation**: No need to read lengthy official documentation. All types and comments are available in JS DOC, allowing you to develop your bot without leaving your IDE.~~
16
- - Codegenerator produces TypeScript code and OpenApi specification now! Documentation was removed from TypeScript interfaces in order to keep npm package smaller.
12
+ This client facilitates interaction with the Telegram Bot API. It was created primarily because Telegram does not offer an official SDK for their API.
17
13
 
14
+ ## Features:
15
+ - **Typesafe Client**: This is a clean client written in TypeScript with no abstractions.
16
+ - **Complete**: The entire API is generated from [the official documentation](https://core.telegram.org/bots/api) using a [code generator](./codegen/main.ts)
17
+ - **Readable Method Names**: Method names, such as `setChatAdministratorCustomTitle`, are converted to snake_case for easier code readability, e.g., `set_chat_administrator_custom_title`.
18
18
  - **Type Mapping**: Types from the documentation are converted to TypeScript types:
19
19
  - `Integer` becomes `number`
20
20
  - `True` becomes `boolean`
21
21
  - `String or Number` becomes `string | number`
22
22
  - Enumerated types, such as `Type of the chat, can be either “private”, “group”, “supergroup” or “channel”` becomes a standard union of literal types `"private"| "group" | "supergroup" | "channel"`
23
23
  - And so on
24
- - **Readable Method Names**: Method names, such as `SetChatAdministratorCustomTitleInput`, are converted to snake_case for easier code readability, e.g., `set_chat_administrator_custom_title`.
25
24
 
26
25
  ### Usage example
27
26
 
28
- #### Install
29
-
30
- `npm i @effect-ak/tg-bot-client`
31
-
32
27
  #### Creating a Client
33
28
 
34
29
  ```typescript
@@ -39,17 +34,13 @@ const client = makeTgBotClient({
39
34
  });
40
35
  ```
41
36
 
42
- #### Executing api methods, typesafety
37
+ #### Executing api methods
43
38
 
44
39
  `client` has an `execute` method which requires two arguments
45
40
 
46
41
  - the first is the API method, e.g. `send_message`
47
42
  - the second is an object containing the arguments for that method, e.g. `text`
48
43
 
49
- `execute` never returns failed promise, instead, it gives an object with two fields, `success`, `error`.
50
-
51
- if you want, you can use unsafe alternative, `unsafeExecute`, which gives you the result of method or throws an Error
52
-
53
44
  #### 1. Sending a Message with an Effect
54
45
 
55
46
  ```typescript
@@ -99,11 +90,70 @@ const file =
99
90
  await client.getFile({
100
91
  file_id: fileId
101
92
  });
102
- // file is File
103
93
  ```
104
94
 
105
- ---
95
+ ### Chatbot Support
96
+
97
+ You can write the logic for your chatbot and run it locally.
98
+
99
+ Take a look at [example](./example/echo-bot.ts)
100
+
101
+ The Telegram bot supports both push and pull notification models for messages. This package uses the **pull** model for several reasons:
102
+
103
+ - **Flexibility in Handler Deployment:** Allows you to run the bot handler on any JS platform (NodeJs, Browser)
104
+ - **Sequential Message Processing:** Messages in the queue are read one by one, and the handler is invoked for each message. If an error occurs in the handler, the next message remains in the queue, and the bot stops running. When the handler successfully processes a message, it proceeds to the next one.
105
+
106
+ ### Setup Instructions
107
+
108
+ 1. **Create a `config.json` File**
109
+
110
+ In the root of your project, create a `config.json` file with the following content:
111
+
112
+ ```json
113
+ {
114
+ "bot-token": "your-token"
115
+ }
116
+ ```
117
+
118
+ Replace `"your-token"` with your actual Telegram bot token.
119
+
120
+ 2. **Create `bot.js` and Implement Your Bot's Logic**
121
+
122
+ Create a file named `bot.js` and add your bot's logic as shown below:
123
+
124
+ ```typescript
125
+ import { MESSAGE_EFFECTS, runTgChatBot } from "@effect-ak/tg-bot-client"
126
+
127
+ runTgChatBot({
128
+ type: "fromJsonFile",
129
+ on_message: (msg) => {
130
+ if (msg?.text === "bye") {
131
+ return {
132
+ type: "message",
133
+ text: "See you later!",
134
+ message_effect_id: MESSAGE_EFFECTS["❤️"]
135
+ }
136
+ }
137
+
138
+ return {
139
+ type: "message",
140
+ text: "I'm a simple bot"
141
+ }
142
+ }
143
+ })
144
+ ```
145
+
146
+ **Explanation:**
147
+ - **Import Statements:** Import necessary modules from the `@effect-ak/tg-bot-client` package.
148
+ - **`runTgChatBot` Function:** Initializes the Telegram chatbot using the configuration from the `config.json` file.
149
+ - **`on_message` Handler:** Defines the logic for handling incoming messages.
150
+ - If the message text is `"bye"`, the bot responds with `"See you later!"` and adds a heart emoji effect.
151
+ - For any other message, the bot responds with `"I'm a simple bot"`.
152
+
153
+ 3. **Run the Bot**
106
154
 
107
- ### Summary
155
+ To start your chatbot, execute the following command in your terminal:
108
156
 
109
- This code generator and client will continue to be developed. However, for now, I have generated all the methods and types. If you find any errors, please let me know.
157
+ ```bash
158
+ node bot.js
159
+ ```
@@ -1,44 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.makeDownloadFile = void 0;
7
- var _errors = require("./errors.js");
8
- const makeDownloadFile = (config, execute) => {
9
- const getFile = async input => {
10
- const response = await execute.execute("get_file", input);
11
- if (response.error) return {
12
- error: response.error
13
- };
14
- const file_path = response.success?.file_path;
15
- if (!file_path || file_path.length == 0) {
16
- return {
17
- error: new _errors.TgBotClientError({
18
- type: "UnableToGetFile",
19
- cause: "File path not defined"
20
- })
21
- };
22
- }
23
- const file_name = file_path.replaceAll("/", "-");
24
- const url = `${config.baseUrl}/file/bot${config.token}/${file_path}`;
25
- try {
26
- const fileContent = await fetch(url).then(_ => _.arrayBuffer());
27
- const file = new File([new Uint8Array(fileContent)], file_name);
28
- return {
29
- success: file
30
- };
31
- } catch (cause) {
32
- return {
33
- error: new _errors.TgBotClientError({
34
- type: "UnableToGetFile",
35
- cause
36
- })
37
- };
38
- }
39
- };
40
- return {
41
- getFile
42
- };
43
- };
44
- exports.makeDownloadFile = makeDownloadFile;
@@ -1,69 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.makeExecute = void 0;
7
- var _request = require("./request.js");
8
- var _response = require("./response.js");
9
- var _errors = require("./errors.js");
10
- const makeExecute = config => {
11
- const execute = async (method, input) => {
12
- try {
13
- const httpResponse = await fetch(`${config.baseUrl}/bot${config.token}/${snakeToCamel(method)}`, {
14
- body: (0, _request.makePayload)(input) ?? null,
15
- method: "POST"
16
- });
17
- const response = await httpResponse.json();
18
- if (!(0, _response.isTgBotApiResponse)(response)) {
19
- return {
20
- error: new _errors.TgBotClientError({
21
- type: "UnexpectedResponse",
22
- response
23
- })
24
- };
25
- }
26
- if (!httpResponse.ok) {
27
- return {
28
- error: new _errors.TgBotClientError({
29
- type: "NotOkResponse",
30
- ...(response.error_code ? {
31
- errorCode: response.error_code
32
- } : undefined),
33
- ...(response.description ? {
34
- details: response.description
35
- } : undefined)
36
- })
37
- };
38
- }
39
- return {
40
- success: response.result
41
- };
42
- } catch (cause) {
43
- return {
44
- error: new _errors.TgBotClientError({
45
- type: "ClientInternalError",
46
- cause
47
- })
48
- };
49
- }
50
- };
51
- const unsafeExecute = async (method, input) => {
52
- const result = await execute(method, input);
53
- if (result.error) throw result.error;
54
- if (!("success" in result)) throw _errors.TgBotClientError.missingSuccess;
55
- return result.success;
56
- };
57
- return {
58
- execute,
59
- unsafeExecute
60
- };
61
- };
62
- exports.makeExecute = makeExecute;
63
- const snakeToCamel = methodName => methodName.split("_").reduce((result, word, step) => {
64
- if (step == 0) {
65
- return word;
66
- } else {
67
- return result + word.at(0)?.toUpperCase() + word.slice(1);
68
- }
69
- }, "");
@@ -1,22 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.isTgBotApiResponse = void 0;
7
- const isTgBotApiResponse = input => {
8
- if (typeof input !== "object" || input == null) {
9
- return false;
10
- }
11
- if (!("ok" in input && typeof input.ok == "boolean")) {
12
- return false;
13
- }
14
- if ("error_code" in input && typeof input.error_code != "number") {
15
- return false;
16
- }
17
- if ("description" in input && typeof input.description != "string") {
18
- return false;
19
- }
20
- return true;
21
- };
22
- exports.isTgBotApiResponse = isTgBotApiResponse;
@@ -1,13 +0,0 @@
1
- import type { BotConfig } from "./_client.js";
2
- import { TgBotClientError } from "./errors.js";
3
- import type { ExecuteRequest } from "./execute-request.js";
4
- type GetFileResult = {
5
- success?: File;
6
- error?: TgBotClientError;
7
- };
8
- export declare const makeDownloadFile: (config: BotConfig, execute: ExecuteRequest) => {
9
- readonly getFile: (input: {
10
- file_id: string;
11
- }) => Promise<GetFileResult>;
12
- };
13
- export {};
@@ -1,13 +0,0 @@
1
- import type { BotConfig } from "./_client.js";
2
- import type { Api } from "../specification/api.js";
3
- import { TgBotClientError } from "./errors.js";
4
- export type ExecuteRequest = ReturnType<typeof makeExecute>;
5
- type SafeExecuteResult<M extends keyof Api> = {
6
- success?: ReturnType<Api[M]>;
7
- error?: TgBotClientError;
8
- };
9
- export declare const makeExecute: (config: BotConfig) => {
10
- readonly execute: <M extends keyof Api>(method: M, input: Parameters<Api[M]>[0]) => Promise<SafeExecuteResult<M>>;
11
- readonly unsafeExecute: <M extends keyof Api>(method: M, input: Parameters<Api[M]>[0]) => Promise<ReturnType<Api[M]>>;
12
- };
13
- export {};
@@ -1,7 +0,0 @@
1
- export type TgBotApiResponse<O> = {
2
- ok: boolean;
3
- error_code?: number;
4
- description?: string;
5
- result?: O;
6
- };
7
- export declare const isTgBotApiResponse: <O>(input: unknown) => input is TgBotApiResponse<O>;