balebaazoo 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/README.en.md CHANGED
@@ -13,13 +13,40 @@ npm install balebaazoo
13
13
 
14
14
  Requires Node.js 18+.
15
15
 
16
+ ## TypeScript setup
17
+
18
+ If you see error **TS1295** (`import` in a CommonJS file), your project needs ESM config:
19
+
20
+ **package.json**
21
+ ```json
22
+ {
23
+ "type": "module"
24
+ }
25
+ ```
26
+
27
+ **tsconfig.json**
28
+ ```json
29
+ {
30
+ "compilerOptions": {
31
+ "module": "NodeNext",
32
+ "moduleResolution": "NodeNext",
33
+ "verbatimModuleSyntax": true,
34
+ "types": ["node"]
35
+ }
36
+ }
37
+ ```
38
+
39
+ Use the ready-made template: [`templates/starter`](templates/starter)
40
+
16
41
  ## Quick start
17
42
 
18
43
  ```typescript
19
- import { Bot, InlineKeyboard } from "balebaazoo";
44
+ import { Bot, InlineKeyboard, setupGracefulShutdown } from "balebaazoo";
20
45
 
21
46
  const bot = new Bot(process.env.BOT_TOKEN!);
22
47
 
48
+ setupGracefulShutdown(bot);
49
+
23
50
  bot.command("start", (ctx) =>
24
51
  ctx.reply("Hello! Welcome to my bot.", {
25
52
  reply_markup: new InlineKeyboard().text("About", "about"),
@@ -31,14 +58,21 @@ bot.on("callback_query:data", async (ctx) => {
31
58
  await ctx.reply("More info...");
32
59
  });
33
60
 
34
- bot.start();
61
+ void bot.start({
62
+ onStart: (me) => console.log(`Running as @${me.username ?? me.id}`),
63
+ onError: (error) => console.error(error),
64
+ });
35
65
  ```
36
66
 
67
+ > `bot.start()` blocks until `bot.stop()`. Use `onStart` for post-init logging.
68
+
37
69
  ## Features
38
70
 
39
71
  - Full Bale Bot API client with TypeScript types
40
72
  - Bot framework: middleware, filters, commands, composers
41
- - Polling and webhook support
73
+ - Lifecycle: `onStart`, `setupGracefulShutdown`, `dropPendingUpdates`, `launch()`
74
+ - Polling (with AbortSignal) and webhook support
75
+ - `bot.catch()` and `errorHandler` middleware
42
76
  - Context shortcuts (`ctx.reply`, `ctx.replyWithPhoto`, ...)
43
77
  - Wallet payments, `askReview`, `inquireTransaction`
44
78
  - Zero runtime dependencies
package/README.md CHANGED
@@ -1,3 +1,9 @@
1
+ <div dir="rtl" lang="fa">
2
+
3
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vazirmatn@33.0.3/Vazirmatn-font-face.css">
4
+
5
+ <div style="font-family: Vazirmatn, Tahoma, sans-serif;">
6
+
1
7
  # balebaazoo
2
8
 
3
9
  SDK مدرن و type-safe برای توسعهٔ بازو (Bot) در پیام‌رسان بله.
@@ -22,10 +28,12 @@ npm install balebaazoo
22
28
  ## شروع سریع
23
29
 
