@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 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
- console.log("update handle error", {
3612
- updateId: update.update_id,
3613
- updateKey: Object.keys(update).at(1),
3614
- name: error._tag,
3615
- ...error.cause instanceof Error ? { error: error.cause.message } : void 0
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 updateHandler = handlers[`on_${update.type}`];
3645
- if (!updateHandler) {
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: () => updateHandler(update),
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.log("error", {
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: import_tg_bot_client2.MESSAGE_EFFECTS["\u{1F4A9}"],
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 response = yield* (0, import_tg_bot_client.executeTgBotMethod)(
3713
- `send_${handleResult.response.type}`,
3714
- {
3715
- ...handleResult.response,
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* (0, import_tg_bot_client3.executeTgBotMethod)("get_updates", {
3768
- timeout: pollSettings.poll_timeout,
3769
- ...updateId ? { offset: updateId } : void 0
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
- return yield* (0, import_tg_bot_client3.executeTgBotMethod)("get_updates", {
3784
- offset: pollState.lastUpdateId,
3785
- limit: 0
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 import_tg_bot_client4 = require("@effect-ak/tg-bot-client");
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 contextWithToken = make4(import_tg_bot_client4.TgBotApiToken, input.bot_token);
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, input.mode),
3939
+ provideService(BotUpdateHandlersTag, mode),
3865
3940
  provideService(
3866
3941
  BotPollSettingsTag,
3867
3942
  BotPollSettings.make(input.poll ?? {})
3868
3943
  ),
3869
- provideContext(contextWithToken)
3944
+ provideContext(contextWithClient)
3870
3945
  );
3871
- const reload = (mode) => service2.runBotInBackground.pipe(
3872
- provideService(BotUpdateHandlersTag, mode),
3873
- provideContext(contextWithToken),
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
- interface RunBotInput {
44
+ type RunBotInput = RunBotInputSingle | RunBotInputBatch;
45
+ interface RunBotInputSingle extends BotUpdatesHandlers {
43
46
  bot_token: string;
44
- mode: BotMode;
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
- readonly [K in AvailableUpdateTypes as `on_${K}`]?: HandleUpdateFunction<NonNullable<Update[K]>>;
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 | _effect_ak_tg_bot_client.TgBotApiToken>;
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, _effect_ak_tg_bot_client.TgBotApiToken>;
84
- declare const handleOneUpdate: (updateObject: Update, handlers: BotUpdatesHandlers) => Micro.Micro<HandleUpdateError | undefined, HandleUpdateError | _effect_ak_tg_bot_client.TgBotClientError, _effect_ak_tg_bot_client.TgBotApiToken>;
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, _effect_ak_tg_bot_client.TgBotClientError | FetchUpdatesError> | undefined;
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 | _effect_ak_tg_bot_client.TgBotApiToken>;
102
- readonly getFiber: () => Micro.MicroFiber<BatchUpdateResult, TgBotClientError | FetchUpdatesError> | undefined;
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, _effect_ak_tg_bot_client.TgBotClientError | FetchUpdatesError> | undefined;
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
- console.log("update handle error", {
3572
- updateId: update.update_id,
3573
- updateKey: Object.keys(update).at(1),
3574
- name: error._tag,
3575
- ...error.cause instanceof Error ? { error: error.cause.message } : void 0
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 updateHandler = handlers[`on_${update.type}`];
3605
- if (!updateHandler) {
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: () => updateHandler(update),
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.log("error", {
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 response = yield* executeTgBotMethod(
3673
- `send_${handleResult.response.type}`,
3674
- {
3675
- ...handleResult.response,
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* executeTgBotMethod2("get_updates", {
3728
- timeout: pollSettings.poll_timeout,
3729
- ...updateId ? { offset: updateId } : void 0
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
- return yield* executeTgBotMethod2("get_updates", {
3744
- offset: pollState.lastUpdateId,
3745
- limit: 0
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 { TgBotApiToken } from "@effect-ak/tg-bot-client";
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 contextWithToken = make4(TgBotApiToken, input.bot_token);
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, input.mode),
3897
+ provideService(BotUpdateHandlersTag, mode),
3825
3898
  provideService(
3826
3899
  BotPollSettingsTag,
3827
3900
  BotPollSettings.make(input.poll ?? {})
3828
3901
  ),
3829
- provideContext(contextWithToken)
3902
+ provideContext(contextWithClient)
3830
3903
  );
3831
- const reload = (mode) => service2.runBotInBackground.pipe(
3832
- provideService(BotUpdateHandlersTag, mode),
3833
- provideContext(contextWithToken),
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.0.0",
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.0.0",
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
+ [![NPM Version](https://img.shields.io/npm/v/%40effect-ak%2Ftg-bot)](https://www.npmjs.com/package/@effect-ak/tg-bot)
4
+ ![NPM Downloads](https://img.shields.io/npm/dw/%40effect-ak%2Ftg-bot?link=)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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)