@xbibzlibrary/telebibz 0.4.4 → 3.0.1
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/CHANGELOG.md +35 -138
- package/LICENSE +1 -1
- package/NOTICE.md +9 -4
- package/README.md +173 -242
- package/examples/01-quickstart.js +13 -0
- package/examples/02-menu-tombol.js +22 -0
- package/examples/03-wizard.js +29 -0
- package/examples/04-broadcast.js +25 -0
- package/examples/05-kirim-file.js +19 -0
- package/examples/06-menu.js +35 -0
- package/examples/07-inline-query.js +18 -0
- package/index.d.ts +81 -0
- package/index.js +42 -0
- package/lib/api.js +154 -0
- package/lib/broadcast.js +35 -0
- package/lib/composer.js +174 -0
- package/lib/context.js +190 -0
- package/lib/errors.js +35 -0
- package/lib/file.js +41 -0
- package/lib/inline-query.js +28 -0
- package/lib/keyboard.js +83 -0
- package/lib/logger.js +38 -0
- package/lib/menus.js +90 -0
- package/lib/net.js +115 -0
- package/lib/ratelimit.js +61 -0
- package/lib/runner.js +45 -0
- package/lib/session.js +36 -0
- package/lib/telebibz.js +190 -0
- package/lib/wizard.js +81 -0
- package/package.json +35 -97
- package/test/all.test.js +334 -0
- package/CODE_OF_CONDUCT.md +0 -37
- package/CONTRIBUTING.md +0 -59
- package/CONTRIBUTION_RULES.md +0 -41
- package/GOVERNANCE.md +0 -47
- package/README.id.md +0 -292
- package/README.zh-CN.md +0 -292
- package/RELEASE_AUTOMATION.md +0 -78
- package/RELEASE_POLICY.md +0 -32
- package/SECURITY.md +0 -47
- package/SHOWCASE.md +0 -29
- package/SUPPORT.md +0 -30
- package/assets/readme-preview.html +0 -75
- package/assets/telebibz-logo.png +0 -0
- package/assets/telebibz-readme-preview.png +0 -0
- package/bin/telebibz.mjs +0 -3
- package/dist/generated/api.d.ts +0 -13
- package/dist/generated/api.d.ts.map +0 -1
- package/dist/generated/api.js +0 -192
- package/dist/generated/api.js.map +0 -1
- package/dist/src/api/client.d.ts +0 -62
- package/dist/src/api/client.d.ts.map +0 -1
- package/dist/src/api/client.js +0 -104
- package/dist/src/api/client.js.map +0 -1
- package/dist/src/api/errors.d.ts +0 -45
- package/dist/src/api/errors.d.ts.map +0 -1
- package/dist/src/api/errors.js +0 -65
- package/dist/src/api/errors.js.map +0 -1
- package/dist/src/api/index.d.ts +0 -6
- package/dist/src/api/index.d.ts.map +0 -1
- package/dist/src/api/index.js +0 -6
- package/dist/src/api/index.js.map +0 -1
- package/dist/src/api/telegram-types/LICENSE +0 -21
- package/dist/src/api/telegram-types/api.d.ts +0 -22
- package/dist/src/api/telegram-types/checklist.d.ts +0 -72
- package/dist/src/api/telegram-types/inline.d.ts +0 -692
- package/dist/src/api/telegram-types/langs.d.ts +0 -193
- package/dist/src/api/telegram-types/manage.d.ts +0 -1144
- package/dist/src/api/telegram-types/markup.d.ts +0 -268
- package/dist/src/api/telegram-types/message.d.ts +0 -1537
- package/dist/src/api/telegram-types/methods.d.ts +0 -2870
- package/dist/src/api/telegram-types/mod.d.ts +0 -14
- package/dist/src/api/telegram-types/passport.d.ts +0 -163
- package/dist/src/api/telegram-types/payment.d.ts +0 -570
- package/dist/src/api/telegram-types/rich.d.ts +0 -1010
- package/dist/src/api/telegram-types/settings.d.ts +0 -120
- package/dist/src/api/telegram-types/story.d.ts +0 -89
- package/dist/src/api/telegram-types/update.d.ts +0 -84
- package/dist/src/api/telegram.d.ts +0 -7
- package/dist/src/api/telegram.d.ts.map +0 -1
- package/dist/src/api/telegram.js +0 -2
- package/dist/src/api/telegram.js.map +0 -1
- package/dist/src/api/transport.d.ts +0 -68
- package/dist/src/api/transport.d.ts.map +0 -1
- package/dist/src/api/transport.js +0 -264
- package/dist/src/api/transport.js.map +0 -1
- package/dist/src/api/types.d.ts +0 -466
- package/dist/src/api/types.d.ts.map +0 -1
- package/dist/src/api/types.js +0 -2
- package/dist/src/api/types.js.map +0 -1
- package/dist/src/branding/terminal.d.ts +0 -77
- package/dist/src/branding/terminal.d.ts.map +0 -1
- package/dist/src/branding/terminal.js +0 -328
- package/dist/src/branding/terminal.js.map +0 -1
- package/dist/src/broadcast/broadcast.d.ts +0 -50
- package/dist/src/broadcast/broadcast.d.ts.map +0 -1
- package/dist/src/broadcast/broadcast.js +0 -56
- package/dist/src/broadcast/broadcast.js.map +0 -1
- package/dist/src/cache/cache.d.ts +0 -34
- package/dist/src/cache/cache.d.ts.map +0 -1
- package/dist/src/cache/cache.js +0 -41
- package/dist/src/cache/cache.js.map +0 -1
- package/dist/src/cli.d.ts +0 -2
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js +0 -84
- package/dist/src/cli.js.map +0 -1
- package/dist/src/context/context.d.ts +0 -124
- package/dist/src/context/context.d.ts.map +0 -1
- package/dist/src/context/context.js +0 -302
- package/dist/src/context/context.js.map +0 -1
- package/dist/src/core/bot.d.ts +0 -204
- package/dist/src/core/bot.d.ts.map +0 -1
- package/dist/src/core/bot.js +0 -506
- package/dist/src/core/bot.js.map +0 -1
- package/dist/src/core/events.d.ts +0 -75
- package/dist/src/core/events.d.ts.map +0 -1
- package/dist/src/core/events.js +0 -35
- package/dist/src/core/events.js.map +0 -1
- package/dist/src/core/webhook-reply.d.ts +0 -34
- package/dist/src/core/webhook-reply.d.ts.map +0 -1
- package/dist/src/core/webhook-reply.js +0 -37
- package/dist/src/core/webhook-reply.js.map +0 -1
- package/dist/src/index.d.ts +0 -24
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -24
- package/dist/src/index.js.map +0 -1
- package/dist/src/keyboard/index.d.ts +0 -46
- package/dist/src/keyboard/index.d.ts.map +0 -1
- package/dist/src/keyboard/index.js +0 -55
- package/dist/src/keyboard/index.js.map +0 -1
- package/dist/src/middleware/compose.d.ts +0 -5
- package/dist/src/middleware/compose.d.ts.map +0 -1
- package/dist/src/middleware/compose.js +0 -17
- package/dist/src/middleware/compose.js.map +0 -1
- package/dist/src/observability/logger.d.ts +0 -78
- package/dist/src/observability/logger.d.ts.map +0 -1
- package/dist/src/observability/logger.js +0 -285
- package/dist/src/observability/logger.js.map +0 -1
- package/dist/src/plugins/plugin.d.ts +0 -38
- package/dist/src/plugins/plugin.d.ts.map +0 -1
- package/dist/src/plugins/plugin.js +0 -59
- package/dist/src/plugins/plugin.js.map +0 -1
- package/dist/src/queue/queue.d.ts +0 -77
- package/dist/src/queue/queue.d.ts.map +0 -1
- package/dist/src/queue/queue.js +0 -213
- package/dist/src/queue/queue.js.map +0 -1
- package/dist/src/router/router.d.ts +0 -61
- package/dist/src/router/router.d.ts.map +0 -1
- package/dist/src/router/router.js +0 -183
- package/dist/src/router/router.js.map +0 -1
- package/dist/src/state/conversation.d.ts +0 -56
- package/dist/src/state/conversation.d.ts.map +0 -1
- package/dist/src/state/conversation.js +0 -133
- package/dist/src/state/conversation.js.map +0 -1
- package/dist/src/state/forms.d.ts +0 -34
- package/dist/src/state/forms.d.ts.map +0 -1
- package/dist/src/state/forms.js +0 -44
- package/dist/src/state/forms.js.map +0 -1
- package/dist/src/state/menu.d.ts +0 -78
- package/dist/src/state/menu.d.ts.map +0 -1
- package/dist/src/state/menu.js +0 -127
- package/dist/src/state/menu.js.map +0 -1
- package/dist/src/storage/storage.d.ts +0 -146
- package/dist/src/storage/storage.d.ts.map +0 -1
- package/dist/src/storage/storage.js +0 -195
- package/dist/src/storage/storage.js.map +0 -1
- package/dist/src/telegram-features.d.ts +0 -33
- package/dist/src/telegram-features.d.ts.map +0 -1
- package/dist/src/telegram-features.js +0 -71
- package/dist/src/telegram-features.js.map +0 -1
- package/dist/src/testing.d.ts +0 -24
- package/dist/src/testing.d.ts.map +0 -1
- package/dist/src/testing.js +0 -38
- package/dist/src/testing.js.map +0 -1
- package/dist/src/utils/concurrency.d.ts +0 -25
- package/dist/src/utils/concurrency.d.ts.map +0 -1
- package/dist/src/utils/concurrency.js +0 -52
- package/dist/src/utils/concurrency.js.map +0 -1
- package/dist/src/utils/files.d.ts +0 -45
- package/dist/src/utils/files.d.ts.map +0 -1
- package/dist/src/utils/files.js +0 -53
- package/dist/src/utils/files.js.map +0 -1
- package/dist/src/utils/text.d.ts +0 -39
- package/dist/src/utils/text.d.ts.map +0 -1
- package/dist/src/utils/text.js +0 -56
- package/dist/src/utils/text.js.map +0 -1
- package/dist/src/webhook/handler.d.ts +0 -19
- package/dist/src/webhook/handler.d.ts.map +0 -1
- package/dist/src/webhook/handler.js +0 -141
- package/dist/src/webhook/handler.js.map +0 -1
- package/dist-cjs/generated/api.js +0 -194
- package/dist-cjs/package.json +0 -3
- package/dist-cjs/src/api/client.js +0 -107
- package/dist-cjs/src/api/errors.js +0 -74
- package/dist-cjs/src/api/index.js +0 -21
- package/dist-cjs/src/api/telegram-types/LICENSE +0 -21
- package/dist-cjs/src/api/telegram-types/api.d.ts +0 -22
- package/dist-cjs/src/api/telegram-types/checklist.d.ts +0 -72
- package/dist-cjs/src/api/telegram-types/inline.d.ts +0 -692
- package/dist-cjs/src/api/telegram-types/langs.d.ts +0 -193
- package/dist-cjs/src/api/telegram-types/manage.d.ts +0 -1144
- package/dist-cjs/src/api/telegram-types/markup.d.ts +0 -268
- package/dist-cjs/src/api/telegram-types/message.d.ts +0 -1537
- package/dist-cjs/src/api/telegram-types/methods.d.ts +0 -2870
- package/dist-cjs/src/api/telegram-types/mod.d.ts +0 -14
- package/dist-cjs/src/api/telegram-types/passport.d.ts +0 -163
- package/dist-cjs/src/api/telegram-types/payment.d.ts +0 -570
- package/dist-cjs/src/api/telegram-types/rich.d.ts +0 -1010
- package/dist-cjs/src/api/telegram-types/settings.d.ts +0 -120
- package/dist-cjs/src/api/telegram-types/story.d.ts +0 -89
- package/dist-cjs/src/api/telegram-types/update.d.ts +0 -84
- package/dist-cjs/src/api/telegram.js +0 -2
- package/dist-cjs/src/api/transport.js +0 -267
- package/dist-cjs/src/api/types.js +0 -2
- package/dist-cjs/src/branding/terminal.js +0 -338
- package/dist-cjs/src/broadcast/broadcast.js +0 -58
- package/dist-cjs/src/cache/cache.js +0 -45
- package/dist-cjs/src/cli.js +0 -86
- package/dist-cjs/src/context/context.js +0 -305
- package/dist-cjs/src/core/bot.js +0 -510
- package/dist-cjs/src/core/events.js +0 -38
- package/dist-cjs/src/core/webhook-reply.js +0 -42
- package/dist-cjs/src/index.js +0 -47
- package/dist-cjs/src/keyboard/index.js +0 -61
- package/dist-cjs/src/middleware/compose.js +0 -20
- package/dist-cjs/src/observability/logger.js +0 -293
- package/dist-cjs/src/plugins/plugin.js +0 -63
- package/dist-cjs/src/queue/queue.js +0 -219
- package/dist-cjs/src/router/router.js +0 -186
- package/dist-cjs/src/state/conversation.js +0 -139
- package/dist-cjs/src/state/forms.js +0 -47
- package/dist-cjs/src/state/menu.js +0 -133
- package/dist-cjs/src/storage/storage.js +0 -202
- package/dist-cjs/src/telegram-features.js +0 -76
- package/dist-cjs/src/testing.js +0 -45
- package/dist-cjs/src/utils/concurrency.js +0 -57
- package/dist-cjs/src/utils/files.js +0 -58
- package/dist-cjs/src/utils/text.js +0 -63
- package/dist-cjs/src/webhook/handler.js +0 -144
- package/docs/API.id.md +0 -1935
- package/docs/API.md +0 -1969
- package/docs/API.zh-CN.md +0 -1929
- package/docs/GETTING_STARTED.id.md +0 -85
- package/docs/GETTING_STARTED.md +0 -85
- package/docs/GETTING_STARTED.zh-CN.md +0 -85
- package/docs/GITHUB_PACKAGES.id.md +0 -82
- package/docs/GITHUB_PACKAGES.md +0 -82
- package/docs/GITHUB_PACKAGES.zh-CN.md +0 -82
- package/docs/README.md +0 -48
- package/docs/STORAGE.id.md +0 -105
- package/docs/STORAGE.md +0 -105
- package/docs/STORAGE.zh-CN.md +0 -105
- package/examples/README.md +0 -37
- package/examples/files.ts +0 -35
- package/examples/minimal.ts +0 -12
- package/examples/tsconfig.json +0 -9
- package/examples/webhook.ts +0 -42
- package/examples/wizard-registration.ts +0 -42
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ReplyKeyboard = exports.InlineKeyboard = void 0;
|
|
4
|
-
exports.removeKeyboard = removeKeyboard;
|
|
5
|
-
exports.forceReply = forceReply;
|
|
6
|
-
class InlineKeyboard {
|
|
7
|
-
rows = [];
|
|
8
|
-
static from(rows) { const keyboard = new InlineKeyboard(); keyboard.rows.push(...rows.map((row) => [...row])); return keyboard; }
|
|
9
|
-
text(text, callbackData) { return this.button({ text, callback_data: callbackData }); }
|
|
10
|
-
url(text, url) { return this.button({ text, url }); }
|
|
11
|
-
webApp(text, url) { return this.button({ text, web_app: { url } }); }
|
|
12
|
-
pay(text = "Pay") { return this.button({ text, pay: true }); }
|
|
13
|
-
copy(text, copiedText) { return this.button({ text, copy_text: { text: copiedText } }); }
|
|
14
|
-
switchInline(text, query = "") { return this.button({ text, switch_inline_query: query }); }
|
|
15
|
-
switchInlineCurrent(text, query = "") { return this.button({ text, switch_inline_query_current_chat: query }); }
|
|
16
|
-
login(text, url, options = {}) { return this.button({ text, login_url: { url, ...options } }); }
|
|
17
|
-
game(text) { return this.button({ text, callback_game: {} }); }
|
|
18
|
-
button(button) { this.ensureRow().push(validateInlineButton(button)); return this; }
|
|
19
|
-
row(...buttons) { this.rows.push(buttons.map(validateInlineButton)); return this; }
|
|
20
|
-
conditional(condition, factory) { return condition ? factory(this) : this; }
|
|
21
|
-
grid(buttons, columns) { if (!Number.isInteger(columns) || columns < 1)
|
|
22
|
-
throw new RangeError("columns must be a positive integer"); for (let index = 0; index < buttons.length; index += columns)
|
|
23
|
-
this.rows.push(buttons.slice(index, index + columns).map(validateInlineButton)); return this; }
|
|
24
|
-
build() { return { inline_keyboard: this.rows.map((row) => [...row]) }; }
|
|
25
|
-
asReplyMarkup() { return this.build(); }
|
|
26
|
-
ensureRow() { const row = this.rows.at(-1); if (row)
|
|
27
|
-
return row; const next = []; this.rows.push(next); return next; }
|
|
28
|
-
}
|
|
29
|
-
exports.InlineKeyboard = InlineKeyboard;
|
|
30
|
-
class ReplyKeyboard {
|
|
31
|
-
rows = [];
|
|
32
|
-
options = {};
|
|
33
|
-
text(text) { return this.button({ text }); }
|
|
34
|
-
contact(text) { return this.button({ text, request_contact: true }); }
|
|
35
|
-
location(text) { return this.button({ text, request_location: true }); }
|
|
36
|
-
poll(text, type) { return this.button({ text, request_poll: type ? { type } : {} }); }
|
|
37
|
-
webApp(text, url) { return this.button({ text, web_app: { url } }); }
|
|
38
|
-
requestUsers(text, options = {}) { return this.button({ text, request_users: options }); }
|
|
39
|
-
requestChat(text, options = {}) { return this.button({ text, request_chat: options }); }
|
|
40
|
-
resized(resize = true) { this.options.resize_keyboard = resize; return this; }
|
|
41
|
-
oneTime(oneTime = true) { this.options.one_time_keyboard = oneTime; return this; }
|
|
42
|
-
persistent(persistent = true) { this.options.is_persistent = persistent; return this; }
|
|
43
|
-
placeholder(text) { this.options.input_field_placeholder = text; return this; }
|
|
44
|
-
selective(selective = true) { this.options.selective = selective; return this; }
|
|
45
|
-
button(button) { this.ensureRow().push(button); return this; }
|
|
46
|
-
row(...buttons) { this.rows.push([...buttons]); return this; }
|
|
47
|
-
grid(buttons, columns) { if (!Number.isInteger(columns) || columns < 1)
|
|
48
|
-
throw new RangeError("columns must be a positive integer"); for (let index = 0; index < buttons.length; index += columns)
|
|
49
|
-
this.rows.push(buttons.slice(index, index + columns)); return this; }
|
|
50
|
-
build(options = {}) { return { keyboard: this.rows.map((row) => [...row]), ...this.options, ...options }; }
|
|
51
|
-
asReplyMarkup() { return this.build(); }
|
|
52
|
-
ensureRow() { const row = this.rows.at(-1); if (row)
|
|
53
|
-
return row; const next = []; this.rows.push(next); return next; }
|
|
54
|
-
}
|
|
55
|
-
exports.ReplyKeyboard = ReplyKeyboard;
|
|
56
|
-
function removeKeyboard(selective = false) { return { remove_keyboard: true, ...(selective ? { selective: true } : {}) }; }
|
|
57
|
-
function forceReply(placeholder, selective = false) { return { force_reply: true, ...(placeholder ? { input_field_placeholder: placeholder } : {}), ...(selective ? { selective: true } : {}) }; }
|
|
58
|
-
function validateInlineButton(button) { if (!button.text)
|
|
59
|
-
throw new Error("Inline keyboard button text is required."); const actions = [button.url, button.callback_data, button.web_app, button.login_url, button.switch_inline_query, button.switch_inline_query_current_chat, button.callback_game, button.pay, button.copy_text].filter((value) => value !== undefined); if (actions.length !== 1)
|
|
60
|
-
throw new Error(`Inline button ${button.text} must contain exactly one action.`); if (button.callback_data && new TextEncoder().encode(button.callback_data).length > 64)
|
|
61
|
-
throw new RangeError("callback_data must be at most 64 bytes."); return { ...button }; }
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.compose = compose;
|
|
4
|
-
exports.middleware = middleware;
|
|
5
|
-
function compose(middleware) {
|
|
6
|
-
return async function run(ctx) {
|
|
7
|
-
let index = -1;
|
|
8
|
-
const dispatch = async (position) => {
|
|
9
|
-
if (position <= index)
|
|
10
|
-
throw new Error("next() called multiple times");
|
|
11
|
-
index = position;
|
|
12
|
-
const handler = middleware[position];
|
|
13
|
-
if (!handler)
|
|
14
|
-
return;
|
|
15
|
-
await handler(ctx, () => dispatch(position + 1));
|
|
16
|
-
};
|
|
17
|
-
await dispatch(0);
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
function middleware(handler) { return handler; }
|
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Logger = exports.INCOMING_TEXT_LIMIT = void 0;
|
|
4
|
-
exports.redact = redact;
|
|
5
|
-
exports.formatLocalStamp = formatLocalStamp;
|
|
6
|
-
exports.describeIncomingUpdate = describeIncomingUpdate;
|
|
7
|
-
exports.summarizeUpdate = summarizeUpdate;
|
|
8
|
-
exports.createLogger = createLogger;
|
|
9
|
-
// Higher number = more verbose. `silent` must sort BELOW `error` so that every
|
|
10
|
-
// message level is filtered out (a value above all others would let every
|
|
11
|
-
// message through, including errors).
|
|
12
|
-
const priorities = { silent: -1, error: 0, warn: 1, info: 2, debug: 3, trace: 4 };
|
|
13
|
-
const defaultRedactKeys = ["token", "secret", "password", "authorization", "cookie", "private_key", "api_key", "npm_token", "bot_token"];
|
|
14
|
-
const ANSI = {
|
|
15
|
-
reset: "\u001b[0m",
|
|
16
|
-
dim: "\u001b[2m",
|
|
17
|
-
red: "\u001b[31m",
|
|
18
|
-
yellow: "\u001b[33m",
|
|
19
|
-
green: "\u001b[32m",
|
|
20
|
-
cyan: "\u001b[36m",
|
|
21
|
-
blue: "\u001b[34m",
|
|
22
|
-
magenta: "\u001b[35m",
|
|
23
|
-
};
|
|
24
|
-
function isObject(value) { return typeof value === "object" && value !== null; }
|
|
25
|
-
function shouldRedact(key, keys) { const normalized = key.toLowerCase().replace(/[-_]/g, ""); return keys.some((candidate) => normalized.includes(candidate.toLowerCase().replace(/[-_]/g, ""))); }
|
|
26
|
-
function redactString(value) {
|
|
27
|
-
return value
|
|
28
|
-
.replace(/\b\d{6,12}:[A-Za-z0-9_-]{20,}\b/g, "[REDACTED_TELEGRAM_TOKEN]")
|
|
29
|
-
.replace(/\bnpm_[A-Za-z0-9]{20,}\b/g, "[REDACTED_NPM_TOKEN]");
|
|
30
|
-
}
|
|
31
|
-
function safeValue(value, keys, seen = new WeakSet()) {
|
|
32
|
-
if (typeof value === "string")
|
|
33
|
-
return redactString(value);
|
|
34
|
-
if (typeof value === "bigint")
|
|
35
|
-
return `${value}n`;
|
|
36
|
-
if (value instanceof Error)
|
|
37
|
-
return safeValue({ name: value.name, message: value.message, stack: value.stack }, keys, seen);
|
|
38
|
-
if (Array.isArray(value)) {
|
|
39
|
-
if (seen.has(value))
|
|
40
|
-
return "[Circular]";
|
|
41
|
-
seen.add(value);
|
|
42
|
-
return value.map((item) => safeValue(item, keys, seen));
|
|
43
|
-
}
|
|
44
|
-
if (!isObject(value))
|
|
45
|
-
return value;
|
|
46
|
-
if (seen.has(value))
|
|
47
|
-
return "[Circular]";
|
|
48
|
-
seen.add(value);
|
|
49
|
-
const result = {};
|
|
50
|
-
for (const [key, nested] of Object.entries(value))
|
|
51
|
-
result[key] = shouldRedact(key, keys) ? "[REDACTED]" : safeValue(nested, keys, seen);
|
|
52
|
-
return result;
|
|
53
|
-
}
|
|
54
|
-
function redact(value, keys = defaultRedactKeys) { return safeValue(value, keys); }
|
|
55
|
-
function updateType(update) {
|
|
56
|
-
if (update.callback_query)
|
|
57
|
-
return "callback_query";
|
|
58
|
-
if (update.inline_query)
|
|
59
|
-
return "inline_query";
|
|
60
|
-
if (update.business_message)
|
|
61
|
-
return "business_message";
|
|
62
|
-
if (update.edited_business_message)
|
|
63
|
-
return "edited_business_message";
|
|
64
|
-
if (update.guest_message)
|
|
65
|
-
return "guest_message";
|
|
66
|
-
if (update.edited_guest_message)
|
|
67
|
-
return "edited_guest_message";
|
|
68
|
-
if (update.message_reaction)
|
|
69
|
-
return "message_reaction";
|
|
70
|
-
if (update.message_reaction_count)
|
|
71
|
-
return "message_reaction_count";
|
|
72
|
-
if (update.chat_boost)
|
|
73
|
-
return "chat_boost";
|
|
74
|
-
if (update.removed_chat_boost)
|
|
75
|
-
return "removed_chat_boost";
|
|
76
|
-
if (update.message)
|
|
77
|
-
return "message";
|
|
78
|
-
if (update.edited_message)
|
|
79
|
-
return "edited_message";
|
|
80
|
-
if (update.channel_post)
|
|
81
|
-
return "channel_post";
|
|
82
|
-
if (update.edited_channel_post)
|
|
83
|
-
return "edited_channel_post";
|
|
84
|
-
if (update.my_chat_member)
|
|
85
|
-
return "my_chat_member";
|
|
86
|
-
if (update.chat_member)
|
|
87
|
-
return "chat_member";
|
|
88
|
-
if (update.chat_join_request)
|
|
89
|
-
return "chat_join_request";
|
|
90
|
-
if (update.poll)
|
|
91
|
-
return "poll";
|
|
92
|
-
if (update.poll_answer)
|
|
93
|
-
return "poll_answer";
|
|
94
|
-
return "unknown";
|
|
95
|
-
}
|
|
96
|
-
exports.INCOMING_TEXT_LIMIT = 50;
|
|
97
|
-
function padNumber(value) { return String(value).padStart(2, "0"); }
|
|
98
|
-
/** Formats a date as `dd/mm/yyyy hh:mm:ss` in local time. */
|
|
99
|
-
function formatLocalStamp(date = new Date()) {
|
|
100
|
-
return `${padNumber(date.getDate())}/${padNumber(date.getMonth() + 1)}/${date.getFullYear()} ${padNumber(date.getHours())}:${padNumber(date.getMinutes())}:${padNumber(date.getSeconds())}`;
|
|
101
|
-
}
|
|
102
|
-
function truncateForDisplay(value, limit = exports.INCOMING_TEXT_LIMIT) {
|
|
103
|
-
if (value.length <= limit)
|
|
104
|
-
return { text: value, truncated: false };
|
|
105
|
-
return { text: `${value.slice(0, limit)}…`, truncated: true };
|
|
106
|
-
}
|
|
107
|
-
function nicknameFromUser(user) {
|
|
108
|
-
if (!user)
|
|
109
|
-
return undefined;
|
|
110
|
-
const name = [user.first_name, user.last_name].filter(Boolean).join(" ").trim();
|
|
111
|
-
return name.length > 0 ? name : user.username ?? String(user.id);
|
|
112
|
-
}
|
|
113
|
-
function messageMediaType(message) {
|
|
114
|
-
for (const key of ["photo", "video", "video_note", "animation", "document", "audio", "voice", "sticker", "location", "venue", "contact", "dice", "poll", "story", "paid_media"]) {
|
|
115
|
-
if (message[key] !== undefined)
|
|
116
|
-
return key;
|
|
117
|
-
}
|
|
118
|
-
return undefined;
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Builds the human-readable description of an incoming update used for the
|
|
122
|
-
* `[ => ] Message From ...` terminal lines. Text messages and commands are
|
|
123
|
-
* truncated to 50 characters; callback button data is shown in full.
|
|
124
|
-
*/
|
|
125
|
-
function describeIncomingUpdate(update) {
|
|
126
|
-
const callback = update.callback_query;
|
|
127
|
-
if (callback) {
|
|
128
|
-
const result = { kind: "Callback", fromId: callback.from.id, nickname: nicknameFromUser(callback.from), contentLabel: "Data", content: callback.data };
|
|
129
|
-
return result;
|
|
130
|
-
}
|
|
131
|
-
const message = (update.message ?? update.edited_message ?? update.channel_post ?? update.edited_channel_post ?? update.business_message ?? update.edited_business_message ?? update.guest_message ?? update.edited_guest_message);
|
|
132
|
-
if (message) {
|
|
133
|
-
const from = message.from;
|
|
134
|
-
const nickname = nicknameFromUser(from) ?? (message.chat.title !== undefined ? String(message.chat.title) : message.chat.id !== undefined ? String(message.chat.id) : undefined);
|
|
135
|
-
const input = { kind: "Message", fromId: from?.id ?? message.chat.id, nickname };
|
|
136
|
-
const text = message.text;
|
|
137
|
-
if (text !== undefined) {
|
|
138
|
-
const truncated = truncateForDisplay(text);
|
|
139
|
-
input.contentLabel = "Text";
|
|
140
|
-
input.content = truncated.text;
|
|
141
|
-
input.truncated = truncated.truncated;
|
|
142
|
-
}
|
|
143
|
-
else if (message.caption !== undefined) {
|
|
144
|
-
const truncated = truncateForDisplay(message.caption);
|
|
145
|
-
input.contentLabel = "Caption";
|
|
146
|
-
input.content = truncated.text;
|
|
147
|
-
input.truncated = truncated.truncated;
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
const media = messageMediaType(message);
|
|
151
|
-
if (media !== undefined)
|
|
152
|
-
input.contentLabel = media === "photo" ? "Photo" : media.replace(/_/g, " ").replace(/^./, (character) => character.toUpperCase());
|
|
153
|
-
}
|
|
154
|
-
return input;
|
|
155
|
-
}
|
|
156
|
-
const inline = update.inline_query;
|
|
157
|
-
if (inline) {
|
|
158
|
-
const truncated = truncateForDisplay(inline.query);
|
|
159
|
-
return { kind: "Inline Query", fromId: inline.from.id, nickname: nicknameFromUser(inline.from), contentLabel: "Query", content: truncated.text, truncated: truncated.truncated };
|
|
160
|
-
}
|
|
161
|
-
const chosen = update.chosen_inline_result;
|
|
162
|
-
if (chosen)
|
|
163
|
-
return { kind: "Inline Result", fromId: chosen.from.id, nickname: nicknameFromUser(chosen.from), contentLabel: "Result", content: chosen.result_id };
|
|
164
|
-
const chatScoped = update.chat_member ?? update.my_chat_member ?? update.chat_join_request;
|
|
165
|
-
if (chatScoped) {
|
|
166
|
-
return { kind: "Update", fromId: chatScoped.from.id, nickname: nicknameFromUser(chatScoped.from), contentLabel: "Chat", content: chatScoped.chat.title ?? String(chatScoped.chat.id) };
|
|
167
|
-
}
|
|
168
|
-
const reaction = update.message_reaction;
|
|
169
|
-
const reactor = nicknameFromUser(reaction?.from);
|
|
170
|
-
if (reactor)
|
|
171
|
-
return { kind: "Reaction", fromId: reaction?.from?.id, nickname: reactor };
|
|
172
|
-
return { kind: "Update", context: { type: updateType(update) } };
|
|
173
|
-
}
|
|
174
|
-
function summarizeUpdate(update, includeContent = false) {
|
|
175
|
-
const callback = update.callback_query;
|
|
176
|
-
const message = (update.message ?? update.edited_message ?? update.channel_post ?? update.edited_channel_post ?? update.business_message ?? update.edited_business_message ?? update.guest_message ?? update.edited_guest_message ?? callback?.message);
|
|
177
|
-
const summary = { updateId: update.update_id, type: updateType(update) };
|
|
178
|
-
if (message?.chat?.id !== undefined)
|
|
179
|
-
summary.chatId = message.chat.id;
|
|
180
|
-
if (message?.from?.id !== undefined)
|
|
181
|
-
summary.fromUserId = message.from.id;
|
|
182
|
-
if (message?.message_id !== undefined)
|
|
183
|
-
summary.messageId = message.message_id;
|
|
184
|
-
if (includeContent) {
|
|
185
|
-
const text = message?.text ?? message?.caption;
|
|
186
|
-
if (text !== undefined)
|
|
187
|
-
summary.text = text;
|
|
188
|
-
if (callback?.data !== undefined)
|
|
189
|
-
summary.callbackData = callback.data;
|
|
190
|
-
}
|
|
191
|
-
return summary;
|
|
192
|
-
}
|
|
193
|
-
function levelColor(level) {
|
|
194
|
-
if (level === "error")
|
|
195
|
-
return ANSI.red;
|
|
196
|
-
if (level === "warn")
|
|
197
|
-
return ANSI.yellow;
|
|
198
|
-
if (level === "debug")
|
|
199
|
-
return ANSI.blue;
|
|
200
|
-
if (level === "trace")
|
|
201
|
-
return ANSI.magenta;
|
|
202
|
-
return ANSI.green;
|
|
203
|
-
}
|
|
204
|
-
function formatEntry(entry, color, format) {
|
|
205
|
-
if (format === "json") {
|
|
206
|
-
const line = JSON.stringify(entry);
|
|
207
|
-
return color ? `${ANSI.dim}${line}${ANSI.reset}` : line;
|
|
208
|
-
}
|
|
209
|
-
const time = entry.timestamp.slice(11, 23);
|
|
210
|
-
const label = entry.level.toUpperCase().padEnd(5, " ");
|
|
211
|
-
const context = entry.context && Object.keys(entry.context).length ? ` ${JSON.stringify(entry.context)}` : "";
|
|
212
|
-
const line = `${time} ${label} ${entry.event}${context}`;
|
|
213
|
-
return color ? `${levelColor(entry.level)}${line}${ANSI.reset}` : line;
|
|
214
|
-
}
|
|
215
|
-
function createDefaultSink(options, redactKeys) {
|
|
216
|
-
const stream = options.stream ?? process.stdout;
|
|
217
|
-
const color = options.color ?? Boolean(stream.isTTY && !process.env.NO_COLOR);
|
|
218
|
-
const format = options.format ?? "pretty";
|
|
219
|
-
return (entry) => {
|
|
220
|
-
if (typeof entry.text === "string") {
|
|
221
|
-
stream.write(`${redactString(entry.text)}\n`);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
const safeEntry = redact(entry, redactKeys);
|
|
225
|
-
const line = formatEntry(safeEntry, color, format);
|
|
226
|
-
stream.write(`${line}\n`);
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
class Logger {
|
|
230
|
-
level;
|
|
231
|
-
format;
|
|
232
|
-
color;
|
|
233
|
-
includeUpdateContent;
|
|
234
|
-
sink;
|
|
235
|
-
redactKeys;
|
|
236
|
-
baseContext;
|
|
237
|
-
constructor(options = {}) {
|
|
238
|
-
this.level = options.level ?? "info";
|
|
239
|
-
this.format = options.format ?? "pretty";
|
|
240
|
-
const stream = options.stream ?? process.stdout;
|
|
241
|
-
this.color = options.color ?? Boolean(stream.isTTY && !process.env.NO_COLOR);
|
|
242
|
-
this.includeUpdateContent = options.includeUpdateContent ?? false;
|
|
243
|
-
this.redactKeys = options.redactKeys ?? defaultRedactKeys;
|
|
244
|
-
this.sink = options.sink ?? createDefaultSink({ color: this.color, format: this.format, stream }, this.redactKeys);
|
|
245
|
-
this.baseContext = options.context ?? {};
|
|
246
|
-
}
|
|
247
|
-
child(context) { return new Logger({ level: this.level, format: this.format, color: this.color, sink: this.sink, includeUpdateContent: this.includeUpdateContent, redactKeys: this.redactKeys, context: { ...this.baseContext, ...context } }); }
|
|
248
|
-
trace(event, context) { this.write("trace", event, context); }
|
|
249
|
-
debug(event, context) { this.write("debug", event, context); }
|
|
250
|
-
info(event, context) { this.write("info", event, context); }
|
|
251
|
-
warn(event, context) { this.write("warn", event, context); }
|
|
252
|
-
error(event, context) { this.write("error", event, context); }
|
|
253
|
-
/**
|
|
254
|
-
* Logs an incoming update as the human-readable terminal line:
|
|
255
|
-
* `[ => ] Message From {id} {nickname} {dd/mm/yyyy} {hh:mm:ss}` followed by
|
|
256
|
-
* an indented content line. In `json` format it emits a structured entry
|
|
257
|
-
* with the event name `update.received`.
|
|
258
|
-
*/
|
|
259
|
-
incoming(input) {
|
|
260
|
-
if (priorities.info > priorities[this.level])
|
|
261
|
-
return;
|
|
262
|
-
if (this.format === "json") {
|
|
263
|
-
const context = { kind: input.kind, ...(input.fromId !== undefined ? { fromId: input.fromId } : {}), ...(input.nickname !== undefined ? { nickname: input.nickname } : {}), ...(input.content !== undefined ? { content: input.content } : {}), ...(input.truncated ? { truncated: true } : {}), ...input.context };
|
|
264
|
-
this.write("info", "update.received", context);
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
const stamp = formatLocalStamp();
|
|
268
|
-
const identity = [input.fromId !== undefined ? String(input.fromId) : undefined, input.nickname].filter(Boolean).join(" ");
|
|
269
|
-
const arrow = this.color ? `\u001b[36m\u001b[1m[ => ]\u001b[0m` : "[ => ]";
|
|
270
|
-
const kind = this.color ? `\u001b[1m${input.kind}\u001b[0m` : input.kind;
|
|
271
|
-
const header = `${arrow} ${kind} From ${identity} ${this.color ? `\u001b[2m${stamp}\u001b[0m` : stamp}`;
|
|
272
|
-
const lines = [redactString(header)];
|
|
273
|
-
if (input.content !== undefined || input.contentLabel !== undefined) {
|
|
274
|
-
const label = input.contentLabel ?? "Content";
|
|
275
|
-
const value = input.content ?? "";
|
|
276
|
-
const contentLine = ` ↳ ${label}: ${value}`;
|
|
277
|
-
lines.push(this.color ? `\u001b[2m${redactString(contentLine)}\u001b[0m` : redactString(contentLine));
|
|
278
|
-
}
|
|
279
|
-
this.sink({ timestamp: new Date().toISOString(), level: "info", event: "update.received", text: lines.join("\n") });
|
|
280
|
-
}
|
|
281
|
-
write(level, event, context = {}) {
|
|
282
|
-
if (priorities[level] > priorities[this.level])
|
|
283
|
-
return;
|
|
284
|
-
const merged = { ...this.baseContext, ...context };
|
|
285
|
-
const safe = redact(merged, this.redactKeys);
|
|
286
|
-
const entry = { timestamp: new Date().toISOString(), level, event };
|
|
287
|
-
if (Object.keys(safe).length)
|
|
288
|
-
entry.context = safe;
|
|
289
|
-
this.sink(entry);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
exports.Logger = Logger;
|
|
293
|
-
function createLogger(options) { return new Logger(options); }
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PluginManager = exports.ServiceContainer = void 0;
|
|
4
|
-
class ServiceContainer {
|
|
5
|
-
values = new Map();
|
|
6
|
-
register(name, value) { this.values.set(name, value); return this; }
|
|
7
|
-
get(name) { if (!this.values.has(name))
|
|
8
|
-
throw new Error(`Service not registered: ${String(name)}`); return this.values.get(name); }
|
|
9
|
-
has(name) { return this.values.has(name); }
|
|
10
|
-
delete(name) { return this.values.delete(name); }
|
|
11
|
-
}
|
|
12
|
-
exports.ServiceContainer = ServiceContainer;
|
|
13
|
-
class PluginManager {
|
|
14
|
-
plugins = [];
|
|
15
|
-
api;
|
|
16
|
-
setupComplete = false;
|
|
17
|
-
constructor(bot) {
|
|
18
|
-
const host = bot;
|
|
19
|
-
this.api = {
|
|
20
|
-
bot,
|
|
21
|
-
services: new ServiceContainer(),
|
|
22
|
-
registerMiddleware: (middleware) => {
|
|
23
|
-
if (typeof middleware !== "function" || !host.use)
|
|
24
|
-
throw new TypeError("Plugin middleware must be a function and the bot must support use().");
|
|
25
|
-
host.use(middleware);
|
|
26
|
-
},
|
|
27
|
-
registerRoute: (route) => {
|
|
28
|
-
if (typeof route === "function") {
|
|
29
|
-
route(bot);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
if (!route || typeof route !== "object" || !host.router?.route)
|
|
33
|
-
throw new TypeError("Plugin route must be a registration function or route descriptor.");
|
|
34
|
-
const descriptor = route;
|
|
35
|
-
if (typeof descriptor.matcher !== "function" || !Array.isArray(descriptor.middleware) || descriptor.middleware.some((item) => typeof item !== "function"))
|
|
36
|
-
throw new TypeError("Plugin route descriptor is invalid.");
|
|
37
|
-
host.router.route(descriptor.matcher, ...descriptor.middleware);
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
use(plugin) { if (this.plugins.some((existing) => existing.name === plugin.name))
|
|
42
|
-
throw new Error(`Plugin already registered: ${plugin.name}`); this.plugins.push(plugin); return this; }
|
|
43
|
-
/** Installs plugins once; re-running setup after a restart must not double-register middleware. */
|
|
44
|
-
async setup() {
|
|
45
|
-
if (this.setupComplete)
|
|
46
|
-
return;
|
|
47
|
-
for (const plugin of this.plugins) {
|
|
48
|
-
await plugin.install?.(this.api);
|
|
49
|
-
await plugin.setup?.(this.api);
|
|
50
|
-
}
|
|
51
|
-
this.setupComplete = true;
|
|
52
|
-
}
|
|
53
|
-
async start() { for (const plugin of this.plugins)
|
|
54
|
-
await plugin.onStart?.(this.api); }
|
|
55
|
-
async update(context) { for (const plugin of this.plugins)
|
|
56
|
-
await plugin.onUpdate?.(context); }
|
|
57
|
-
async stop() { for (const plugin of this.plugins)
|
|
58
|
-
await plugin.onStop?.(this.api); }
|
|
59
|
-
async dispose() { for (const plugin of [...this.plugins].reverse())
|
|
60
|
-
await plugin.dispose?.(this.api); }
|
|
61
|
-
list() { return this.plugins; }
|
|
62
|
-
}
|
|
63
|
-
exports.PluginManager = PluginManager;
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Scheduler = exports.TaskQueue = void 0;
|
|
4
|
-
exports.parseCronExpression = parseCronExpression;
|
|
5
|
-
exports.nextCronOccurrence = nextCronOccurrence;
|
|
6
|
-
function sleep(delayMs) { return new Promise((resolve) => setTimeout(resolve, delayMs)); }
|
|
7
|
-
class TaskQueue {
|
|
8
|
-
worker;
|
|
9
|
-
options;
|
|
10
|
-
pending = [];
|
|
11
|
-
jobs = new Map();
|
|
12
|
-
controllers = new Map();
|
|
13
|
-
active = 0;
|
|
14
|
-
sequence = 0;
|
|
15
|
-
draining = false;
|
|
16
|
-
closed = false;
|
|
17
|
-
constructor(worker, options = {}) {
|
|
18
|
-
this.worker = worker;
|
|
19
|
-
this.options = options;
|
|
20
|
-
if ((options.concurrency ?? 1) < 1 || !Number.isInteger(options.concurrency ?? 1))
|
|
21
|
-
throw new RangeError("concurrency must be a positive integer");
|
|
22
|
-
if ((options.retries ?? 0) < 0 || !Number.isInteger(options.retries ?? 0))
|
|
23
|
-
throw new RangeError("retries must be a non-negative integer");
|
|
24
|
-
}
|
|
25
|
-
add(data, options = {}) {
|
|
26
|
-
if (this.closed)
|
|
27
|
-
throw new Error("TaskQueue is closed");
|
|
28
|
-
const id = options.id ?? `job_${Date.now()}_${this.sequence++}`;
|
|
29
|
-
if (this.jobs.has(id))
|
|
30
|
-
throw new Error(`Job already exists: ${id}`);
|
|
31
|
-
if (options.delayMs !== undefined && options.delayMs < 0)
|
|
32
|
-
throw new RangeError("delayMs must be non-negative");
|
|
33
|
-
const job = { id, data, attempts: 0, priority: options.priority ?? 0, runAt: Date.now() + (options.delayMs ?? 0), status: "queued" };
|
|
34
|
-
this.jobs.set(id, job);
|
|
35
|
-
this.pending.push(job);
|
|
36
|
-
this.pending.sort((a, b) => b.priority - a.priority || a.runAt - b.runAt);
|
|
37
|
-
void this.drain();
|
|
38
|
-
return { ...job };
|
|
39
|
-
}
|
|
40
|
-
get(id) { const job = this.jobs.get(id); return job ? { ...job } : undefined; }
|
|
41
|
-
cancel(id) { const job = this.jobs.get(id); if (!job || ["completed", "failed", "cancelled"].includes(job.status))
|
|
42
|
-
return false; job.status = "cancelled"; this.controllers.get(id)?.abort(); return true; }
|
|
43
|
-
async onIdle() { while (this.pending.some((job) => job.status === "queued") || this.active)
|
|
44
|
-
await sleep(10); }
|
|
45
|
-
async close() {
|
|
46
|
-
this.closed = true;
|
|
47
|
-
this.draining = false;
|
|
48
|
-
for (const id of this.controllers.keys())
|
|
49
|
-
this.cancel(id);
|
|
50
|
-
for (const job of this.pending)
|
|
51
|
-
if (job.status === "queued")
|
|
52
|
-
job.status = "cancelled";
|
|
53
|
-
this.pending.length = 0;
|
|
54
|
-
await this.onIdle();
|
|
55
|
-
}
|
|
56
|
-
async drain() {
|
|
57
|
-
if (this.draining || this.closed)
|
|
58
|
-
return;
|
|
59
|
-
this.draining = true;
|
|
60
|
-
try {
|
|
61
|
-
while (!this.closed && this.pending.some((job) => job.status === "queued") && this.active < (this.options.concurrency ?? 1)) {
|
|
62
|
-
const index = this.pending.findIndex((job) => job.runAt <= Date.now() && job.status === "queued");
|
|
63
|
-
if (index < 0) {
|
|
64
|
-
const next = Math.min(...this.pending.filter((job) => job.status === "queued").map((job) => job.runAt));
|
|
65
|
-
await sleep(Math.max(1, next - Date.now()));
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
const [job] = this.pending.splice(index, 1);
|
|
69
|
-
if (!job)
|
|
70
|
-
continue;
|
|
71
|
-
this.active += 1;
|
|
72
|
-
void this.run(job).finally(() => { this.active -= 1; void this.drain(); });
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
finally {
|
|
76
|
-
this.draining = false;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
async run(job) {
|
|
80
|
-
const maxAttempts = this.options.retries ?? 0;
|
|
81
|
-
const controller = new AbortController();
|
|
82
|
-
this.controllers.set(job.id, controller);
|
|
83
|
-
try {
|
|
84
|
-
while (job.attempts <= maxAttempts && job.status !== "cancelled") {
|
|
85
|
-
job.status = "running";
|
|
86
|
-
job.attempts += 1;
|
|
87
|
-
try {
|
|
88
|
-
await this.worker(job, controller.signal);
|
|
89
|
-
job.status = "completed";
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
job.error = error;
|
|
94
|
-
const wasCancelled = job.status === "cancelled";
|
|
95
|
-
if (job.attempts > maxAttempts || wasCancelled) {
|
|
96
|
-
job.status = wasCancelled ? "cancelled" : "failed";
|
|
97
|
-
await this.options.onError?.(error, { ...job });
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const delay = Math.min(this.options.maxBackoffMs ?? 30_000, (this.options.backoffMs ?? 250) * 2 ** (job.attempts - 1));
|
|
101
|
-
await sleep(delay);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
finally {
|
|
106
|
-
this.controllers.delete(job.id);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
exports.TaskQueue = TaskQueue;
|
|
111
|
-
function parseCronField(token, min, max, normalize) {
|
|
112
|
-
if (!token)
|
|
113
|
-
throw new Error("Cron fields cannot be empty");
|
|
114
|
-
const values = new Set();
|
|
115
|
-
for (const part of token.split(",")) {
|
|
116
|
-
const [rangePart, stepPart] = part.split("/");
|
|
117
|
-
const step = stepPart === undefined ? 1 : Number(stepPart);
|
|
118
|
-
if (!Number.isInteger(step) || step < 1)
|
|
119
|
-
throw new Error(`Invalid cron step: ${part}`);
|
|
120
|
-
const bounds = rangePart === "*" ? [min, max] : rangePart?.split("-").map(Number);
|
|
121
|
-
if (!bounds || bounds.length < 1 || bounds.some((value) => !Number.isInteger(value)))
|
|
122
|
-
throw new Error(`Invalid cron range: ${part}`);
|
|
123
|
-
const start = bounds[0];
|
|
124
|
-
const end = (bounds[1] ?? start);
|
|
125
|
-
if (start < min || end > max || start > end)
|
|
126
|
-
throw new Error(`Cron value out of range: ${part}`);
|
|
127
|
-
for (let value = start; value <= end; value += step)
|
|
128
|
-
values.add(normalize ? normalize(value) : value);
|
|
129
|
-
}
|
|
130
|
-
return { min, max, values };
|
|
131
|
-
}
|
|
132
|
-
function parseCronExpression(expression) {
|
|
133
|
-
const fields = expression.trim().split(/\s+/);
|
|
134
|
-
if (fields.length !== 5)
|
|
135
|
-
throw new Error("Cron expression must contain exactly five fields: minute hour day-of-month month day-of-week");
|
|
136
|
-
return {
|
|
137
|
-
minute: parseCronField(fields[0], 0, 59),
|
|
138
|
-
hour: parseCronField(fields[1], 0, 23),
|
|
139
|
-
dayOfMonth: parseCronField(fields[2], 1, 31),
|
|
140
|
-
month: parseCronField(fields[3], 1, 12),
|
|
141
|
-
dayOfWeek: parseCronField(fields[4], 0, 7, (value) => value === 7 ? 0 : value),
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
function nextCronOccurrence(expression, from = new Date()) {
|
|
145
|
-
const cron = typeof expression === "string" ? parseCronExpression(expression) : expression;
|
|
146
|
-
const candidate = new Date(from);
|
|
147
|
-
candidate.setSeconds(0, 0);
|
|
148
|
-
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
149
|
-
const maxMinutes = 366 * 24 * 60 * 5;
|
|
150
|
-
for (let index = 0; index < maxMinutes; index += 1) {
|
|
151
|
-
const dayOfMonthMatches = cron.dayOfMonth.values.has(candidate.getDate());
|
|
152
|
-
const dayOfWeekMatches = cron.dayOfWeek.values.has(candidate.getDay());
|
|
153
|
-
const dayMatches = cron.dayOfMonth.values.size === cron.dayOfMonth.max - cron.dayOfMonth.min + 1
|
|
154
|
-
|| cron.dayOfWeek.values.size === cron.dayOfWeek.max - cron.dayOfWeek.min + 1
|
|
155
|
-
? dayOfMonthMatches && dayOfWeekMatches
|
|
156
|
-
: dayOfMonthMatches || dayOfWeekMatches;
|
|
157
|
-
if (cron.month.values.has(candidate.getMonth() + 1) && cron.hour.values.has(candidate.getHours()) && cron.minute.values.has(candidate.getMinutes()) && dayMatches)
|
|
158
|
-
return new Date(candidate);
|
|
159
|
-
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
160
|
-
}
|
|
161
|
-
throw new Error(`Cron expression has no occurrence within five years: ${typeof expression === "string" ? expression : "[parsed]"}`);
|
|
162
|
-
}
|
|
163
|
-
class Scheduler {
|
|
164
|
-
options;
|
|
165
|
-
timers = new Map();
|
|
166
|
-
nextRuns = new Map();
|
|
167
|
-
constructor(options = {}) {
|
|
168
|
-
this.options = options;
|
|
169
|
-
}
|
|
170
|
-
every(id, intervalMs, task) {
|
|
171
|
-
if (!Number.isFinite(intervalMs) || intervalMs <= 0)
|
|
172
|
-
throw new RangeError("intervalMs must be positive");
|
|
173
|
-
this.cancel(id);
|
|
174
|
-
const timer = setInterval(() => { void this.run(id, task); }, intervalMs);
|
|
175
|
-
this.timers.set(id, timer);
|
|
176
|
-
return { id, cancel: () => this.cancel(id) };
|
|
177
|
-
}
|
|
178
|
-
after(id, delayMs, task) {
|
|
179
|
-
if (!Number.isFinite(delayMs) || delayMs < 0)
|
|
180
|
-
throw new RangeError("delayMs must be non-negative");
|
|
181
|
-
this.cancel(id);
|
|
182
|
-
const timer = setTimeout(() => { this.timers.delete(id); void this.run(id, task); }, delayMs);
|
|
183
|
-
this.timers.set(id, timer);
|
|
184
|
-
return { id, cancel: () => this.cancel(id) };
|
|
185
|
-
}
|
|
186
|
-
cron(id, expression, task) {
|
|
187
|
-
const parsed = parseCronExpression(expression);
|
|
188
|
-
this.cancel(id);
|
|
189
|
-
const scheduleNext = () => {
|
|
190
|
-
const nextRunAt = nextCronOccurrence(parsed);
|
|
191
|
-
this.nextRuns.set(id, nextRunAt);
|
|
192
|
-
const timer = setTimeout(() => {
|
|
193
|
-
this.timers.delete(id);
|
|
194
|
-
void this.run(id, task).finally(() => { if (this.nextRuns.has(id))
|
|
195
|
-
scheduleNext(); });
|
|
196
|
-
}, Math.max(1, nextRunAt.getTime() - Date.now()));
|
|
197
|
-
this.timers.set(id, timer);
|
|
198
|
-
};
|
|
199
|
-
scheduleNext();
|
|
200
|
-
const scheduler = this;
|
|
201
|
-
return { id, cancel: () => scheduler.cancel(id), get nextRunAt() { return scheduler.nextRuns.get(id); } };
|
|
202
|
-
}
|
|
203
|
-
getNextRun(id) { const value = this.nextRuns.get(id); return value ? new Date(value) : undefined; }
|
|
204
|
-
cancel(id) { const timer = this.timers.get(id); if (!timer) {
|
|
205
|
-
this.nextRuns.delete(id);
|
|
206
|
-
return false;
|
|
207
|
-
} clearTimeout(timer); clearInterval(timer); this.timers.delete(id); this.nextRuns.delete(id); return true; }
|
|
208
|
-
clear() { for (const id of [...this.timers.keys()])
|
|
209
|
-
this.cancel(id); }
|
|
210
|
-
async run(id, task) {
|
|
211
|
-
try {
|
|
212
|
-
await task();
|
|
213
|
-
}
|
|
214
|
-
catch (error) {
|
|
215
|
-
await this.options.onError?.(error, id);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
exports.Scheduler = Scheduler;
|