24
30
  ```typescript
25
- import { Bot, InlineKeyboard } from "balebaazoo";
31
+ import { Bot, InlineKeyboard, setupGracefulShutdown } from "balebaazoo";
26
32
 
27
33
  const bot = new Bot(process.env.BOT_TOKEN!);
28
34
 
35
+ setupGracefulShutdown(bot);
36
+
29
37
  bot.command("start", (ctx) =>
30
38
  ctx.reply("سلام! به بازو خوش آمدید 👋", {
31
39
  reply_markup: new InlineKeyboard().text("درباره ما", "about"),
@@ -37,18 +45,67 @@ bot.on("callback_query:data", async (ctx) => {
37
45
  await ctx.reply("اطلاعات بیشتر...");
38
46
  });
39
47
 
40
- bot.start();
48
+ void bot.start({
49
+ onStart: (me) => console.log(`Running as @${me.username ?? me.id}`),
50
+ onError: (error) => console.error(error),
51
+ });
52
+ ```
53
+
54
+ > **نکته:** `bot.start()` تا زمان `bot.stop()` ادامه دارد. برای لاگ بعد از راه‌اندازی از `onStart` استفاده کنید، نه `await` بعد از `start()`.
55
+
56
+ ## TypeScript
57
+
58
+ پروژهٔ شما باید ESM باشد:
59
+
60
+ ```json
61
+ {
62
+ "type": "module"
63
+ }
64
+ ```
65
+
66
+ ```json
67
+ {
68
+ "compilerOptions": {
69
+ "module": "NodeNext",
70
+ "moduleResolution": "NodeNext",
71
+ "verbatimModuleSyntax": true,
72
+ "types": ["node"]
73
+ }
74
+ }
75
+ ```
76
+
77
+ قالب آماده: [`templates/starter`](templates/starter)
78
+
79
+ ### خطای TS1295
80
+
81
+ اگر `ECMAScript imports and exports cannot be written in a CommonJS file` دیدید، `"type": "module"` را به `package.json` اضافه کنید.
82
+
83
+ ### خطای TS2322 با `ctx.reply()`
84
+
85
+ نسخهٔ `1.0.1+` اجازه می‌دهد handler مستقیماً `ctx.reply()` را return کند:
86
+
87
+ ```typescript
88
+ bot.command("start", (ctx) => ctx.reply("سلام!"));
89
+ ```
90
+
91
+ ### nodemon + TypeScript
92
+
93
+ ```json
94
+ {
95
+ "watch": ["src"],
96
+ "ext": "ts,json",
97
+ "exec": "tsx src/bot.ts"
98
+ }
41
99
  ```
42
100
 
43
101
  ## ویژگی‌ها
44
102
 
45
- - **Type-safe** — تایپ‌های کامل TypeScript برای API و handlerها
46
- - **Middleware** — سیستم middleware و `Composer` برای ساختار ماژولار
47
- - **Filter queries** — `message:text`، `callback_query:data`، `pre_checkout_query` و ...
48
- - **Shortcuts** — `ctx.reply()`، `ctx.replyWithPhoto()`، `ctx.answerCallbackQuery()`
49
- - **Polling و Webhook** — `bot.start()` و `webhookFromJson()`
50
- - **Bale-native**`askReview`، `inquireTransaction`، کیف‌پول، Markdown بله
51
- - **بدون dependency** — فقط Node.js 18+ با fetch بومی
103
+ - **Type-safe** — تایپ‌های کامل TypeScript
104
+ - **Middleware** — `Composer`، `bot.catch()`، `errorHandler`
105
+ - **Lifecycle** — `onStart`، `setupGracefulShutdown`، `dropPendingUpdates`، `launch()`
106
+ - **Polling و Webhook** — long polling با AbortSignal و webhook handler
107
+ - **Bale-native** — `askReview`، `inquireTransaction`، کیف‌پول
108
+ - **بدون dependency** — Node.js 18+
52
109
 
53
110
  ## مثال‌ها
54
111
 
@@ -68,3 +125,6 @@ npm test
68
125
  ## لایسنس
69
126
 
70
127
  GPL-3.0-or-later
