@effect-ak/tg-bot-client 0.0.7 → 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/{client/const.js → const.js} +2 -1
  23. package/dist/cjs/index.js +19 -8
  24. package/dist/dts/bot/factory/_service.d.ts +37 -0
  25. package/dist/dts/bot/factory/client-config.d.ts +3 -0
  26. package/dist/dts/bot/factory/make-bot.d.ts +7 -0
  27. package/dist/dts/bot/message-handler/_service.d.ts +6 -0
  28. package/dist/dts/bot/message-handler/types.d.ts +16 -0
  29. package/dist/dts/bot/message-handler/utils.d.ts +6 -0
  30. package/dist/dts/bot/run.d.ts +7 -0
  31. package/dist/dts/bot/update-poller/_service.d.ts +21 -0
  32. package/dist/dts/bot/update-poller/errors.d.ts +1 -0
  33. package/dist/dts/bot/update-poller/fetch-updates.d.ts +17 -0
  34. package/dist/dts/bot/update-poller/poll-and-handle.d.ts +16 -0
  35. package/dist/dts/bot/update-poller/settings.d.ts +6 -0
  36. package/dist/dts/client/_client.d.ts +6 -17
  37. package/dist/dts/client/config.d.ts +8 -0
  38. package/dist/dts/client/errors.d.ts +6 -4
  39. package/dist/dts/client/execute-request/_service.d.ts +15 -0
  40. package/dist/dts/client/execute-request/execute.d.ts +5 -0
  41. package/dist/dts/client/file/_service.d.ts +16 -0
  42. package/dist/dts/client/file/get-file.d.ts +5 -0
  43. package/dist/dts/client/guards.d.ts +17 -0
  44. package/dist/dts/{client/const.d.ts → const.d.ts} +1 -0
  45. package/dist/dts/index.d.ts +2 -1
  46. package/dist/dts/specification/api.d.ts +786 -2776
  47. package/dist/dts/specification/types.d.ts +1387 -3547
  48. package/dist/esm/bot/factory/_service.js +12 -0
  49. package/dist/esm/bot/factory/client-config.js +20 -0
  50. package/dist/esm/bot/factory/make-bot.js +9 -0
  51. package/dist/esm/bot/message-handler/_service.js +4 -0
  52. package/dist/esm/bot/message-handler/types.js +1 -0
  53. package/dist/esm/bot/message-handler/utils.js +12 -0
  54. package/dist/esm/bot/run.js +6 -0
  55. package/dist/esm/bot/update-poller/_service.js +31 -0
  56. package/dist/esm/bot/update-poller/errors.js +1 -0
  57. package/dist/esm/bot/update-poller/fetch-updates.js +49 -0
  58. package/dist/esm/bot/update-poller/poll-and-handle.js +37 -0
  59. package/dist/esm/bot/update-poller/settings.js +15 -0
  60. package/dist/esm/client/_client.js +15 -14
  61. package/dist/esm/client/config.js +8 -0
  62. package/dist/esm/client/errors.js +6 -9
  63. package/dist/esm/client/execute-request/_service.js +12 -0
  64. package/dist/esm/client/execute-request/execute.js +37 -0
  65. package/dist/esm/client/{request.js → execute-request/payload.js} +2 -4
  66. package/dist/esm/client/file/_service.js +14 -0
  67. package/dist/esm/client/file/get-file.js +24 -0
  68. package/dist/esm/client/guards.js +7 -0
  69. package/dist/esm/{client/const.js → const.js} +1 -0
  70. package/dist/esm/index.js +2 -1
  71. package/package.json +12 -4
  72. package/readme.md +83 -24
  73. package/dist/cjs/client/download-file.js +0 -44
  74. package/dist/cjs/client/execute-request.js +0 -69
  75. package/dist/cjs/client/response.js +0 -22
  76. package/dist/dts/client/download-file.d.ts +0 -13
  77. package/dist/dts/client/execute-request.d.ts +0 -13
  78. package/dist/dts/client/response.d.ts +0 -7
  79. package/dist/esm/client/download-file.js +0 -36
  80. package/dist/esm/client/execute-request.js +0 -62
  81. package/dist/esm/client/response.js +0 -15
  82. /package/dist/dts/client/{request.d.ts → execute-request/payload.d.ts} +0 -0
