@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.
- package/dist/cjs/bot/factory/_service.js +20 -0
- package/dist/cjs/bot/factory/client-config.js +29 -0
- package/dist/cjs/bot/factory/make-bot.js +20 -0
- package/dist/cjs/bot/message-handler/_service.js +12 -0
- package/dist/cjs/bot/message-handler/types.js +5 -0
- package/dist/cjs/bot/message-handler/utils.js +19 -0
- package/dist/cjs/bot/run.js +10 -0
- package/dist/cjs/bot/update-poller/_service.js +39 -0
- package/dist/cjs/bot/update-poller/errors.js +5 -0
- package/dist/cjs/bot/update-poller/fetch-updates.js +70 -0
- package/dist/cjs/bot/update-poller/poll-and-handle.js +50 -0
- package/dist/cjs/bot/update-poller/settings.js +23 -0
- package/dist/cjs/client/_client.js +17 -14
- package/dist/cjs/client/config.js +17 -0
- package/dist/cjs/client/errors.js +8 -9
- package/dist/cjs/client/execute-request/_service.js +20 -0
- package/dist/cjs/client/execute-request/execute.js +59 -0
- package/dist/cjs/client/{request.js → execute-request/payload.js} +2 -2
- package/dist/cjs/client/file/_service.js +22 -0
- package/dist/cjs/client/file/get-file.js +38 -0
- package/dist/cjs/client/guards.js +12 -0
- package/dist/cjs/{client/const.js → const.js} +2 -1
- package/dist/cjs/index.js +19 -8
- package/dist/dts/bot/factory/_service.d.ts +37 -0
- package/dist/dts/bot/factory/client-config.d.ts +3 -0
- package/dist/dts/bot/factory/make-bot.d.ts +7 -0
- package/dist/dts/bot/message-handler/_service.d.ts +6 -0
- package/dist/dts/bot/message-handler/types.d.ts +16 -0
- package/dist/dts/bot/message-handler/utils.d.ts +6 -0
- package/dist/dts/bot/run.d.ts +7 -0
- package/dist/dts/bot/update-poller/_service.d.ts +21 -0
- package/dist/dts/bot/update-poller/errors.d.ts +1 -0
- package/dist/dts/bot/update-poller/fetch-updates.d.ts +17 -0
- package/dist/dts/bot/update-poller/poll-and-handle.d.ts +16 -0
- package/dist/dts/bot/update-poller/settings.d.ts +6 -0
- package/dist/dts/client/_client.d.ts +6 -17
- package/dist/dts/client/config.d.ts +8 -0
- package/dist/dts/client/errors.d.ts +6 -4
- package/dist/dts/client/execute-request/_service.d.ts +15 -0
- package/dist/dts/client/execute-request/execute.d.ts +5 -0
- package/dist/dts/client/file/_service.d.ts +16 -0
- package/dist/dts/client/file/get-file.d.ts +5 -0
- package/dist/dts/client/guards.d.ts +17 -0
- package/dist/dts/{client/const.d.ts → const.d.ts} +1 -0
- package/dist/dts/index.d.ts +2 -1
- package/dist/dts/specification/api.d.ts +786 -2776
- package/dist/dts/specification/types.d.ts +1387 -3547
- package/dist/esm/bot/factory/_service.js +12 -0
- package/dist/esm/bot/factory/client-config.js +20 -0
- package/dist/esm/bot/factory/make-bot.js +9 -0
- package/dist/esm/bot/message-handler/_service.js +4 -0
- package/dist/esm/bot/message-handler/types.js +1 -0
- package/dist/esm/bot/message-handler/utils.js +12 -0
- package/dist/esm/bot/run.js +6 -0
- package/dist/esm/bot/update-poller/_service.js +31 -0
- package/dist/esm/bot/update-poller/errors.js +1 -0
- package/dist/esm/bot/update-poller/fetch-updates.js +49 -0
- package/dist/esm/bot/update-poller/poll-and-handle.js +37 -0
- package/dist/esm/bot/update-poller/settings.js +15 -0
- package/dist/esm/client/_client.js +15 -14
- package/dist/esm/client/config.js +8 -0
- package/dist/esm/client/errors.js +6 -9
- package/dist/esm/client/execute-request/_service.js +12 -0
- package/dist/esm/client/execute-request/execute.js +37 -0
- package/dist/esm/client/{request.js → execute-request/payload.js} +2 -4
- package/dist/esm/client/file/_service.js +14 -0
- package/dist/esm/client/file/get-file.js +24 -0
- package/dist/esm/client/guards.js +7 -0
- package/dist/esm/{client/const.js → const.js} +1 -0
- package/dist/esm/index.js +2 -1
- package/package.json +12 -4
- package/readme.md +83 -24
- package/dist/cjs/client/download-file.js +0 -44
- package/dist/cjs/client/execute-request.js +0 -69
- package/dist/cjs/client/response.js +0 -22
- package/dist/dts/client/download-file.d.ts +0 -13
- package/dist/dts/client/execute-request.d.ts +0 -13
- package/dist/dts/client/response.d.ts +0 -7
- package/dist/esm/client/download-file.js +0 -36
- package/dist/esm/client/execute-request.js +0 -62
- package/dist/esm/client/response.js +0 -15
- /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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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 (
|
|
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
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-ak/tg-bot-client",
|
|
3
|
-
"version": "0.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,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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-

|
|
1
|
+
[](https://www.npmjs.com/package/@effect-ak/tg-bot-client)
|
|
2
|
+
[](https://effect-ak.github.io/telegram-bot-api/)
|
|
3
|
+

|
|
4
|
+

|
|
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
|
-
|
|
8
|
+
### Motivation
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
155
|
+
To start your chatbot, execute the following command in your terminal:
|
|
99
156
|
|
|
100
|
-
|
|
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;
|