@effect-ak/tg-bot 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/dist/index.cjs +114 -37
- package/dist/index.d.ts +50 -23
- package/dist/index.js +111 -36
- package/package.json +7 -3
- package/readme.md +507 -0
package/dist/index.cjs
CHANGED
|
@@ -25,8 +25,10 @@ __export(index_exports, {
|
|
|
25
25
|
BotPollSettingsTag: () => BotPollSettingsTag,
|
|
26
26
|
BotResponse: () => BotResponse,
|
|
27
27
|
BotRunService: () => BotRunService,
|
|
28
|
+
BotTgClientTag: () => BotTgClientTag,
|
|
28
29
|
BotUpdateHandlersTag: () => BotUpdateHandlersTag,
|
|
29
30
|
HandleUpdateError: () => HandleUpdateError,
|
|
31
|
+
createBotContext: () => createBotContext,
|
|
30
32
|
defineBot: () => defineBot,
|
|
31
33
|
extractUpdate: () => extractUpdate,
|
|
32
34
|
handleEntireBatch: () => handleEntireBatch,
|
|
@@ -2655,6 +2657,26 @@ var BotResponse = class _BotResponse extends TaggedClass("BotResponse") {
|
|
|
2655
2657
|
static ignore = new _BotResponse({});
|
|
2656
2658
|
};
|
|
2657
2659
|
|
|
2660
|
+
// src/internal/bot-context.ts
|
|
2661
|
+
var extractCommand = (update) => {
|
|
2662
|
+
if (typeof update !== "object" || update === null) return void 0;
|
|
2663
|
+
const u = update;
|
|
2664
|
+
if (!u.entities || !u.text) return void 0;
|
|
2665
|
+
const entity = u.entities.find((e) => e.type === "bot_command");
|
|
2666
|
+
if (!entity) return void 0;
|
|
2667
|
+
return u.text.slice(entity.offset, entity.offset + entity.length);
|
|
2668
|
+
};
|
|
2669
|
+
var createBotContext = (update) => {
|
|
2670
|
+
const command = extractCommand(update);
|
|
2671
|
+
return {
|
|
2672
|
+
command,
|
|
2673
|
+
reply: (text, options) => BotResponse.make({ type: "message", text, ...options }),
|
|
2674
|
+
replyWithDocument: (document, options) => BotResponse.make({ type: "document", document, ...options }),
|
|
2675
|
+
replyWithPhoto: (photo, options) => BotResponse.make({ type: "photo", photo, ...options }),
|
|
2676
|
+
ignore: BotResponse.ignore
|
|
2677
|
+
};
|
|
2678
|
+
};
|
|
2679
|
+
|
|
2658
2680
|
// ../../node_modules/.pnpm/effect@3.12.0/node_modules/effect/dist/esm/Effectable.js
|
|
2659
2681
|
var EffectPrototype2 = EffectPrototype;
|
|
2660
2682
|
|
|
@@ -3491,11 +3513,12 @@ var runPromise = (effect, options) => runPromiseExit(effect, options).then((exit
|
|
|
3491
3513
|
|
|
3492
3514
|
// src/internal/handle-update.ts
|
|
3493
3515
|
var import_tg_bot_client = require("@effect-ak/tg-bot-client");
|
|
3494
|
-
var import_tg_bot_client2 = require("@effect-ak/tg-bot-client");
|
|
3495
3516
|
|
|
3496
3517
|
// src/internal/poll-settings.ts
|
|
3497
3518
|
var BotUpdateHandlersTag = class extends Tag2("BotUpdateHandlers")() {
|
|
3498
3519
|
};
|
|
3520
|
+
var BotTgClientTag = class extends Tag2("BotTgClient")() {
|
|
3521
|
+
};
|
|
3499
3522
|
var BotPollSettings = class _BotPollSettings extends Class2 {
|
|
3500
3523
|
static make(input) {
|
|
3501
3524
|
let batch_size = input.batch_size ?? 10;
|
|
@@ -3545,6 +3568,36 @@ var BotPollSettingsTag = class extends Reference2()(
|
|
|
3545
3568
|
};
|
|
3546
3569
|
|
|
3547
3570
|
// src/internal/handle-update.ts
|
|
3571
|
+
var isGuardedHandler = (handler) => typeof handler === "object" && handler !== null && "handle" in handler;
|
|
3572
|
+
var executeSingleGuard = async (guard, update, ctx) => {
|
|
3573
|
+
const input = { update, ctx };
|
|
3574
|
+
if (guard.match) {
|
|
3575
|
+
const matched = await guard.match(input);
|
|
3576
|
+
if (!matched) return null;
|
|
3577
|
+
}
|
|
3578
|
+
return await guard.handle(input);
|
|
3579
|
+
};
|
|
3580
|
+
var executeGuards = async (guards, update, ctx) => {
|
|
3581
|
+
for (const guard of guards) {
|
|
3582
|
+
const result = await executeSingleGuard(guard, update, ctx);
|
|
3583
|
+
if (result !== null) return result;
|
|
3584
|
+
}
|
|
3585
|
+
return BotResponse.ignore;
|
|
3586
|
+
};
|
|
3587
|
+
var executeHandler = (handler, update, ctx) => {
|
|
3588
|
+
if (typeof handler === "function") {
|
|
3589
|
+
return handler(update);
|
|
3590
|
+
}
|
|
3591
|
+
if (Array.isArray(handler)) {
|
|
3592
|
+
return executeGuards(handler, update, ctx);
|
|
3593
|
+
}
|
|
3594
|
+
if (isGuardedHandler(handler)) {
|
|
3595
|
+
return executeSingleGuard(handler, update, ctx).then(
|
|
3596
|
+
(r) => r ?? BotResponse.ignore
|
|
3597
|
+
);
|
|
3598
|
+
}
|
|
3599
|
+
return BotResponse.ignore;
|
|
3600
|
+
};
|
|
3548
3601
|
var extractUpdate = (input) => {
|
|
3549
3602
|
for (const [field, value] of Object.entries(input)) {
|
|
3550
3603
|
if (field == "update_id") {
|
|
@@ -3603,17 +3656,24 @@ var handleEntireBatch = (updates, handlers) => try_({
|
|
|
3603
3656
|
})
|
|
3604
3657
|
);
|
|
3605
3658
|
var HandleUpdateError = class extends TaggedError("HandleUpdateError") {
|
|
3659
|
+
logInfo() {
|
|
3660
|
+
return {
|
|
3661
|
+
updateId: this.update.update_id,
|
|
3662
|
+
updateKey: Object.keys(this.update).at(1),
|
|
3663
|
+
name: this._tag,
|
|
3664
|
+
...this.cause instanceof Error && { error: this.cause.message }
|
|
3665
|
+
};
|
|
3666
|
+
}
|
|
3606
3667
|
};
|
|
3607
3668
|
var handleOneByOne = (updates, handlers, pollSettings) => forEach3(
|
|
3608
3669
|
updates,
|
|
3609
3670
|
(update) => handleOneUpdate(update, handlers).pipe(
|
|
3610
3671
|
catchAll((error) => {
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
});
|
|
3672
|
+
if (error instanceof HandleUpdateError) {
|
|
3673
|
+
console.warn("update handle error", error.logInfo());
|
|
3674
|
+
} else {
|
|
3675
|
+
console.warn("unknown error", error);
|
|
3676
|
+
}
|
|
3617
3677
|
return succeed(error);
|
|
3618
3678
|
})
|
|
3619
3679
|
),
|
|
@@ -3641,8 +3701,8 @@ var handleOneUpdate = (updateObject, handlers) => gen(function* () {
|
|
|
3641
3701
|
})
|
|
3642
3702
|
);
|
|
3643
3703
|
}
|
|
3644
|
-
const
|
|
3645
|
-
if (!
|
|
3704
|
+
const handler = handlers[`on_${update.type}`];
|
|
3705
|
+
if (!handler) {
|
|
3646
3706
|
return yield* fail3(
|
|
3647
3707
|
new HandleUpdateError({
|
|
3648
3708
|
name: "HandlerNotDefined",
|
|
@@ -3657,9 +3717,10 @@ var handleOneUpdate = (updateObject, handlers) => gen(function* () {
|
|
|
3657
3717
|
message: `${update.text.slice(0, 5)}...`
|
|
3658
3718
|
});
|
|
3659
3719
|
}
|
|
3720
|
+
const ctx = createBotContext(update);
|
|
3660
3721
|
let handleUpdateError;
|
|
3661
3722
|
const handleResult = yield* try_({
|
|
3662
|
-
try: () =>
|
|
3723
|
+
try: () => executeHandler(handler, update, ctx),
|
|
3663
3724
|
catch: (error) => new HandleUpdateError({
|
|
3664
3725
|
name: "BotHandlerError",
|
|
3665
3726
|
update: updateObject,
|
|
@@ -3681,17 +3742,12 @@ var handleOneUpdate = (updateObject, handlers) => gen(function* () {
|
|
|
3681
3742
|
}),
|
|
3682
3743
|
catchAll((error) => {
|
|
3683
3744
|
handleUpdateError = error;
|
|
3684
|
-
console.
|
|
3685
|
-
updateId: updateObject.update_id,
|
|
3686
|
-
updateKey: Object.keys(updateObject).at(1),
|
|
3687
|
-
name: error._tag,
|
|
3688
|
-
...error.cause instanceof Error ? { error: error.cause.message } : void 0
|
|
3689
|
-
});
|
|
3745
|
+
console.warn("error", error.logInfo());
|
|
3690
3746
|
return succeed(
|
|
3691
3747
|
BotResponse.make({
|
|
3692
3748
|
type: "message",
|
|
3693
3749
|
text: `Some internal error has happend(${error.name}) while handling this message`,
|
|
3694
|
-
message_effect_id:
|
|
3750
|
+
message_effect_id: import_tg_bot_client.MESSAGE_EFFECTS["\u{1F4A9}"],
|
|
3695
3751
|
...updateObject.message?.message_id ? {
|
|
3696
3752
|
reply_parameters: {
|
|
3697
3753
|
message_id: updateObject.message?.message_id
|
|
@@ -3709,13 +3765,15 @@ var handleOneUpdate = (updateObject, handlers) => gen(function* () {
|
|
|
3709
3765
|
return;
|
|
3710
3766
|
}
|
|
3711
3767
|
if ("chat" in update && handleResult.response) {
|
|
3712
|
-
const
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3768
|
+
const client = yield* service(BotTgClientTag);
|
|
3769
|
+
const responsePayload = handleResult.response;
|
|
3770
|
+
const response = yield* tryPromise({
|
|
3771
|
+
try: () => client.execute(`send_${responsePayload.type}`, {
|
|
3772
|
+
...responsePayload,
|
|
3716
3773
|
chat_id: update.chat.id
|
|
3717
|
-
}
|
|
3718
|
-
|
|
3774
|
+
}),
|
|
3775
|
+
catch: (error) => error
|
|
3776
|
+
});
|
|
3719
3777
|
if (pollSettings.log_level == "debug" && "text") {
|
|
3720
3778
|
console.debug("bot response", response);
|
|
3721
3779
|
}
|
|
@@ -3724,7 +3782,6 @@ var handleOneUpdate = (updateObject, handlers) => gen(function* () {
|
|
|
3724
3782
|
});
|
|
3725
3783
|
|
|
3726
3784
|
// src/service/fetch-updates.ts
|
|
3727
|
-
var import_tg_bot_client3 = require("@effect-ak/tg-bot-client");
|
|
3728
3785
|
var BotFetchUpdatesService = class extends Reference2()(
|
|
3729
3786
|
"BotFetchUpdatesService",
|
|
3730
3787
|
{
|
|
@@ -3755,6 +3812,7 @@ var FetchUpdatesError = class extends TaggedError("FetchUpdatesError") {
|
|
|
3755
3812
|
};
|
|
3756
3813
|
var _fetchUpdates = (pollState) => gen(function* () {
|
|
3757
3814
|
const pollSettings = yield* service(BotPollSettingsTag);
|
|
3815
|
+
const client = yield* service(BotTgClientTag);
|
|
3758
3816
|
if (pollSettings.max_empty_responses && pollState.emptyResponses == pollSettings.max_empty_responses) {
|
|
3759
3817
|
return yield* fail3(
|
|
3760
3818
|
new FetchUpdatesError({ name: "TooManyEmptyResponses" })
|
|
@@ -3764,9 +3822,12 @@ var _fetchUpdates = (pollState) => gen(function* () {
|
|
|
3764
3822
|
if (pollSettings.log_level == "debug") {
|
|
3765
3823
|
console.debug("getting updates", pollState);
|
|
3766
3824
|
}
|
|
3767
|
-
const updates = yield* (
|
|
3768
|
-
|
|
3769
|
-
|
|
3825
|
+
const updates = yield* tryPromise({
|
|
3826
|
+
try: () => client.execute("get_updates", {
|
|
3827
|
+
timeout: pollSettings.poll_timeout,
|
|
3828
|
+
...updateId ? { offset: updateId } : void 0
|
|
3829
|
+
}),
|
|
3830
|
+
catch: (error) => error
|
|
3770
3831
|
}).pipe(andThen((_) => _.sort((_2) => _2.update_id)));
|
|
3771
3832
|
if (updates.length) {
|
|
3772
3833
|
console.debug(`got a batch of updates (${updates.length})`);
|
|
@@ -3780,9 +3841,14 @@ var _fetchUpdates = (pollState) => gen(function* () {
|
|
|
3780
3841
|
var _commitLastBatch = (pollState) => gen(function* () {
|
|
3781
3842
|
console.log("commit", { pollState });
|
|
3782
3843
|
if (pollState.lastUpdateId) {
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3844
|
+
const client = yield* service(BotTgClientTag);
|
|
3845
|
+
const offset = pollState.lastUpdateId;
|
|
3846
|
+
return yield* tryPromise({
|
|
3847
|
+
try: () => client.execute("get_updates", {
|
|
3848
|
+
offset,
|
|
3849
|
+
limit: 0
|
|
3850
|
+
}),
|
|
3851
|
+
catch: (error) => error
|
|
3786
3852
|
}).pipe(
|
|
3787
3853
|
andThen(
|
|
3788
3854
|
andThen(service(BotPollSettingsTag), (pollSettings) => {
|
|
@@ -3856,21 +3922,30 @@ var _runBotDaemon = (state) => gen(function* () {
|
|
|
3856
3922
|
});
|
|
3857
3923
|
|
|
3858
3924
|
// src/internal/launch.ts
|
|
3859
|
-
var
|
|
3925
|
+
var import_tg_bot_client2 = require("@effect-ak/tg-bot-client");
|
|
3926
|
+
var extractMode = (input) => {
|
|
3927
|
+
if (input.mode === "batch") {
|
|
3928
|
+
return { type: "batch", on_batch: input.on_batch };
|
|
3929
|
+
}
|
|
3930
|
+
const { bot_token, mode, poll, ...handlers } = input;
|
|
3931
|
+
return { type: "single", ...handlers };
|
|
3932
|
+
};
|
|
3860
3933
|
var launchBot = (input) => gen(function* () {
|
|
3861
3934
|
const service2 = yield* service(BotRunService);
|
|
3862
|
-
const
|
|
3935
|
+
const client = (0, import_tg_bot_client2.makeTgBotClient)({ bot_token: input.bot_token });
|
|
3936
|
+
const contextWithClient = make4(BotTgClientTag, client);
|
|
3937
|
+
const mode = extractMode(input);
|
|
3863
3938
|
yield* service2.runBotInBackground.pipe(
|
|
3864
|
-
provideService(BotUpdateHandlersTag,
|
|
3939
|
+
provideService(BotUpdateHandlersTag, mode),
|
|
3865
3940
|
provideService(
|
|
3866
3941
|
BotPollSettingsTag,
|
|
3867
3942
|
BotPollSettings.make(input.poll ?? {})
|
|
3868
3943
|
),
|
|
3869
|
-
provideContext(
|
|
3944
|
+
provideContext(contextWithClient)
|
|
3870
3945
|
);
|
|
3871
|
-
const reload = (
|
|
3872
|
-
provideService(BotUpdateHandlersTag,
|
|
3873
|
-
provideContext(
|
|
3946
|
+
const reload = (mode2) => service2.runBotInBackground.pipe(
|
|
3947
|
+
provideService(BotUpdateHandlersTag, mode2),
|
|
3948
|
+
provideContext(contextWithClient),
|
|
3874
3949
|
runPromise
|
|
3875
3950
|
);
|
|
3876
3951
|
return {
|
|
@@ -3893,8 +3968,10 @@ var defineBot = (input) => {
|
|
|
3893
3968
|
BotPollSettingsTag,
|
|
3894
3969
|
BotResponse,
|
|
3895
3970
|
BotRunService,
|
|
3971
|
+
BotTgClientTag,
|
|
3896
3972
|
BotUpdateHandlersTag,
|
|
3897
3973
|
HandleUpdateError,
|
|
3974
|
+
createBotContext,
|
|
3898
3975
|
defineBot,
|
|
3899
3976
|
extractUpdate,
|
|
3900
3977
|
handleEntireBatch,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import * as effect_Types from 'effect/Types';
|
|
2
2
|
import { Api, Update } from '@effect-ak/tg-bot-api';
|
|
3
|
+
import * as Context from 'effect/Context';
|
|
4
|
+
import * as Data from 'effect/Data';
|
|
5
|
+
import { TgBotClient } from '@effect-ak/tg-bot-client';
|
|
3
6
|
import * as effect_Cause from 'effect/Cause';
|
|
4
|
-
import * as _effect_ak_tg_bot_client from '@effect-ak/tg-bot-client';
|
|
5
|
-
import { TgBotClientError } from '@effect-ak/tg-bot-client';
|
|
6
7
|
import * as Micro from 'effect/Micro';
|
|
7
|
-
import * as Data from 'effect/Data';
|
|
8
|
-
import * as Context from 'effect/Context';
|
|
9
8
|
|
|
10
9
|
type BotResult = {
|
|
11
10
|
[K in keyof Api]: K extends `send_${infer R}` ? {
|
|
@@ -25,6 +24,9 @@ declare class BotResponse extends BotResponse_base<{
|
|
|
25
24
|
declare const BotUpdateHandlersTag_base: Context.TagClass<BotUpdateHandlersTag, "BotUpdateHandlers", BotMode>;
|
|
26
25
|
declare class BotUpdateHandlersTag extends BotUpdateHandlersTag_base {
|
|
27
26
|
}
|
|
27
|
+
declare const BotTgClientTag_base: Context.TagClass<BotTgClientTag, "BotTgClient", TgBotClient>;
|
|
28
|
+
declare class BotTgClientTag extends BotTgClientTag_base {
|
|
29
|
+
}
|
|
28
30
|
interface PollSettings {
|
|
29
31
|
log_level: "info" | "debug";
|
|
30
32
|
on_error: "stop" | "continue";
|
|
@@ -39,9 +41,15 @@ declare const BotPollSettingsTag_base: Context.ReferenceClass<BotPollSettings, "
|
|
|
39
41
|
declare class BotPollSettingsTag extends BotPollSettingsTag_base {
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
type RunBotInput = RunBotInputSingle | RunBotInputBatch;
|
|
45
|
+
interface RunBotInputSingle extends BotUpdatesHandlers {
|
|
43
46
|
bot_token: string;
|
|
44
|
-
mode:
|
|
47
|
+
mode: "single";
|
|
48
|
+
poll?: Partial<PollSettings>;
|
|
49
|
+
}
|
|
50
|
+
interface RunBotInputBatch extends HandleBatchUpdateFunction {
|
|
51
|
+
bot_token: string;
|
|
52
|
+
mode: "batch";
|
|
45
53
|
poll?: Partial<PollSettings>;
|
|
46
54
|
}
|
|
47
55
|
type ExtractedUpdate<K extends AvailableUpdateTypes> = {
|
|
@@ -49,8 +57,27 @@ type ExtractedUpdate<K extends AvailableUpdateTypes> = {
|
|
|
49
57
|
} & Update[K];
|
|
50
58
|
type AvailableUpdateTypes = Exclude<keyof Update, "update_id">;
|
|
51
59
|
type HandleUpdateFunction<U> = (update: U) => BotResponse | PromiseLike<BotResponse>;
|
|
60
|
+
interface BotContext {
|
|
61
|
+
readonly command: string | undefined;
|
|
62
|
+
readonly reply: (text: string, options?: Omit<BotResponseParams<"message">, "text" | "type">) => BotResponse;
|
|
63
|
+
readonly replyWithDocument: (document: BotResponseParams<"document">["document"], options?: Omit<BotResponseParams<"document">, "document" | "type">) => BotResponse;
|
|
64
|
+
readonly replyWithPhoto: (photo: BotResponseParams<"photo">["photo"], options?: Omit<BotResponseParams<"photo">, "photo" | "type">) => BotResponse;
|
|
65
|
+
readonly ignore: BotResponse;
|
|
66
|
+
}
|
|
67
|
+
type BotResponseParams<T extends string> = Extract<Parameters<typeof BotResponse.make>[0], {
|
|
68
|
+
type: T;
|
|
69
|
+
}>;
|
|
70
|
+
interface HandlerInput<U> {
|
|
71
|
+
readonly update: U;
|
|
72
|
+
readonly ctx: BotContext;
|
|
73
|
+
}
|
|
74
|
+
interface GuardedHandler<U> {
|
|
75
|
+
readonly match?: (input: HandlerInput<U>) => boolean | PromiseLike<boolean>;
|
|
76
|
+
readonly handle: (input: HandlerInput<U>) => BotResponse | PromiseLike<BotResponse>;
|
|
77
|
+
}
|
|
78
|
+
type UpdateHandler<U> = HandleUpdateFunction<U> | GuardedHandler<U> | GuardedHandler<U>[];
|
|
52
79
|
type BotUpdatesHandlers = {
|
|
53
|
-
|
|
80
|
+
[K in AvailableUpdateTypes as `on_${K}`]?: UpdateHandler<NonNullable<Update[K]>>;
|
|
54
81
|
};
|
|
55
82
|
interface HandleBatchUpdateFunction {
|
|
56
83
|
readonly on_batch: (update: Update[]) => boolean | PromiseLike<boolean>;
|
|
@@ -63,13 +90,15 @@ interface BotBatchMode extends HandleBatchUpdateFunction {
|
|
|
63
90
|
}
|
|
64
91
|
type BotMode = BotSingleMode | BotBatchMode;
|
|
65
92
|
|
|
93
|
+
declare const createBotContext: (update: unknown) => BotContext;
|
|
94
|
+
|
|
66
95
|
declare const extractUpdate: <U extends AvailableUpdateTypes>(input: Update) => ExtractedUpdate<U> | undefined;
|
|
67
96
|
declare class BatchUpdateResult extends Data.Class<{
|
|
68
97
|
hasErrors: boolean;
|
|
69
98
|
updates: Update[];
|
|
70
99
|
}> {
|
|
71
100
|
}
|
|
72
|
-
declare const handleUpdates: (updates: Update[]) => Micro.Micro<BatchUpdateResult, never, BotUpdateHandlersTag |
|
|
101
|
+
declare const handleUpdates: (updates: Update[]) => Micro.Micro<BatchUpdateResult, never, BotUpdateHandlersTag | BotTgClientTag>;
|
|
73
102
|
declare const handleEntireBatch: (updates: Update[], handlers: HandleBatchUpdateFunction) => Micro.Micro<BatchUpdateResult, never, never>;
|
|
74
103
|
declare const HandleUpdateError_base: new <A extends Record<string, any> = {}>(args: effect_Types.Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => effect_Cause.YieldableError & {
|
|
75
104
|
readonly _tag: "HandleUpdateError";
|
|
@@ -79,35 +108,33 @@ declare class HandleUpdateError extends HandleUpdateError_base<{
|
|
|
79
108
|
update: Update;
|
|
80
109
|
cause?: unknown;
|
|
81
110
|
}> {
|
|
111
|
+
logInfo(): {
|
|
112
|
+
error?: string;
|
|
113
|
+
updateId: number;
|
|
114
|
+
updateKey: string | undefined;
|
|
115
|
+
name: "HandleUpdateError";
|
|
116
|
+
};
|
|
82
117
|
}
|
|
83
|
-
declare const handleOneByOne: (updates: Update[], handlers: BotUpdatesHandlers, pollSettings: PollSettings) => Micro.Micro<BatchUpdateResult, never,
|
|
84
|
-
declare const handleOneUpdate: (updateObject: Update, handlers: BotUpdatesHandlers) => Micro.Micro<HandleUpdateError | undefined,
|
|
85
|
-
|
|
86
|
-
declare const FetchUpdatesError_base: new <A extends Record<string, any> = {}>(args: effect_Types.Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => effect_Cause.YieldableError & {
|
|
87
|
-
readonly _tag: "FetchUpdatesError";
|
|
88
|
-
} & Readonly<A>;
|
|
89
|
-
declare class FetchUpdatesError extends FetchUpdatesError_base<{
|
|
90
|
-
name: "TooManyEmptyResponses" | "NoUpdatesToCommit";
|
|
91
|
-
}> {
|
|
92
|
-
}
|
|
118
|
+
declare const handleOneByOne: (updates: Update[], handlers: BotUpdatesHandlers, pollSettings: PollSettings) => Micro.Micro<BatchUpdateResult, never, BotTgClientTag>;
|
|
119
|
+
declare const handleOneUpdate: (updateObject: Update, handlers: BotUpdatesHandlers) => Micro.Micro<HandleUpdateError | undefined, unknown, BotTgClientTag>;
|
|
93
120
|
|
|
94
121
|
type BotInstance = Micro.Micro.Success<ReturnType<typeof launchBot>>;
|
|
95
122
|
declare const launchBot: (input: RunBotInput) => Micro.Micro<{
|
|
96
123
|
readonly reload: (mode: BotMode) => Promise<void>;
|
|
97
|
-
readonly fiber: () => Micro.MicroFiber<BatchUpdateResult,
|
|
124
|
+
readonly fiber: () => Micro.MicroFiber<BatchUpdateResult, unknown> | undefined;
|
|
98
125
|
}, never, never>;
|
|
99
126
|
|
|
100
127
|
declare const BotRunService_base: Context.ReferenceClass<BotRunService, "BotRunService", {
|
|
101
|
-
readonly runBotInBackground: Micro.Micro<void, never, BotUpdateHandlersTag |
|
|
102
|
-
readonly getFiber: () => Micro.MicroFiber<BatchUpdateResult,
|
|
128
|
+
readonly runBotInBackground: Micro.Micro<void, never, BotUpdateHandlersTag | BotTgClientTag>;
|
|
129
|
+
readonly getFiber: () => Micro.MicroFiber<BatchUpdateResult, unknown> | undefined;
|
|
103
130
|
}>;
|
|
104
131
|
declare class BotRunService extends BotRunService_base {
|
|
105
132
|
}
|
|
106
133
|
|
|
107
134
|
declare const runTgChatBot: (input: RunBotInput) => Promise<{
|
|
108
135
|
readonly reload: (mode: BotMode) => Promise<void>;
|
|
109
|
-
readonly fiber: () => Micro.MicroFiber<BatchUpdateResult,
|
|
136
|
+
readonly fiber: () => Micro.MicroFiber<BatchUpdateResult, unknown> | undefined;
|
|
110
137
|
}>;
|
|
111
138
|
declare const defineBot: (input: BotUpdatesHandlers) => BotUpdatesHandlers;
|
|
112
139
|
|
|
113
|
-
export { type AvailableUpdateTypes, BatchUpdateResult, type BotBatchMode, type BotInstance, type BotMode, BotPollSettings, BotPollSettingsTag, BotResponse, BotRunService, type BotSingleMode, BotUpdateHandlersTag, type BotUpdatesHandlers, type ExtractedUpdate, type HandleBatchUpdateFunction, HandleUpdateError, type HandleUpdateFunction, type PollSettings, type RunBotInput, defineBot, extractUpdate, handleEntireBatch, handleOneByOne, handleOneUpdate, handleUpdates, launchBot, runTgChatBot };
|
|
140
|
+
export { type AvailableUpdateTypes, BatchUpdateResult, type BotBatchMode, type BotContext, type BotInstance, type BotMode, BotPollSettings, BotPollSettingsTag, BotResponse, BotRunService, type BotSingleMode, BotTgClientTag, BotUpdateHandlersTag, type BotUpdatesHandlers, type ExtractedUpdate, type GuardedHandler, type HandleBatchUpdateFunction, HandleUpdateError, type HandleUpdateFunction, type HandlerInput, type PollSettings, type RunBotInput, type RunBotInputBatch, type RunBotInputSingle, type UpdateHandler, createBotContext, defineBot, extractUpdate, handleEntireBatch, handleOneByOne, handleOneUpdate, handleUpdates, launchBot, runTgChatBot };
|
package/dist/index.js
CHANGED
|
@@ -2615,6 +2615,26 @@ var BotResponse = class _BotResponse extends TaggedClass("BotResponse") {
|
|
|
2615
2615
|
static ignore = new _BotResponse({});
|
|
2616
2616
|
};
|
|
2617
2617
|
|
|
2618
|
+
// src/internal/bot-context.ts
|
|
2619
|
+
var extractCommand = (update) => {
|
|
2620
|
+
if (typeof update !== "object" || update === null) return void 0;
|
|
2621
|
+
const u = update;
|
|
2622
|
+
if (!u.entities || !u.text) return void 0;
|
|
2623
|
+
const entity = u.entities.find((e) => e.type === "bot_command");
|
|
2624
|
+
if (!entity) return void 0;
|
|
2625
|
+
return u.text.slice(entity.offset, entity.offset + entity.length);
|
|
2626
|
+
};
|
|
2627
|
+
var createBotContext = (update) => {
|
|
2628
|
+
const command = extractCommand(update);
|
|
2629
|
+
return {
|
|
2630
|
+
command,
|
|
2631
|
+
reply: (text, options) => BotResponse.make({ type: "message", text, ...options }),
|
|
2632
|
+
replyWithDocument: (document, options) => BotResponse.make({ type: "document", document, ...options }),
|
|
2633
|
+
replyWithPhoto: (photo, options) => BotResponse.make({ type: "photo", photo, ...options }),
|
|
2634
|
+
ignore: BotResponse.ignore
|
|
2635
|
+
};
|
|
2636
|
+
};
|
|
2637
|
+
|
|
2618
2638
|
// ../../node_modules/.pnpm/effect@3.12.0/node_modules/effect/dist/esm/Effectable.js
|
|
2619
2639
|
var EffectPrototype2 = EffectPrototype;
|
|
2620
2640
|
|
|
@@ -3450,12 +3470,13 @@ var runPromise = (effect, options) => runPromiseExit(effect, options).then((exit
|
|
|
3450
3470
|
});
|
|
3451
3471
|
|
|
3452
3472
|
// src/internal/handle-update.ts
|
|
3453
|
-
import { executeTgBotMethod } from "@effect-ak/tg-bot-client";
|
|
3454
3473
|
import { MESSAGE_EFFECTS } from "@effect-ak/tg-bot-client";
|
|
3455
3474
|
|
|
3456
3475
|
// src/internal/poll-settings.ts
|
|
3457
3476
|
var BotUpdateHandlersTag = class extends Tag2("BotUpdateHandlers")() {
|
|
3458
3477
|
};
|
|
3478
|
+
var BotTgClientTag = class extends Tag2("BotTgClient")() {
|
|
3479
|
+
};
|
|
3459
3480
|
var BotPollSettings = class _BotPollSettings extends Class2 {
|
|
3460
3481
|
static make(input) {
|
|
3461
3482
|
let batch_size = input.batch_size ?? 10;
|
|
@@ -3505,6 +3526,36 @@ var BotPollSettingsTag = class extends Reference2()(
|
|
|
3505
3526
|
};
|
|
3506
3527
|
|
|
3507
3528
|
// src/internal/handle-update.ts
|
|
3529
|
+
var isGuardedHandler = (handler) => typeof handler === "object" && handler !== null && "handle" in handler;
|
|
3530
|
+
var executeSingleGuard = async (guard, update, ctx) => {
|
|
3531
|
+
const input = { update, ctx };
|
|
3532
|
+
if (guard.match) {
|
|
3533
|
+
const matched = await guard.match(input);
|
|
3534
|
+
if (!matched) return null;
|
|
3535
|
+
}
|
|
3536
|
+
return await guard.handle(input);
|
|
3537
|
+
};
|
|
3538
|
+
var executeGuards = async (guards, update, ctx) => {
|
|
3539
|
+
for (const guard of guards) {
|
|
3540
|
+
const result = await executeSingleGuard(guard, update, ctx);
|
|
3541
|
+
if (result !== null) return result;
|
|
3542
|
+
}
|
|
3543
|
+
return BotResponse.ignore;
|
|
3544
|
+
};
|
|
3545
|
+
var executeHandler = (handler, update, ctx) => {
|
|
3546
|
+
if (typeof handler === "function") {
|
|
3547
|
+
return handler(update);
|
|
3548
|
+
}
|
|
3549
|
+
if (Array.isArray(handler)) {
|
|
3550
|
+
return executeGuards(handler, update, ctx);
|
|
3551
|
+
}
|
|
3552
|
+
if (isGuardedHandler(handler)) {
|
|
3553
|
+
return executeSingleGuard(handler, update, ctx).then(
|
|
3554
|
+
(r) => r ?? BotResponse.ignore
|
|
3555
|
+
);
|
|
3556
|
+
}
|
|
3557
|
+
return BotResponse.ignore;
|
|
3558
|
+
};
|
|
3508
3559
|
var extractUpdate = (input) => {
|
|
3509
3560
|
for (const [field, value] of Object.entries(input)) {
|
|
3510
3561
|
if (field == "update_id") {
|
|
@@ -3563,17 +3614,24 @@ var handleEntireBatch = (updates, handlers) => try_({
|
|
|
3563
3614
|
})
|
|
3564
3615
|
);
|
|
3565
3616
|
var HandleUpdateError = class extends TaggedError("HandleUpdateError") {
|
|
3617
|
+
logInfo() {
|
|
3618
|
+
return {
|
|
3619
|
+
updateId: this.update.update_id,
|
|
3620
|
+
updateKey: Object.keys(this.update).at(1),
|
|
3621
|
+
name: this._tag,
|
|
3622
|
+
...this.cause instanceof Error && { error: this.cause.message }
|
|
3623
|
+
};
|
|
3624
|
+
}
|
|
3566
3625
|
};
|
|
3567
3626
|
var handleOneByOne = (updates, handlers, pollSettings) => forEach3(
|
|
3568
3627
|
updates,
|
|
3569
3628
|
(update) => handleOneUpdate(update, handlers).pipe(
|
|
3570
3629
|
catchAll((error) => {
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
});
|
|
3630
|
+
if (error instanceof HandleUpdateError) {
|
|
3631
|
+
console.warn("update handle error", error.logInfo());
|
|
3632
|
+
} else {
|
|
3633
|
+
console.warn("unknown error", error);
|
|
3634
|
+
}
|
|
3577
3635
|
return succeed(error);
|
|
3578
3636
|
})
|
|
3579
3637
|
),
|
|
@@ -3601,8 +3659,8 @@ var handleOneUpdate = (updateObject, handlers) => gen(function* () {
|
|
|
3601
3659
|
})
|
|
3602
3660
|
);
|
|
3603
3661
|
}
|
|
3604
|
-
const
|
|
3605
|
-
if (!
|
|
3662
|
+
const handler = handlers[`on_${update.type}`];
|
|
3663
|
+
if (!handler) {
|
|
3606
3664
|
return yield* fail3(
|
|
3607
3665
|
new HandleUpdateError({
|
|
3608
3666
|
name: "HandlerNotDefined",
|
|
@@ -3617,9 +3675,10 @@ var handleOneUpdate = (updateObject, handlers) => gen(function* () {
|
|
|
3617
3675
|
message: `${update.text.slice(0, 5)}...`
|
|
3618
3676
|
});
|
|
3619
3677
|
}
|
|
3678
|
+
const ctx = createBotContext(update);
|
|
3620
3679
|
let handleUpdateError;
|
|
3621
3680
|
const handleResult = yield* try_({
|
|
3622
|
-
try: () =>
|
|
3681
|
+
try: () => executeHandler(handler, update, ctx),
|
|
3623
3682
|
catch: (error) => new HandleUpdateError({
|
|
3624
3683
|
name: "BotHandlerError",
|
|
3625
3684
|
update: updateObject,
|
|
@@ -3641,12 +3700,7 @@ var handleOneUpdate = (updateObject, handlers) => gen(function* () {
|
|
|
3641
3700
|
}),
|
|
3642
3701
|
catchAll((error) => {
|
|
3643
3702
|
handleUpdateError = error;
|
|
3644
|
-
console.
|
|
3645
|
-
updateId: updateObject.update_id,
|
|
3646
|
-
updateKey: Object.keys(updateObject).at(1),
|
|
3647
|
-
name: error._tag,
|
|
3648
|
-
...error.cause instanceof Error ? { error: error.cause.message } : void 0
|
|
3649
|
-
});
|
|
3703
|
+
console.warn("error", error.logInfo());
|
|
3650
3704
|
return succeed(
|
|
3651
3705
|
BotResponse.make({
|
|
3652
3706
|
type: "message",
|
|
@@ -3669,13 +3723,15 @@ var handleOneUpdate = (updateObject, handlers) => gen(function* () {
|
|
|
3669
3723
|
return;
|
|
3670
3724
|
}
|
|
3671
3725
|
if ("chat" in update && handleResult.response) {
|
|
3672
|
-
const
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3726
|
+
const client = yield* service(BotTgClientTag);
|
|
3727
|
+
const responsePayload = handleResult.response;
|
|
3728
|
+
const response = yield* tryPromise({
|
|
3729
|
+
try: () => client.execute(`send_${responsePayload.type}`, {
|
|
3730
|
+
...responsePayload,
|
|
3676
3731
|
chat_id: update.chat.id
|
|
3677
|
-
}
|
|
3678
|
-
|
|
3732
|
+
}),
|
|
3733
|
+
catch: (error) => error
|
|
3734
|
+
});
|
|
3679
3735
|
if (pollSettings.log_level == "debug" && "text") {
|
|
3680
3736
|
console.debug("bot response", response);
|
|
3681
3737
|
}
|
|
@@ -3684,7 +3740,6 @@ var handleOneUpdate = (updateObject, handlers) => gen(function* () {
|
|
|
3684
3740
|
});
|
|
3685
3741
|
|
|
3686
3742
|
// src/service/fetch-updates.ts
|
|
3687
|
-
import { executeTgBotMethod as executeTgBotMethod2 } from "@effect-ak/tg-bot-client";
|
|
3688
3743
|
var BotFetchUpdatesService = class extends Reference2()(
|
|
3689
3744
|
"BotFetchUpdatesService",
|
|
3690
3745
|
{
|
|
@@ -3715,6 +3770,7 @@ var FetchUpdatesError = class extends TaggedError("FetchUpdatesError") {
|
|
|
3715
3770
|
};
|
|
3716
3771
|
var _fetchUpdates = (pollState) => gen(function* () {
|
|
3717
3772
|
const pollSettings = yield* service(BotPollSettingsTag);
|
|
3773
|
+
const client = yield* service(BotTgClientTag);
|
|
3718
3774
|
if (pollSettings.max_empty_responses && pollState.emptyResponses == pollSettings.max_empty_responses) {
|
|
3719
3775
|
return yield* fail3(
|
|
3720
3776
|
new FetchUpdatesError({ name: "TooManyEmptyResponses" })
|
|
@@ -3724,9 +3780,12 @@ var _fetchUpdates = (pollState) => gen(function* () {
|
|
|
3724
3780
|
if (pollSettings.log_level == "debug") {
|
|
3725
3781
|
console.debug("getting updates", pollState);
|
|
3726
3782
|
}
|
|
3727
|
-
const updates = yield*
|
|
3728
|
-
|
|
3729
|
-
|
|
3783
|
+
const updates = yield* tryPromise({
|
|
3784
|
+
try: () => client.execute("get_updates", {
|
|
3785
|
+
timeout: pollSettings.poll_timeout,
|
|
3786
|
+
...updateId ? { offset: updateId } : void 0
|
|
3787
|
+
}),
|
|
3788
|
+
catch: (error) => error
|
|
3730
3789
|
}).pipe(andThen((_) => _.sort((_2) => _2.update_id)));
|
|
3731
3790
|
if (updates.length) {
|
|
3732
3791
|
console.debug(`got a batch of updates (${updates.length})`);
|
|
@@ -3740,9 +3799,14 @@ var _fetchUpdates = (pollState) => gen(function* () {
|
|
|
3740
3799
|
var _commitLastBatch = (pollState) => gen(function* () {
|
|
3741
3800
|
console.log("commit", { pollState });
|
|
3742
3801
|
if (pollState.lastUpdateId) {
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3802
|
+
const client = yield* service(BotTgClientTag);
|
|
3803
|
+
const offset = pollState.lastUpdateId;
|
|
3804
|
+
return yield* tryPromise({
|
|
3805
|
+
try: () => client.execute("get_updates", {
|
|
3806
|
+
offset,
|
|
3807
|
+
limit: 0
|
|
3808
|
+
}),
|
|
3809
|
+
catch: (error) => error
|
|
3746
3810
|
}).pipe(
|
|
3747
3811
|
andThen(
|
|
3748
3812
|
andThen(service(BotPollSettingsTag), (pollSettings) => {
|
|
@@ -3816,21 +3880,30 @@ var _runBotDaemon = (state) => gen(function* () {
|
|
|
3816
3880
|
});
|
|
3817
3881
|
|
|
3818
3882
|
// src/internal/launch.ts
|
|
3819
|
-
import {
|
|
3883
|
+
import { makeTgBotClient } from "@effect-ak/tg-bot-client";
|
|
3884
|
+
var extractMode = (input) => {
|
|
3885
|
+
if (input.mode === "batch") {
|
|
3886
|
+
return { type: "batch", on_batch: input.on_batch };
|
|
3887
|
+
}
|
|
3888
|
+
const { bot_token, mode, poll, ...handlers } = input;
|
|
3889
|
+
return { type: "single", ...handlers };
|
|
3890
|
+
};
|
|
3820
3891
|
var launchBot = (input) => gen(function* () {
|
|
3821
3892
|
const service2 = yield* service(BotRunService);
|
|
3822
|
-
const
|
|
3893
|
+
const client = makeTgBotClient({ bot_token: input.bot_token });
|
|
3894
|
+
const contextWithClient = make4(BotTgClientTag, client);
|
|
3895
|
+
const mode = extractMode(input);
|
|
3823
3896
|
yield* service2.runBotInBackground.pipe(
|
|
3824
|
-
provideService(BotUpdateHandlersTag,
|
|
3897
|
+
provideService(BotUpdateHandlersTag, mode),
|
|
3825
3898
|
provideService(
|
|
3826
3899
|
BotPollSettingsTag,
|
|
3827
3900
|
BotPollSettings.make(input.poll ?? {})
|
|
3828
3901
|
),
|
|
3829
|
-
provideContext(
|
|
3902
|
+
provideContext(contextWithClient)
|
|
3830
3903
|
);
|
|
3831
|
-
const reload = (
|
|
3832
|
-
provideService(BotUpdateHandlersTag,
|
|
3833
|
-
provideContext(
|
|
3904
|
+
const reload = (mode2) => service2.runBotInBackground.pipe(
|
|
3905
|
+
provideService(BotUpdateHandlersTag, mode2),
|
|
3906
|
+
provideContext(contextWithClient),
|
|
3834
3907
|
runPromise
|
|
3835
3908
|
);
|
|
3836
3909
|
return {
|
|
@@ -3852,8 +3925,10 @@ export {
|
|
|
3852
3925
|
BotPollSettingsTag,
|
|
3853
3926
|
BotResponse,
|
|
3854
3927
|
BotRunService,
|
|
3928
|
+
BotTgClientTag,
|
|
3855
3929
|
BotUpdateHandlersTag,
|
|
3856
3930
|
HandleUpdateError,
|
|
3931
|
+
createBotContext,
|
|
3857
3932
|
defineBot,
|
|
3858
3933
|
extractUpdate,
|
|
3859
3934
|
handleEntireBatch,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-ak/tg-bot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Telegram Bot runner",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,10 +34,14 @@
|
|
|
34
34
|
"dist/*.d.ts"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@effect-ak/tg-bot-client": "1.
|
|
37
|
+
"@effect-ak/tg-bot-client": "^1.1.0",
|
|
38
38
|
"@effect-ak/tg-bot-api": "0.9.2"
|
|
39
39
|
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"effect": "^3.12.7"
|
|
42
|
+
},
|
|
40
43
|
"scripts": {
|
|
41
|
-
"build": "tsup"
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"typecheck": "tsc"
|
|
42
46
|
}
|
|
43
47
|
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
# @effect-ak/tg-bot
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@effect-ak/tg-bot)
|
|
4
|
+

|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
Effect-based Telegram bot runner that handles long polling, update processing, and error management automatically.
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Features](#features)
|
|
12
|
+
- [Installation](#installation)
|
|
13
|
+
- [Quick Start](#quick-start)
|
|
14
|
+
- [Core Concepts](#core-concepts)
|
|
15
|
+
- [Single Mode](#single-mode)
|
|
16
|
+
- [Batch Mode](#batch-mode)
|
|
17
|
+
- [Bot Response](#bot-response)
|
|
18
|
+
- [Usage Examples](#usage-examples)
|
|
19
|
+
- [Echo Bot](#echo-bot)
|
|
20
|
+
- [Command Handler](#command-handler)
|
|
21
|
+
- [Batch Processing](#batch-processing)
|
|
22
|
+
- [Using Effect](#using-effectjs)
|
|
23
|
+
- [Hot Reload](#hot-reload)
|
|
24
|
+
- [Configuration](#configuration)
|
|
25
|
+
- [API Reference](#api-reference)
|
|
26
|
+
- [How It Works](#how-it-works)
|
|
27
|
+
- [Error Handling](#error-handling)
|
|
28
|
+
- [Playground](#playground)
|
|
29
|
+
- [Related Packages](#related-packages)
|
|
30
|
+
- [License](#license)
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- **Effect-based**: Built on top of [Effect](https://effect.website/) for powerful functional programming patterns
|
|
35
|
+
- **Two Processing Modes**: Handle updates one-by-one or in batches
|
|
36
|
+
- **Automatic Long Polling**: Manages connection to Telegram servers
|
|
37
|
+
- **Type-Safe Handlers**: Full TypeScript support for all update types
|
|
38
|
+
- **Error Recovery**: Configurable error handling strategies
|
|
39
|
+
- **Concurrent Processing**: Process multiple updates in parallel (up to 10 concurrent handlers)
|
|
40
|
+
- **Hot Reload**: Reload bot handlers without restarting
|
|
41
|
+
- **Built-in Logging**: Configurable logging levels
|
|
42
|
+
- **No Public URL Required**: Uses pull model - run bots anywhere, even in a browser
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install @effect-ak/tg-bot effect
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pnpm add @effect-ak/tg-bot effect
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
yarn add @effect-ak/tg-bot effect
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Note:** `effect` is a peer dependency and must be installed separately.
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { runTgChatBot } from "@effect-ak/tg-bot"
|
|
64
|
+
|
|
65
|
+
runTgChatBot({
|
|
66
|
+
bot_token: "YOUR_BOT_TOKEN",
|
|
67
|
+
mode: "single",
|
|
68
|
+
on_message: [
|
|
69
|
+
{
|
|
70
|
+
match: ({ update }) => !!update.text,
|
|
71
|
+
handle: ({ update, ctx }) => ctx.reply(`You said: ${update.text}`)
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Core Concepts
|
|
78
|
+
|
|
79
|
+
### Single Mode
|
|
80
|
+
|
|
81
|
+
In single mode, the bot processes each update individually with a dedicated handler for each update type.
|
|
82
|
+
|
|
83
|
+
**Handler Format (v2 with guards):**
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
on_message: [
|
|
87
|
+
{
|
|
88
|
+
match: ({ update, ctx }) => ctx.command === "/start", // optional filter
|
|
89
|
+
handle: ({ update, ctx }) => ctx.reply("Welcome!") // handler
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
match: ({ update }) => !!update.text,
|
|
93
|
+
handle: ({ ctx }) => ctx.reply("Got your message!")
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
handle: ({ ctx }) => ctx.ignore // fallback (no match = always runs)
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Context helpers:**
|
|
102
|
+
- `ctx.reply(text, options?)` - Send a message
|
|
103
|
+
- `ctx.replyWithDocument(document, options?)` - Send a document
|
|
104
|
+
- `ctx.replyWithPhoto(photo, options?)` - Send a photo
|
|
105
|
+
- `ctx.command` - Parsed command (e.g., "/start", "/help")
|
|
106
|
+
- `ctx.ignore` - Skip response
|
|
107
|
+
|
|
108
|
+
**Available Handlers:**
|
|
109
|
+
- `on_message` - New incoming message
|
|
110
|
+
- `on_edited_message` - Message was edited
|
|
111
|
+
- `on_channel_post` - New channel post
|
|
112
|
+
- `on_edited_channel_post` - Channel post was edited
|
|
113
|
+
- `on_inline_query` - Inline query
|
|
114
|
+
- `on_chosen_inline_result` - Chosen inline result
|
|
115
|
+
- `on_callback_query` - Callback query from inline keyboard
|
|
116
|
+
- `on_shipping_query` - Shipping query
|
|
117
|
+
- `on_pre_checkout_query` - Pre-checkout query
|
|
118
|
+
- `on_poll` - Poll state update
|
|
119
|
+
- `on_poll_answer` - User changed their answer in a poll
|
|
120
|
+
- `on_my_chat_member` - Bot's chat member status changed
|
|
121
|
+
- `on_chat_member` - Chat member status changed
|
|
122
|
+
- `on_chat_join_request` - Request to join chat
|
|
123
|
+
|
|
124
|
+
**Legacy format (v1 - still supported):**
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
on_message: (message) => {
|
|
128
|
+
if (!message.text) return BotResponse.ignore
|
|
129
|
+
return BotResponse.make({ type: "message", text: "Hello!" })
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Batch Mode
|
|
134
|
+
|
|
135
|
+
In batch mode, the bot receives all updates as an array and processes them together.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
runTgChatBot({
|
|
139
|
+
bot_token: "YOUR_BOT_TOKEN",
|
|
140
|
+
mode: "batch",
|
|
141
|
+
on_batch: async (updates) => {
|
|
142
|
+
console.log(`Processing ${updates.length} updates`)
|
|
143
|
+
// Process updates...
|
|
144
|
+
return true // Continue polling
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Bot Response
|
|
150
|
+
|
|
151
|
+
Handlers return a `BotResponse` object that describes what to send back to the user.
|
|
152
|
+
|
|
153
|
+
**Creating Responses:**
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { BotResponse } from "@effect-ak/tg-bot"
|
|
157
|
+
|
|
158
|
+
// Send a message
|
|
159
|
+
BotResponse.make({
|
|
160
|
+
type: "message",
|
|
161
|
+
text: "Hello!"
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// Send a photo
|
|
165
|
+
BotResponse.make({
|
|
166
|
+
type: "photo",
|
|
167
|
+
photo: {
|
|
168
|
+
file_content: photoBuffer,
|
|
169
|
+
file_name: "image.jpg"
|
|
170
|
+
},
|
|
171
|
+
caption: "Check this out!"
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// Ignore update (don't send anything)
|
|
175
|
+
BotResponse.ignore
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Supported Response Types:**
|
|
179
|
+
All Telegram `send_*` methods are supported: `message`, `photo`, `document`, `video`, `audio`, `voice`, `sticker`, `dice`, etc.
|
|
180
|
+
|
|
181
|
+
## Usage Examples
|
|
182
|
+
|
|
183
|
+
### Echo Bot
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { runTgChatBot, defineBot } from "@effect-ak/tg-bot"
|
|
187
|
+
|
|
188
|
+
const ECHO_BOT = defineBot({
|
|
189
|
+
on_message: [
|
|
190
|
+
{
|
|
191
|
+
match: ({ update }) => !!update.text,
|
|
192
|
+
handle: ({ update, ctx }) => ctx.reply(update.text!)
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
runTgChatBot({
|
|
198
|
+
bot_token: "YOUR_BOT_TOKEN",
|
|
199
|
+
mode: "single",
|
|
200
|
+
...ECHO_BOT
|
|
201
|
+
})
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Command Handler
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { runTgChatBot } from "@effect-ak/tg-bot"
|
|
208
|
+
import { MESSAGE_EFFECTS } from "@effect-ak/tg-bot-client"
|
|
209
|
+
|
|
210
|
+
runTgChatBot({
|
|
211
|
+
bot_token: "YOUR_BOT_TOKEN",
|
|
212
|
+
mode: "single",
|
|
213
|
+
on_message: [
|
|
214
|
+
{
|
|
215
|
+
match: ({ ctx }) => ctx.command === "/start",
|
|
216
|
+
handle: ({ ctx }) => ctx.reply("Welcome! Send me any message.", {
|
|
217
|
+
message_effect_id: MESSAGE_EFFECTS["🎉"]
|
|
218
|
+
})
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
match: ({ ctx }) => ctx.command === "/help",
|
|
222
|
+
handle: ({ ctx }) => ctx.reply("Available commands:\n/start - Start bot\n/help - Show help")
|
|
223
|
+
}
|
|
224
|
+
]
|
|
225
|
+
})
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Batch Processing
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
import { runTgChatBot } from "@effect-ak/tg-bot"
|
|
232
|
+
import { makeTgBotClient } from "@effect-ak/tg-bot-client"
|
|
233
|
+
|
|
234
|
+
const client = makeTgBotClient({ bot_token: "YOUR_BOT_TOKEN" })
|
|
235
|
+
|
|
236
|
+
runTgChatBot({
|
|
237
|
+
bot_token: "YOUR_BOT_TOKEN",
|
|
238
|
+
mode: "batch",
|
|
239
|
+
poll: {
|
|
240
|
+
batch_size: 100,
|
|
241
|
+
poll_timeout: 60
|
|
242
|
+
},
|
|
243
|
+
on_batch: async (updates) => {
|
|
244
|
+
const messages = updates
|
|
245
|
+
.map(u => u.message)
|
|
246
|
+
.filter(m => m != null)
|
|
247
|
+
|
|
248
|
+
await client.execute("send_message", {
|
|
249
|
+
chat_id: "ADMIN_CHAT_ID",
|
|
250
|
+
text: `Processed ${messages.length} messages`
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
return true // Continue polling
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Using Effect
|
|
259
|
+
|
|
260
|
+
Advanced usage with Effect for composable async operations:
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
import { Effect, Micro, pipe } from "effect"
|
|
264
|
+
import { launchBot } from "@effect-ak/tg-bot"
|
|
265
|
+
|
|
266
|
+
Effect.gen(function* () {
|
|
267
|
+
const bot = yield* launchBot({
|
|
268
|
+
bot_token: "YOUR_BOT_TOKEN",
|
|
269
|
+
mode: "single",
|
|
270
|
+
poll: {
|
|
271
|
+
log_level: "debug"
|
|
272
|
+
},
|
|
273
|
+
on_message: [
|
|
274
|
+
{
|
|
275
|
+
match: ({ update }) => !!update.text,
|
|
276
|
+
handle: async ({ ctx }) => {
|
|
277
|
+
await Effect.sleep("2 seconds").pipe(Effect.runPromise)
|
|
278
|
+
return ctx.reply("Delayed response!")
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
]
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
// Access bot fiber for control
|
|
285
|
+
yield* pipe(
|
|
286
|
+
Micro.fiberAwait(bot.fiber()!),
|
|
287
|
+
Effect.andThen(Effect.logInfo("Bot stopped")),
|
|
288
|
+
Effect.forkDaemon
|
|
289
|
+
)
|
|
290
|
+
}).pipe(Effect.runPromise)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Hot Reload
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import { runTgChatBot } from "@effect-ak/tg-bot"
|
|
297
|
+
|
|
298
|
+
const bot = await runTgChatBot({
|
|
299
|
+
bot_token: "YOUR_BOT_TOKEN",
|
|
300
|
+
mode: "single",
|
|
301
|
+
on_message: [
|
|
302
|
+
{
|
|
303
|
+
match: ({ update }) => !!update.text,
|
|
304
|
+
handle: ({ ctx }) => ctx.reply("Version 1")
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
// Later, reload with new handlers
|
|
310
|
+
setTimeout(() => {
|
|
311
|
+
bot.reload({
|
|
312
|
+
type: "single",
|
|
313
|
+
on_message: [
|
|
314
|
+
{
|
|
315
|
+
match: ({ update }) => !!update.text,
|
|
316
|
+
handle: ({ ctx }) => ctx.reply("Version 2 - Hot reloaded!")
|
|
317
|
+
}
|
|
318
|
+
]
|
|
319
|
+
})
|
|
320
|
+
}, 5000)
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Configuration
|
|
324
|
+
|
|
325
|
+
### Poll Settings
|
|
326
|
+
|
|
327
|
+
Configure how the bot polls for updates:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
runTgChatBot({
|
|
331
|
+
bot_token: "YOUR_BOT_TOKEN",
|
|
332
|
+
mode: "single", // or "batch"
|
|
333
|
+
poll: {
|
|
334
|
+
log_level: "debug", // "info" | "debug"
|
|
335
|
+
on_error: "continue", // "stop" | "continue"
|
|
336
|
+
batch_size: 50, // 10-100
|
|
337
|
+
poll_timeout: 30, // 2-120 seconds
|
|
338
|
+
max_empty_responses: 5 // Stop after N empty responses
|
|
339
|
+
},
|
|
340
|
+
on_message: [/* ... */] // handlers at top level
|
|
341
|
+
})
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Options:**
|
|
345
|
+
|
|
346
|
+
- `log_level` (default: `"info"`): Logging verbosity
|
|
347
|
+
- `"info"` - Basic logging (new messages, errors)
|
|
348
|
+
- `"debug"` - Detailed logging (all updates, responses)
|
|
349
|
+
|
|
350
|
+
- `on_error` (default: `"stop"`): Error handling strategy
|
|
351
|
+
- `"stop"` - Stop bot on error
|
|
352
|
+
- `"continue"` - Continue polling after errors
|
|
353
|
+
|
|
354
|
+
- `batch_size` (default: `10`): Number of updates to fetch per poll (10-100)
|
|
355
|
+
|
|
356
|
+
- `poll_timeout` (default: `10`): Long polling timeout in seconds (2-120)
|
|
357
|
+
|
|
358
|
+
- `max_empty_responses` (default: `undefined`): Stop after N consecutive empty responses (useful for testing)
|
|
359
|
+
|
|
360
|
+
## API Reference
|
|
361
|
+
|
|
362
|
+
### `runTgChatBot(input)`
|
|
363
|
+
|
|
364
|
+
Starts the bot with long polling.
|
|
365
|
+
|
|
366
|
+
**Parameters:**
|
|
367
|
+
- `bot_token` (string, required): Bot token from @BotFather
|
|
368
|
+
- `mode` (`"single"` | `"batch"`, required): Processing mode
|
|
369
|
+
- `poll` (object, optional): Polling configuration
|
|
370
|
+
- `on_message`, `on_callback_query`, etc. (optional): Update handlers (for single mode)
|
|
371
|
+
- `on_batch` (required for batch mode): Batch handler function
|
|
372
|
+
|
|
373
|
+
**Returns:** `Promise<BotInstance>`
|
|
374
|
+
|
|
375
|
+
### `launchBot(input)`
|
|
376
|
+
|
|
377
|
+
Launches bot and returns a bot instance for advanced control.
|
|
378
|
+
|
|
379
|
+
**Returns:** `Micro<BotInstance>`
|
|
380
|
+
- `BotInstance.reload(mode)` - Hot reload handlers
|
|
381
|
+
- `BotInstance.fiber()` - Access underlying Effect fiber
|
|
382
|
+
|
|
383
|
+
### `defineBot(handlers)`
|
|
384
|
+
|
|
385
|
+
Helper to define bot handlers with type checking and validation.
|
|
386
|
+
|
|
387
|
+
**Parameters:**
|
|
388
|
+
- `handlers` (object): Handler functions for different update types
|
|
389
|
+
|
|
390
|
+
**Returns:** `BotUpdatesHandlers`
|
|
391
|
+
|
|
392
|
+
### `BotResponse.make(response)`
|
|
393
|
+
|
|
394
|
+
Creates a bot response.
|
|
395
|
+
|
|
396
|
+
**Parameters:**
|
|
397
|
+
- `response` (object): Response configuration with `type` and parameters
|
|
398
|
+
|
|
399
|
+
**Returns:** `BotResponse`
|
|
400
|
+
|
|
401
|
+
### `BotResponse.ignore`
|
|
402
|
+
|
|
403
|
+
Singleton instance for ignoring updates (no response).
|
|
404
|
+
|
|
405
|
+
## How It Works
|
|
406
|
+
|
|
407
|
+
### Pull Model Architecture
|
|
408
|
+
|
|
409
|
+
The Telegram bot API supports both **push** and **pull** notification models. This package uses the **pull** model for several key advantages:
|
|
410
|
+
|
|
411
|
+
- **Run bots anywhere without public URLs:** No need to expose public ports or configure webhooks. You can run bots locally, in a browser, or behind firewalls.
|
|
412
|
+
- **Leverage Telegram's infrastructure:** Telegram stores updates for 24 hours, giving you plenty of time to process them.
|
|
413
|
+
- **Simpler deployment:** No SSL certificates, no webhook configuration, no reverse proxies required.
|
|
414
|
+
|
|
415
|
+
### Architecture Diagram
|
|
416
|
+
|
|
417
|
+
```mermaid
|
|
418
|
+
graph TD
|
|
419
|
+
User[User] -->|Sends message| TgBot[Telegram Bot]
|
|
420
|
+
TgBot -->|Stores for 24h| Queue[Updates Queue<br/>api.telegram.org/bot/updates]
|
|
421
|
+
|
|
422
|
+
subgraph Your Code
|
|
423
|
+
Runner[Bot Runner<br/>@effect-ak/tg-bot]
|
|
424
|
+
Handler[Your Handler Function]
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
Runner -->|Long polling| Queue
|
|
428
|
+
Runner -->|Invokes with update| Handler
|
|
429
|
+
Handler -->|Returns BotResponse| Runner
|
|
430
|
+
Runner -->|Sends response| TgBot
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**How it works:**
|
|
434
|
+
1. User sends a message to your bot
|
|
435
|
+
2. Telegram stores the update in a queue for 24 hours
|
|
436
|
+
3. Bot runner polls the queue using long polling
|
|
437
|
+
4. Runner invokes your handler function with the update
|
|
438
|
+
5. Handler returns a `BotResponse`
|
|
439
|
+
6. Runner sends the response back to Telegram
|
|
440
|
+
7. Runner tracks the last processed update ID to avoid duplicates
|
|
441
|
+
|
|
442
|
+
## Error Handling
|
|
443
|
+
|
|
444
|
+
The bot automatically handles errors at different levels:
|
|
445
|
+
|
|
446
|
+
### Update Handler Errors
|
|
447
|
+
|
|
448
|
+
If a handler throws an error, the bot:
|
|
449
|
+
1. Logs the error with update details
|
|
450
|
+
2. Sends an error message to the user (in single mode)
|
|
451
|
+
3. Continues processing other updates (if `on_error: "continue"`)
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
on_message: [
|
|
455
|
+
{
|
|
456
|
+
match: ({ ctx }) => ctx.command === "/error",
|
|
457
|
+
handle: () => {
|
|
458
|
+
throw new Error("Something went wrong!")
|
|
459
|
+
// Bot will catch this and send error message to user
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
match: ({ update }) => !!update.text,
|
|
464
|
+
handle: ({ ctx }) => ctx.reply("OK")
|
|
465
|
+
}
|
|
466
|
+
]
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Batch Handler Errors
|
|
470
|
+
|
|
471
|
+
In batch mode, returning `false` stops the bot:
|
|
472
|
+
|
|
473
|
+
```typescript
|
|
474
|
+
on_batch: async (updates) => {
|
|
475
|
+
try {
|
|
476
|
+
// Process updates
|
|
477
|
+
return true // Continue
|
|
478
|
+
} catch (error) {
|
|
479
|
+
console.error(error)
|
|
480
|
+
return false // Stop bot
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Concurrent Processing
|
|
486
|
+
|
|
487
|
+
In single mode, up to 10 updates are processed concurrently. If some handlers fail, others continue processing.
|
|
488
|
+
|
|
489
|
+
## Playground
|
|
490
|
+
|
|
491
|
+
Develop and test your bot directly in the browser:
|
|
492
|
+
|
|
493
|
+
**[Chat Bot Playground](https://effect-ak.github.io/telegram-bot-playground/)**
|
|
494
|
+
|
|
495
|
+
No installation required - perfect for quick prototyping and learning!
|
|
496
|
+
|
|
497
|
+
## Related Packages
|
|
498
|
+
|
|
499
|
+
This package is part of the `tg-bot-client` monorepo:
|
|
500
|
+
|
|
501
|
+
- **[@effect-ak/tg-bot-client](../client)** - Type-safe HTTP client for Telegram Bot API
|
|
502
|
+
- **[@effect-ak/tg-bot-api](../api)** - TypeScript types for Telegram Bot API and Mini Apps
|
|
503
|
+
- **[@effect-ak/tg-bot-codegen](../codegen)** - Code generator that parses official documentation
|
|
504
|
+
|
|
505
|
+
## License
|
|
506
|
+
|
|
507
|
+
MIT © [Aleksandr Kondaurov](https://github.com/effect-ak)
|