@@ -0,0 +1,12 @@
1
+ import * as Micro from "effect/Micro";
2
+ import * as Context from "effect/Context";
3
+ import { TgBotClientConfig } from "../../client/config.js";
4
+ import { makeClientConfigFrom } from "./client-config.js";
5
+ import { makeBot } from "./make-bot.js";
6
+ export class BotFactoryService extends Context.Tag("BotFactoryService")() {
7
+ }
8
+ ;
9
+ export const BotFactoryServiceDefault = {
10
+ makeBot,
11
+ runBot: (input) => makeBot(input).pipe(Micro.provideServiceEffect(TgBotClientConfig, makeClientConfigFrom(input)))
12
+ };
@@ -0,0 +1,20 @@
1
+ import * as Micro from "effect/Micro";
2
+ import { readFileSync } from "fs";
3
+ import { makeTgBotClientConfig } from "../../client/config.js";
4
+ import { isTgBotClientSettingsInput } from "../../client/guards.js";
5
+ export const makeClientConfigFrom = (input) => Micro.gen(function* () {
6
+ if (input.type == "config") {
7
+ return makeTgBotClientConfig(input);
8
+ }
9
+ const config = yield* Micro.try({
10
+ try: () => JSON.parse(readFileSync("config.json").toString("utf-8")),
11
+ catch: error => {
12
+ console.warn(error);
13
+ return "ReadingConfigError";
14
+ }
15
+ });
16
+ if (!isTgBotClientSettingsInput(config)) {
17
+ return yield* Micro.fail("InvalidConfig");
18
+ }
19
+ return makeTgBotClientConfig(config);
20
+ });
@@ -0,0 +1,9 @@
1
+ import * as Micro from "effect/Micro";
2
+ import { BotUpdatePollerService, BotUpdatesPollerServiceDefault } from "../../bot/update-poller/_service.js";
3
+ export const makeBot = (messageHandler) => Micro.gen(function* () {
4
+ const { runBot } = yield* Micro.service(BotUpdatePollerService);
5
+ return yield* runBot(messageHandler);
6
+ }).pipe(Micro.provideServiceEffect(BotUpdatePollerService, BotUpdatesPollerServiceDefault), Micro.tapError(error => {
7
+ console.error(error);
8
+ return Micro.void;
9
+ }));
@@ -0,0 +1,4 @@
1
+ import * as Context from "effect/Context";
2
+ export class BotMessageHandler extends Context.Tag("BotMessageHandler")() {
3
+ }
4
+ ;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ export const extractUpdate = (input) => {
2
+ for (const [field, value] of Object.entries(input)) {
3
+ if (field == "update_id") {
4
+ continue;
5
+ }
6
+ return {
7
+ type: field,
8
+ ...value
9
+ };
10
+ }
11
+ return undefined;
12
+ };
@@ -0,0 +1,6 @@
1
+ import { Micro } from "effect";
2
+ import { BotFactoryServiceDefault } from "./factory/_service.js";
3
+ export const runTgChatBot = (input) => BotFactoryServiceDefault
4
+ .runBot(input)
5
+ .pipe(Micro.runPromise)
6
+ .finally(() => "Telegram chat bot has been shutdown");
@@ -0,0 +1,31 @@
1
+ import * as Micro from "effect/Micro";
2
+ import * as Context from "effect/Context";
3
+ import { ClientExecuteRequestService, ClientExecuteRequestServiceDefault } from "../../client/execute-request/_service.js";
4
+ import { pollAndHandle } from "./poll-and-handle.js";
5
+ export class BotUpdatePollerService extends Context.Tag("BotUpdatePollerService")() {
6
+ }
7
+ ;
8
+ export const BotUpdatesPollerServiceDefault = Micro.gen(function* () {
9
+ const state = {
10
+ isActive: false,
11
+ };
12
+ const client = yield* Micro.service(ClientExecuteRequestService);
13
+ const runBot = (messageHandler) => Micro.gen(function* () {
14
+ if (state.isActive) {
15
+ return yield* Micro.fail("AlreadyRunning");
16
+ }
17
+ const fiber = yield* pollAndHandle({
18
+ settings: messageHandler,
19
+ execute: client.execute
20
+ }).pipe(Micro.forkDaemon);
21
+ fiber.addObserver((exit) => {
22
+ console.log("bot's fiber has been closed", exit);
23
+ state.isActive = false;
24
+ });
25
+ console.log("Reading bot's updates...");
26
+ return fiber;
27
+ });
28
+ return {
29
+ runBot
30
+ };
31
+ }).pipe(Micro.provideServiceEffect(ClientExecuteRequestService, ClientExecuteRequestServiceDefault));
@@ -0,0 +1 @@
1
+ export {};
@@ -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
- const defaultBaseUrl = "https://api.telegram.org";
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");
@@ -1,3 +1,4 @@
1
+ export const defaultBaseUrl = "https://api.telegram.org";
1
2
  export const MESSAGE_EFFECTS = {
2
3
  "🔥": "5104841245755180586",
3
4
  "👍": "5107584321108051014",
package/dist/esm/index.js CHANGED
@@ -1,4 +1,5 @@
1
- export * from "./client/const.js";
2
1
  export * from "./client/_client.js";
2
+ export * from "./bot/run.js";
3
3
  export * from "./specification/api.js";
4
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.0.7",
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,17 +31,24 @@
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",
37
+ "openapi-types": "^12.1.3",
35
38
  "ts-morph": "^24.0.0",
36
39
  "tsc-alias": "^1.8.10",
37
- "type-fest": "^4.30.0",
40
+ "tsx": "^4.19.2",
38
41
  "typescript": "^5.7.2",
39
42
  "vite-tsconfig-paths": "^5.1.4",
40
43
  "vitest": "^2.1.8"
41
44
  },