128
+
129
+ </div>
130
+ </div>
package/dist/index.cjs CHANGED
@@ -94,6 +94,9 @@ async function isFilePath(source) {
94
94
  if (source.startsWith("http://") || source.startsWith("https://")) {
95
95
  return false;
96
96
  }
97
+ if (!looksLikePath(source)) {
98
+ return false;
99
+ }
97
100
  try {
98
101
  const info = await promises.stat(source);
99
102
  return info.isFile();
@@ -101,6 +104,9 @@ async function isFilePath(source) {
101
104
  return false;
102
105
  }
103
106
  }
107
+ function looksLikePath(source) {
108
+ return source.startsWith("./") || source.startsWith("../") || source.includes("/") || source.includes("\\");
109
+ }
104
110
 
105
111
  // src/api/client.ts
106
112
  var DEFAULT_API_BASE = "https://tapi.bale.ai";
@@ -123,12 +129,15 @@ var Api = class {
123
129
  methodUrl(method) {
124
130
  return `${this.baseUrl}/bot${this.token}/${method}`;
125
131
  }
126
- async call(method, params) {
132
+ async call(method, params, options) {
127
133
  let attempt = 0;
128
134
  while (true) {
129
135
  try {
130
- return await this.invoke(method, params);
136
+ return await this.invoke(method, params, options);
131
137
  } catch (error) {
138
+ if (options?.signal?.aborted || isAbortError(error)) {
139
+ throw error;
140
+ }
132
141
  if (error instanceof BaleAPIError && error.errorCode === 429 && attempt < this.maxRetries) {
133
142
  const retryAfter = (error.parameters?.retry_after ?? 1) * 1e3;
134
143
  await sleep(retryAfter);
@@ -139,7 +148,7 @@ var Api = class {
139
148
  }
140
149
  }
141
150
  }
142
- async invoke(method, params) {
151
+ async invoke(method, params, options) {
143
152
  const { body } = await this.prepareBody(params ?? {});
144
153
  const headers = {};
145
154
  if (!(body instanceof FormData)) {
@@ -149,7 +158,7 @@ var Api = class {
149
158
  method: "POST",
150
159
  headers,
151
160
  body: body instanceof FormData ? body : JSON.stringify(body ?? {})
152
- });
161
+ }, options);
153
162
  }
154
163
  async downloadFile(filePath) {
155
164
  const url = `${this.fileBaseUrl}/${filePath}`;
@@ -191,8 +200,8 @@ var Api = class {
191
200
  }
192
201
  return { body: formData, isMultipart: true };
193
202
  }
194
- async request(method, init) {
195
- const response = await this.fetchWithRetry(this.methodUrl(method), init);
203
+ async request(method, init, options) {
204
+ const response = await this.fetchWithRetry(this.methodUrl(method), init, options);
196
205
  const payload = await response.json();
197
206
  if (!payload.ok) {
198
207
  throw new BaleAPIError(
@@ -203,12 +212,18 @@ var Api = class {
203
212
  }
204
213
  return payload.result;
205
214
  }
206
- async fetchWithRetry(url, init) {
215
+ async fetchWithRetry(url, init, options) {
207
216
  let attempt = 0;
208
217
  let lastError;
209
218
  while (attempt <= this.maxRetries) {
219
+ if (options?.signal?.aborted) {
220
+ throw new DOMException("The operation was aborted", "AbortError");
221
+ }
210
222
  try {
211
- const response = await this.fetchFn(url, init);
223
+ const response = await this.fetchFn(url, {
224
+ ...init,
225
+ signal: options?.signal
226
+ });
212
227
  if (response.status === 429) {
213
228
  const retryAfter = Number(response.headers.get("retry-after") ?? "1");
214
229
  await sleep(retryAfter * 1e3);
@@ -223,6 +238,9 @@ var Api = class {
223
238
  return response;
224
239
  } catch (error) {
225
240
  lastError = error;
241
+ if (isAbortError(error) || options?.signal?.aborted) {
242
+ throw error;
243
+ }
226
244
  if (attempt >= this.maxRetries) {
227
245
  break;
228
246
  }
@@ -238,8 +256,8 @@ var Api = class {
238
256
  getMe() {
239
257
  return this.call("getMe");
240
258
  }
241
- getUpdates(params) {
242
- return this.call("getUpdates", asParams(params));
259
+ getUpdates(params, options) {
260
+ return this.call("getUpdates", asParams(params), options);
243
261
  }
244
262
  setWebhook(params) {
245
263
  return this.call("setWebhook", asParams(params));
@@ -427,6 +445,9 @@ function appendFormValue(formData, key, value) {
427
445
  function sleep(ms) {
428
446
  return new Promise((resolve) => setTimeout(resolve, ms));
429
447
  }
448
+ function isAbortError(error) {
449
+ return error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted"));
450
+ }
430
451
 
431
452
  // src/middleware/types.ts
432
453
  function isMiddlewareObject(value) {
@@ -497,12 +518,10 @@ function extractCommand(text) {
497
518
 
498
519
  // src/filters/query.ts
499
520
  function matchesFilter(ctx, filter) {
500
- const updateMatches = matchUpdate(ctx.update);
501
- return updateMatches.includes(filter);
521
+ return ctx.updateTypes.includes(filter);
502
522
  }
503
523
  function matchesAnyFilter(ctx, filters) {
504
- const updateMatches = matchUpdate(ctx.update);
505
- return filters.some((filter) => updateMatches.includes(filter));
524
+ return filters.some((filter) => ctx.updateTypes.includes(filter));
506
525
  }
507
526
  function matchesChatType(ctx, chatType) {
508
527
  const chat = ctx.chat;
@@ -615,6 +634,7 @@ var Context = class {
615
634
  update;
616
635
  botInfo;
617
636
  callbackQueryAnswered = false;
637
+ _updateTypes;
618
638
  constructor(options) {
619
639
  this.api = options.api;
620
640
  this.update = options.update;
@@ -623,6 +643,12 @@ var Context = class {
623
643
  get updateId() {
624
644
  return this.update.update_id;
625
645
  }
646
+ get updateTypes() {
647
+ if (!this._updateTypes) {
648
+ this._updateTypes = matchUpdate(this.update);
649
+ }
650
+ return this._updateTypes;
651
+ }
626
652
  get message() {
627
653
  return this.update.message ?? this.update.edited_message;
628
654
  }
@@ -782,21 +808,14 @@ function autoAnswerCallback() {
782
808
  }
783
809
  };
784
810
  }
785
- function errorHandler(onError) {
786
- return async (ctx, next) => {
787
- try {
788
- await next();
789
- } catch (error) {
790
- await onError(error, ctx);
791
- }
792
- };
793
- }
794
811
 
795
812
  // src/runner/polling.ts
796
813
  var PollingRunner = class {
797
814
  running = false;
798
815
  abortController;
799
816
  offset = 0;
817
+ loopPromise;
818
+ backoffAttempt = 0;
800
819
  async start(bot, options = {}) {
801
820
  if (this.running) {
802
821
  throw new Error("Polling is already running");
@@ -807,14 +826,22 @@ var PollingRunner = class {
807
826
  const onError = options.onError ?? ((error) => {
808
827
  console.error("[balebaazoo] polling error:", error);
809
828
  });
829
+ this.loopPromise = this.runLoop(bot, options, signal, onError);
830
+ await this.loopPromise;
831
+ }
832
+ async runLoop(bot, options, signal, onError) {
810
833
  while (this.running && !signal.aborted) {
811
834
  try {
812
- const updates = await bot.api.getUpdates({
813
- offset: this.offset,
814
- limit: options.limit ?? 100,
815
- timeout: options.timeout ?? 30,
816
- allowed_updates: options.allowedUpdates
817
- });
835
+ const updates = await bot.api.getUpdates(
836
+ {
837
+ offset: this.offset,
838
+ limit: options.limit ?? 100,
839
+ timeout: options.timeout ?? 30,
840
+ allowed_updates: options.allowedUpdates
841
+ },
842
+ { signal }
843
+ );
844
+ this.backoffAttempt = 0;
818
845
  for (const update of updates) {
819
846
  this.offset = update.update_id + 1;
820
847
  try {
@@ -827,23 +854,49 @@ var PollingRunner = class {
827
854
  break;
828
855
  }
829
856
  } catch (error) {
830
- if (signal.aborted) break;
857
+ if (signal.aborted || isAbortError2(error)) {
858
+ break;
859
+ }
860
+ if (error instanceof BaleAPIError && error.errorCode === 401) {
861
+ onError(error);
862
+ this.running = false;
863
+ break;
864
+ }
831
865
  onError(error);
832
- await sleep2(1e3);
866
+ await sleep2(backoffDelay(this.backoffAttempt));
867
+ this.backoffAttempt++;
833
868
  }
834
869
  }
835
870
  }
871
+ setOffset(offset) {
872
+ this.offset = offset;
873
+ }
836
874
  async stop() {
837
875
  this.running = false;
838
876
  this.abortController?.abort();
877
+ await this.loopPromise;
839
878
  }
840
879
  };
880
+ function isAbortError2(error) {
881
+ return error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted"));
882
+ }
883
+ function backoffDelay(attempt) {
884
+ const base = 1e3;
885
+ const max = 3e4;
886
+ const exponential = Math.min(base * 2 ** attempt, max);
887
+ const jitter = Math.random() * 0.3 * exponential;
888
+ return Math.floor(exponential + jitter);
889
+ }
841
890
  function sleep2(ms) {
842
891
  return new Promise((resolve) => setTimeout(resolve, ms));
843
892
  }
844
893
  async function createWebhookHandler(bot, getUpdate, options = {}) {
845
894
  await bot.init();
846
895
  const maxBodyBytes = options.maxBodyBytes ?? 1024 * 1024;
896
+ const onError = options.onError ?? ((error) => {
897
+ console.error("[balebaazoo] webhook error:", error);
898
+ return new Response("Internal Server Error", { status: 500 });
899
+ });
847
900
  return async (request) => {
848
901
  if (request.method !== "POST") {
849
902
  return new Response("Method Not Allowed", { status: 405 });
@@ -868,8 +921,8 @@ async function createWebhookHandler(bot, getUpdate, options = {}) {
868
921
  await bot.handleUpdate(update);
869
922
  return new Response("OK", { status: 200 });
870
923
  } catch (error) {
871
- console.error("[balebaazoo] webhook error:", error);
872
- return new Response("Internal Server Error", { status: 500 });
924
+ const response = onError(error);
925
+ return response ?? new Response("Internal Server Error", { status: 500 });
873
926
  }
874
927
  };
875
928
  }
@@ -885,6 +938,7 @@ var Bot = class extends Composer {
885
938
  botInfo;
886
939
  polling = new PollingRunner();
887
940
  autoAnswer;
941
+ catchHandler;
888
942
  constructor(token, options = {}) {
889
943
  const api = new Api({ token, ...options });
890
944
  super();
@@ -901,9 +955,22 @@ var Bot = class extends Composer {
901
955
  }
902
956
  return this.botInfo;
903
957
  }
958
+ catch(handler) {
959
+ this.catchHandler = handler;
960
+ return this;
961
+ }
904
962
  async handleUpdate(update) {
963
+ await this.init();
905
964
  const ctx = this.createContext(update);
906
- await this.middleware()(ctx, async () => void 0);
965
+ try {
966
+ await this.middleware()(ctx, async () => void 0);
967
+ } catch (error) {
968
+ if (this.catchHandler) {
969
+ await this.catchHandler(error, ctx);
970
+ return;
971
+ }
972
+ throw error;
973
+ }
907
974
  }
908
975
  createContext(update) {
909
976
  return new Context({
@@ -913,9 +980,44 @@ var Bot = class extends Composer {
913
980
  });
914
981
  }
915
982
  async start(options = {}) {
916
- await this.init();
983
+ const onError = options.onError ?? ((error) => {
984
+ console.error("[balebaazoo] polling error:", error);
985
+ });
986
+ try {
987
+ await this.init();
988
+ } catch (error) {
989
+ onError(error);
990
+ return;
991
+ }
992
+ if (options.dropPendingUpdates) {
993
+ try {
994
+ const updates = await this.api.getUpdates({
995
+ offset: -1,
996
+ limit: 1,
997
+ timeout: 0
998
+ });
999
+ if (updates.length > 0) {
1000
+ const last = updates[updates.length - 1];
1001
+ this.polling.setOffset(last.update_id + 1);
1002
+ }
1003
+ } catch (error) {
1004
+ onError(error);
1005
+ return;
1006
+ }
1007
+ }
1008
+ if (options.onStart) {
1009
+ try {
1010
+ await options.onStart(this.botInfo);
1011
+ } catch (error) {
1012
+ onError(error);
1013
+ return;
1014
+ }
1015
+ }
917
1016
  await this.polling.start(this, options);
918
1017
  }
1018
+ launch(options) {
1019
+ return this.start(options);
1020
+ }
919
1021
  async stop() {
920
1022
  await this.polling.stop();
921
1023
  }
@@ -1012,6 +1114,30 @@ function removeKeyboard() {
1012
1114
  return { remove_keyboard: true };
1013
1115
  }
1014
1116
 
1117
+ // src/middleware/error-handler.ts
1118
+ function errorHandler(onError) {
1119
+ return async (ctx, next) => {
1120
+ try {
1121
+ await next();
1122
+ } catch (error) {
1123
+ await onError(error, ctx);
1124
+ }
1125
+ };
1126
+ }
1127
+
1128
+ // src/runner/shutdown.ts
1129
+ function setupGracefulShutdown(bot, options = {}) {
1130
+ const signals = options.signals ?? ["SIGINT", "SIGTERM"];
1131
+ for (const signal of signals) {
1132
+ process.once(signal, () => {
1133
+ void (async () => {
1134
+ await bot.stop();
1135
+ await options.onShutdown?.();
1136
+ })();
1137
+ });
1138
+ }
1139
+ }
1140
+
1015
1141
  exports.Api = Api;
1016
1142
  exports.BaleAPIError = BaleAPIError;
1017
1143
  exports.BaleError = BaleError;
@@ -1041,6 +1167,7 @@ exports.md = md;
1041
1167
  exports.normalizeMiddleware = normalizeMiddleware;
1042
1168
  exports.removeKeyboard = removeKeyboard;
1043
1169
  exports.runMiddleware = runMiddleware;
1170
+ exports.setupGracefulShutdown = setupGracefulShutdown;
1044
1171
  exports.spoiler = spoiler;
1045
1172
  exports.webhookFromJson = webhookFromJson;
1046
1173
  //# sourceMappingURL=index.cjs.map