45
+ "dependencies": {
46
+ "effect": "^3.4.0"
47
+ },
42
48
  "scripts": {
43
- "gen": "bun run codegen/main",
49
+ "docgen": "docgen",
50
+ "gen": "tsx ./codegen/main",
51
+ "run-bot": "tsx ./src/bot/run",
44
52
  "build": "pnpm build-esm && pnpm build-cjs",
45
53
  "build-esm": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
46
54
  "build-cjs": "babel dist/esm --out-dir dist/cjs"
package/readme.md CHANGED
@@ -1,24 +1,28 @@
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
- ### What is it?
4
6
 
5
- This is a client for interacting with the Telegram Bot API.
6
- The main reason for creating this package is that Telegram does not provide an SDK for working with their API.
7
7
 
8
- 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.
8
+ ### Motivation
9
9
 
10
- ## Features:
11
- - **Pure TypeScript Client**: This is a clean client written in TypeScript with no abstractions.
12
- - **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).
13
- - **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.
14
- - **Type Mapping**: Types from the documentation are converted to TypeScript types. For example, `Integer` becomes `number`, `True` becomes `boolean`, `String or Number` becomes `string | number`, and so on.
15
- - **Readable Method Names**: Method names, such as `SetChatAdministratorCustomTitleInput`, are converted to snake_case for easier code readability, e.g., `set_chat_administrator_custom_title`.
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.
16
11
 
17
- ### Usage example
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.
18
13
 
19
- #### Install
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
+ - **Type Mapping**: Types from the documentation are converted to TypeScript types:
19
+ - `Integer` becomes `number`
20
+ - `True` becomes `boolean`
21
+ - `String or Number` becomes `string | number`
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
+ - And so on
20
24
 
21
- `npm i @effect-ak/tg-bot-client`
25
+ ### Usage example
22
26
 
23
27
  #### Creating a Client
24
28
 
@@ -30,21 +34,17 @@ const client = makeTgBotClient({
30
34
  });
31
35
  ```
32
36
 
33
- #### Executing api methods, typesafety
37
+ #### Executing api methods
34
38
 
35
39
  `client` has an `execute` method which requires two arguments
36
40
 
37
41
  - the first is the API method, e.g. `send_message`
38
42
  - the second is an object containing the arguments for that method, e.g. `text`
39
43
 
40
- `execute` never returns failed promise, instead, it gives an object with two fields, `success`, `error`.
41
-
42
- if you want, you can use unsafe alternative, `unsafeExecute`, which gives you the result of method or throws an Error
43
-
44
44
  #### 1. Sending a Message with an Effect
45
45
 
46
46
  ```typescript
47
- import { MESSAGE_EFFECTS } from "effect-ak/tg-bot-client"
47
+ import { MESSAGE_EFFECTS } from "@effect-ak/tg-bot-client"
48
48
 
49
49
  await client.execute("send_message", {
50
50
  chat_id: "???", // replace ??? with the chat number
@@ -90,11 +90,70 @@ const file =
90
90
  await client.getFile({
91
91
  file_id: fileId
92
92
  });
93
- // file is File
94
93
  ```
95
94
 
96
- ---
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**
97
154
 
98
- ### Summary
155
+ To start your chatbot, execute the following command in your terminal:
99
156
 
100
- 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;