@xbibzlibrary/telebibz 0.1.2 → 0.1.4
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 +17 -1
- package/CODE_OF_CONDUCT.md +37 -0
- package/CONTRIBUTING.md +54 -0
- package/CONTRIBUTION_RULES.md +41 -0
- package/GOVERNANCE.md +47 -0
- package/NOTICE.md +7 -0
- package/README.id.md +172 -0
- package/README.md +65 -33
- package/README.zh-CN.md +172 -0
- package/RELEASE_AUTOMATION.md +66 -0
- package/RELEASE_POLICY.md +2 -2
- package/SECURITY.md +36 -4
- package/SUPPORT.md +30 -0
- package/assets/readme-preview.html +75 -0
- package/assets/telebibz-readme-preview.png +0 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.d.ts.map +1 -1
- package/dist/src/api/index.js +1 -0
- package/dist/src/api/index.js.map +1 -1
- package/dist/src/api/telegram-types/LICENSE +21 -0
- package/dist/src/api/telegram-types/api.d.ts +22 -0
- package/dist/src/api/telegram-types/checklist.d.ts +72 -0
- package/dist/src/api/telegram-types/inline.d.ts +692 -0
- package/dist/src/api/telegram-types/langs.d.ts +193 -0
- package/dist/src/api/telegram-types/manage.d.ts +1144 -0
- package/dist/src/api/telegram-types/markup.d.ts +268 -0
- package/dist/src/api/telegram-types/message.d.ts +1537 -0
- package/dist/src/api/telegram-types/methods.d.ts +2870 -0
- package/dist/src/api/telegram-types/mod.d.ts +14 -0
- package/dist/src/api/telegram-types/passport.d.ts +163 -0
- package/dist/src/api/telegram-types/payment.d.ts +570 -0
- package/dist/src/api/telegram-types/rich.d.ts +1010 -0
- package/dist/src/api/telegram-types/settings.d.ts +120 -0
- package/dist/src/api/telegram-types/story.d.ts +89 -0
- package/dist/src/api/telegram-types/update.d.ts +84 -0
- package/dist/src/api/telegram.d.ts +7 -0
- package/dist/src/api/telegram.d.ts.map +1 -0
- package/dist/src/api/telegram.js +2 -0
- package/dist/src/api/telegram.js.map +1 -0
- package/dist/src/approval/approval.d.ts +8 -0
- package/dist/src/approval/approval.d.ts.map +1 -1
- package/dist/src/approval/approval.js +9 -0
- package/dist/src/approval/approval.js.map +1 -1
- package/dist/src/cache/cache.d.ts +6 -5
- package/dist/src/cache/cache.d.ts.map +1 -1
- package/dist/src/cache/cache.js +7 -3
- package/dist/src/cache/cache.js.map +1 -1
- package/dist/src/context/context.d.ts.map +1 -1
- package/dist/src/context/context.js +26 -3
- package/dist/src/context/context.js.map +1 -1
- package/dist/src/core/bot.d.ts +6 -4
- package/dist/src/core/bot.d.ts.map +1 -1
- package/dist/src/core/bot.js +48 -7
- package/dist/src/core/bot.js.map +1 -1
- package/dist/src/core/events.d.ts +4 -0
- package/dist/src/core/events.d.ts.map +1 -1
- package/dist/src/core/events.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/queue/queue.d.ts +25 -0
- package/dist/src/queue/queue.d.ts.map +1 -1
- package/dist/src/queue/queue.js +175 -51
- package/dist/src/queue/queue.js.map +1 -1
- package/dist/src/router/router.d.ts +8 -1
- package/dist/src/router/router.d.ts.map +1 -1
- package/dist/src/router/router.js +75 -17
- package/dist/src/router/router.js.map +1 -1
- package/dist/src/state/conversation.d.ts +6 -0
- package/dist/src/state/conversation.d.ts.map +1 -1
- package/dist/src/state/conversation.js +79 -11
- package/dist/src/state/conversation.js.map +1 -1
- package/dist/src/state/menu.d.ts +53 -5
- package/dist/src/state/menu.d.ts.map +1 -1
- package/dist/src/state/menu.js +116 -17
- package/dist/src/state/menu.js.map +1 -1
- package/dist/src/storage/storage.d.ts +115 -12
- package/dist/src/storage/storage.d.ts.map +1 -1
- package/dist/src/storage/storage.js +130 -4
- package/dist/src/storage/storage.js.map +1 -1
- package/dist/src/telegram-features.d.ts +33 -0
- package/dist/src/telegram-features.d.ts.map +1 -0
- package/dist/src/telegram-features.js +69 -0
- package/dist/src/telegram-features.js.map +1 -0
- package/dist/src/testing.d.ts +1 -0
- package/dist/src/testing.d.ts.map +1 -1
- package/dist/src/testing.js +16 -0
- package/dist/src/testing.js.map +1 -1
- package/dist-cjs/src/api/index.js +1 -0
- package/dist-cjs/src/api/telegram-types/LICENSE +21 -0
- package/dist-cjs/src/api/telegram-types/api.d.ts +22 -0
- package/dist-cjs/src/api/telegram-types/checklist.d.ts +72 -0
- package/dist-cjs/src/api/telegram-types/inline.d.ts +692 -0
- package/dist-cjs/src/api/telegram-types/langs.d.ts +193 -0
- package/dist-cjs/src/api/telegram-types/manage.d.ts +1144 -0
- package/dist-cjs/src/api/telegram-types/markup.d.ts +268 -0
- package/dist-cjs/src/api/telegram-types/message.d.ts +1537 -0
- package/dist-cjs/src/api/telegram-types/methods.d.ts +2870 -0
- package/dist-cjs/src/api/telegram-types/mod.d.ts +14 -0
- package/dist-cjs/src/api/telegram-types/passport.d.ts +163 -0
- package/dist-cjs/src/api/telegram-types/payment.d.ts +570 -0
- package/dist-cjs/src/api/telegram-types/rich.d.ts +1010 -0
- package/dist-cjs/src/api/telegram-types/settings.d.ts +120 -0
- package/dist-cjs/src/api/telegram-types/story.d.ts +89 -0
- package/dist-cjs/src/api/telegram-types/update.d.ts +84 -0
- package/dist-cjs/src/api/telegram.js +2 -0
- package/dist-cjs/src/approval/approval.js +11 -1
- package/dist-cjs/src/cache/cache.js +7 -3
- package/dist-cjs/src/context/context.js +26 -3
- package/dist-cjs/src/core/bot.js +48 -7
- package/dist-cjs/src/index.js +1 -0
- package/dist-cjs/src/queue/queue.js +177 -51
- package/dist-cjs/src/router/router.js +75 -17
- package/dist-cjs/src/state/conversation.js +79 -11
- package/dist-cjs/src/state/menu.js +118 -18
- package/dist-cjs/src/storage/storage.js +135 -5
- package/dist-cjs/src/telegram-features.js +74 -0
- package/dist-cjs/src/testing.js +17 -0
- package/docs/API.id.md +1800 -0
- package/docs/API.md +1799 -0
- package/docs/API.zh-CN.md +1794 -0
- package/docs/README.md +36 -17
- package/package.json +20 -3
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
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)); }
|
|
4
7
|
class TaskQueue {
|
|
5
8
|
worker;
|
|
6
9
|
options;
|
|
@@ -10,77 +13,200 @@ class TaskQueue {
|
|
|
10
13
|
active = 0;
|
|
11
14
|
sequence = 0;
|
|
12
15
|
draining = false;
|
|
16
|
+
closed = false;
|
|
13
17
|
constructor(worker, options = {}) {
|
|
14
18
|
this.worker = worker;
|
|
15
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 };
|
|
16
39
|
}
|
|
17
|
-
add(data, options = {}) { const id = options.id ?? `job_${Date.now()}_${this.sequence++}`; if (this.jobs.has(id))
|
|
18
|
-
throw new Error(`Job already exists: ${id}`); const job = { id, data, attempts: 0, priority: options.priority ?? 0, runAt: Date.now() + (options.delayMs ?? 0), status: "queued" }; this.jobs.set(id, job); this.pending.push(job); this.pending.sort((a, b) => b.priority - a.priority || a.runAt - b.runAt); void this.drain(); return { ...job }; }
|
|
19
40
|
get(id) { const job = this.jobs.get(id); return job ? { ...job } : undefined; }
|
|
20
41
|
cancel(id) { const job = this.jobs.get(id); if (!job || ["completed", "failed", "cancelled"].includes(job.status))
|
|
21
42
|
return false; job.status = "cancelled"; this.controllers.get(id)?.abort(); return true; }
|
|
22
|
-
async onIdle() { while (this.pending.
|
|
23
|
-
await
|
|
24
|
-
async close() { this.draining = false; for (const id of this.controllers.keys())
|
|
25
|
-
this.cancel(id);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
async onIdle() { while (this.pending.some((job) => job.status === "queued") || this.active)
|
|
44
|
+
await sleep(10); }
|
|
45
|
+
async close() { this.closed = true; this.draining = false; for (const id of this.controllers.keys())
|
|
46
|
+
this.cancel(id); for (const job of this.pending)
|
|
47
|
+
if (job.status === "queued")
|
|
48
|
+
job.status = "cancelled"; this.pending.length = 0; }
|
|
49
|
+
async drain() {
|
|
50
|
+
if (this.draining || this.closed)
|
|
51
|
+
return;
|
|
52
|
+
this.draining = true;
|
|
53
|
+
try {
|
|
54
|
+
while (!this.closed && this.pending.some((job) => job.status === "queued") && this.active < (this.options.concurrency ?? 1)) {
|
|
55
|
+
const index = this.pending.findIndex((job) => job.runAt <= Date.now() && job.status === "queued");
|
|
56
|
+
if (index < 0) {
|
|
57
|
+
const next = Math.min(...this.pending.filter((job) => job.status === "queued").map((job) => job.runAt));
|
|
58
|
+
await sleep(Math.max(1, next - Date.now()));
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const [job] = this.pending.splice(index, 1);
|
|
62
|
+
if (!job)
|
|
63
|
+
continue;
|
|
64
|
+
this.active += 1;
|
|
65
|
+
void this.run(job).finally(() => { this.active -= 1; void this.drain(); });
|
|
34
66
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.active += 1;
|
|
39
|
-
void this.run(job).finally(() => { this.active -= 1; void this.drain(); });
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
this.draining = false;
|
|
40
70
|
}
|
|
41
71
|
}
|
|
42
|
-
|
|
43
|
-
this.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
job.status
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
job.error = error;
|
|
56
|
-
if (job.attempts > maxAttempts) {
|
|
57
|
-
job.status = "failed";
|
|
72
|
+
async run(job) {
|
|
73
|
+
const maxAttempts = this.options.retries ?? 0;
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
this.controllers.set(job.id, controller);
|
|
76
|
+
try {
|
|
77
|
+
while (job.attempts <= maxAttempts && job.status !== "cancelled") {
|
|
78
|
+
job.status = "running";
|
|
79
|
+
job.attempts += 1;
|
|
80
|
+
try {
|
|
81
|
+
await this.worker(job, controller.signal);
|
|
82
|
+
job.status = "completed";
|
|
58
83
|
return;
|
|
59
84
|
}
|
|
60
|
-
|
|
61
|
-
|
|
85
|
+
catch (error) {
|
|
86
|
+
job.error = error;
|
|
87
|
+
const wasCancelled = job.status === "cancelled";
|
|
88
|
+
if (job.attempts > maxAttempts || wasCancelled) {
|
|
89
|
+
job.status = wasCancelled ? "cancelled" : "failed";
|
|
90
|
+
await this.options.onError?.(error, { ...job });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const delay = Math.min(this.options.maxBackoffMs ?? 30_000, (this.options.backoffMs ?? 250) * 2 ** (job.attempts - 1));
|
|
94
|
+
await sleep(delay);
|
|
95
|
+
}
|
|
62
96
|
}
|
|
63
97
|
}
|
|
98
|
+
finally {
|
|
99
|
+
this.controllers.delete(job.id);
|
|
100
|
+
}
|
|
64
101
|
}
|
|
65
|
-
finally {
|
|
66
|
-
this.controllers.delete(job.id);
|
|
67
|
-
} }
|
|
68
102
|
}
|
|
69
103
|
exports.TaskQueue = TaskQueue;
|
|
104
|
+
function parseCronField(token, min, max, normalize) {
|
|
105
|
+
if (!token)
|
|
106
|
+
throw new Error("Cron fields cannot be empty");
|
|
107
|
+
const values = new Set();
|
|
108
|
+
for (const part of token.split(",")) {
|
|
109
|
+
const [rangePart, stepPart] = part.split("/");
|
|
110
|
+
const step = stepPart === undefined ? 1 : Number(stepPart);
|
|
111
|
+
if (!Number.isInteger(step) || step < 1)
|
|
112
|
+
throw new Error(`Invalid cron step: ${part}`);
|
|
113
|
+
const bounds = rangePart === "*" ? [min, max] : rangePart?.split("-").map(Number);
|
|
114
|
+
if (!bounds || bounds.length < 1 || bounds.some((value) => !Number.isInteger(value)))
|
|
115
|
+
throw new Error(`Invalid cron range: ${part}`);
|
|
116
|
+
const start = bounds[0];
|
|
117
|
+
const end = (bounds[1] ?? start);
|
|
118
|
+
if (start < min || end > max || start > end)
|
|
119
|
+
throw new Error(`Cron value out of range: ${part}`);
|
|
120
|
+
for (let value = start; value <= end; value += step)
|
|
121
|
+
values.add(normalize ? normalize(value) : value);
|
|
122
|
+
}
|
|
123
|
+
return { min, max, values };
|
|
124
|
+
}
|
|
125
|
+
function parseCronExpression(expression) {
|
|
126
|
+
const fields = expression.trim().split(/\s+/);
|
|
127
|
+
if (fields.length !== 5)
|
|
128
|
+
throw new Error("Cron expression must contain exactly five fields: minute hour day-of-month month day-of-week");
|
|
129
|
+
return {
|
|
130
|
+
minute: parseCronField(fields[0], 0, 59),
|
|
131
|
+
hour: parseCronField(fields[1], 0, 23),
|
|
132
|
+
dayOfMonth: parseCronField(fields[2], 1, 31),
|
|
133
|
+
month: parseCronField(fields[3], 1, 12),
|
|
134
|
+
dayOfWeek: parseCronField(fields[4], 0, 7, (value) => value === 7 ? 0 : value),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function nextCronOccurrence(expression, from = new Date()) {
|
|
138
|
+
const cron = typeof expression === "string" ? parseCronExpression(expression) : expression;
|
|
139
|
+
const candidate = new Date(from);
|
|
140
|
+
candidate.setSeconds(0, 0);
|
|
141
|
+
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
142
|
+
const maxMinutes = 366 * 24 * 60 * 5;
|
|
143
|
+
for (let index = 0; index < maxMinutes; index += 1) {
|
|
144
|
+
const dayOfMonthMatches = cron.dayOfMonth.values.has(candidate.getDate());
|
|
145
|
+
const dayOfWeekMatches = cron.dayOfWeek.values.has(candidate.getDay());
|
|
146
|
+
const dayMatches = cron.dayOfMonth.values.size === cron.dayOfMonth.max - cron.dayOfMonth.min + 1
|
|
147
|
+
|| cron.dayOfWeek.values.size === cron.dayOfWeek.max - cron.dayOfWeek.min + 1
|
|
148
|
+
? dayOfMonthMatches && dayOfWeekMatches
|
|
149
|
+
: dayOfMonthMatches || dayOfWeekMatches;
|
|
150
|
+
if (cron.month.values.has(candidate.getMonth() + 1) && cron.hour.values.has(candidate.getHours()) && cron.minute.values.has(candidate.getMinutes()) && dayMatches)
|
|
151
|
+
return new Date(candidate);
|
|
152
|
+
candidate.setMinutes(candidate.getMinutes() + 1);
|
|
153
|
+
}
|
|
154
|
+
throw new Error(`Cron expression has no occurrence within five years: ${typeof expression === "string" ? expression : "[parsed]"}`);
|
|
155
|
+
}
|
|
70
156
|
class Scheduler {
|
|
157
|
+
options;
|
|
71
158
|
timers = new Map();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
159
|
+
nextRuns = new Map();
|
|
160
|
+
constructor(options = {}) {
|
|
161
|
+
this.options = options;
|
|
162
|
+
}
|
|
163
|
+
every(id, intervalMs, task) {
|
|
164
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0)
|
|
165
|
+
throw new RangeError("intervalMs must be positive");
|
|
166
|
+
this.cancel(id);
|
|
167
|
+
const timer = setInterval(() => { void this.run(id, task); }, intervalMs);
|
|
168
|
+
this.timers.set(id, timer);
|
|
169
|
+
return { id, cancel: () => this.cancel(id) };
|
|
170
|
+
}
|
|
171
|
+
after(id, delayMs, task) {
|
|
172
|
+
if (!Number.isFinite(delayMs) || delayMs < 0)
|
|
173
|
+
throw new RangeError("delayMs must be non-negative");
|
|
174
|
+
this.cancel(id);
|
|
175
|
+
const timer = setTimeout(() => { this.timers.delete(id); void this.run(id, task); }, delayMs);
|
|
176
|
+
this.timers.set(id, timer);
|
|
177
|
+
return { id, cancel: () => this.cancel(id) };
|
|
178
|
+
}
|
|
179
|
+
cron(id, expression, task) {
|
|
180
|
+
const parsed = parseCronExpression(expression);
|
|
181
|
+
this.cancel(id);
|
|
182
|
+
const scheduleNext = () => {
|
|
183
|
+
const nextRunAt = nextCronOccurrence(parsed);
|
|
184
|
+
this.nextRuns.set(id, nextRunAt);
|
|
185
|
+
const timer = setTimeout(() => {
|
|
186
|
+
this.timers.delete(id);
|
|
187
|
+
void this.run(id, task).finally(() => { if (this.nextRuns.has(id))
|
|
188
|
+
scheduleNext(); });
|
|
189
|
+
}, Math.max(1, nextRunAt.getTime() - Date.now()));
|
|
190
|
+
this.timers.set(id, timer);
|
|
191
|
+
};
|
|
192
|
+
scheduleNext();
|
|
193
|
+
const scheduler = this;
|
|
194
|
+
return { id, cancel: () => scheduler.cancel(id), get nextRunAt() { return scheduler.nextRuns.get(id); } };
|
|
195
|
+
}
|
|
196
|
+
getNextRun(id) { const value = this.nextRuns.get(id); return value ? new Date(value) : undefined; }
|
|
197
|
+
cancel(id) { const timer = this.timers.get(id); if (!timer) {
|
|
198
|
+
this.nextRuns.delete(id);
|
|
199
|
+
return false;
|
|
200
|
+
} clearTimeout(timer); clearInterval(timer); this.timers.delete(id); this.nextRuns.delete(id); return true; }
|
|
78
201
|
clear() { for (const id of [...this.timers.keys()])
|
|
79
202
|
this.cancel(id); }
|
|
203
|
+
async run(id, task) {
|
|
204
|
+
try {
|
|
205
|
+
await task();
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
await this.options.onError?.(error, id);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
80
211
|
}
|
|
81
212
|
exports.Scheduler = Scheduler;
|
|
82
|
-
function parseSimpleCronInterval(expression) { const minute = expression.trim().split(/\s+/)[0]; if (minute?.startsWith("*/")) {
|
|
83
|
-
const value = Number(minute.slice(2));
|
|
84
|
-
if (Number.isInteger(value) && value > 0)
|
|
85
|
-
return value * 60_000;
|
|
86
|
-
} throw new Error("Only interval cron expressions such as */5 are supported by the built-in scheduler; use an adapter for full cron syntax."); }
|
|
@@ -2,25 +2,83 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Router = void 0;
|
|
4
4
|
const compose_js_1 = require("../middleware/compose.js");
|
|
5
|
+
function testRegExp(expression, value) {
|
|
6
|
+
expression.lastIndex = 0;
|
|
7
|
+
return expression.test(value);
|
|
8
|
+
}
|
|
5
9
|
class Router {
|
|
6
10
|
routes = [];
|
|
7
11
|
sequence = 0;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
12
|
+
matchMode;
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.matchMode = options.matchMode ?? "first";
|
|
15
|
+
}
|
|
16
|
+
use(...middleware) {
|
|
17
|
+
if (middleware.length === 0)
|
|
18
|
+
return this;
|
|
19
|
+
this.routes.push({ priority: -1_000_000 + this.sequence++, matcher: () => true, middleware });
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
route(matcher, ...middleware) {
|
|
23
|
+
if (middleware.length === 0)
|
|
24
|
+
throw new Error("A route requires at least one middleware handler.");
|
|
25
|
+
this.routes.push({ priority: this.sequence++, matcher, middleware });
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
command(name, ...middleware) {
|
|
29
|
+
return this.route(async (ctx) => {
|
|
30
|
+
const text = ctx.message?.text;
|
|
31
|
+
if (!text?.startsWith("/"))
|
|
32
|
+
return false;
|
|
33
|
+
const command = text.slice(1).split(/[\s@]/, 1)[0] ?? "";
|
|
34
|
+
return typeof name === "string"
|
|
35
|
+
? command === name.replace(/^\//, "")
|
|
36
|
+
: testRegExp(name, command);
|
|
37
|
+
}, ...middleware);
|
|
38
|
+
}
|
|
39
|
+
text(value, ...middleware) {
|
|
40
|
+
return this.route((ctx) => ctx.message?.text === value, ...middleware);
|
|
41
|
+
}
|
|
42
|
+
regex(expression, ...middleware) {
|
|
43
|
+
return this.route((ctx) => testRegExp(expression, ctx.message?.text ?? ""), ...middleware);
|
|
44
|
+
}
|
|
45
|
+
callback(pattern, ...middleware) {
|
|
46
|
+
return this.route((ctx) => {
|
|
47
|
+
const data = ctx.callbackQuery?.data ?? "";
|
|
48
|
+
return typeof pattern === "string"
|
|
49
|
+
? pattern.endsWith("*")
|
|
50
|
+
? data.startsWith(pattern.slice(0, -1))
|
|
51
|
+
: data === pattern
|
|
52
|
+
: testRegExp(pattern, data);
|
|
53
|
+
}, ...middleware);
|
|
54
|
+
}
|
|
55
|
+
chat(chatId, ...middleware) {
|
|
56
|
+
return this.route((ctx) => {
|
|
57
|
+
const id = ctx.message?.chat?.id;
|
|
58
|
+
return id !== undefined && (id === chatId || String(id) === String(chatId));
|
|
59
|
+
}, ...middleware);
|
|
60
|
+
}
|
|
61
|
+
predicate(matcher, ...middleware) {
|
|
62
|
+
return this.route(matcher, ...middleware);
|
|
63
|
+
}
|
|
64
|
+
nest(child) {
|
|
65
|
+
return this.route(() => true, async (ctx, next) => {
|
|
66
|
+
await child.handle(ctx, next);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async handle(ctx, terminal) {
|
|
70
|
+
const ordered = [...this.routes].sort((left, right) => left.priority - right.priority);
|
|
71
|
+
let matched = false;
|
|
72
|
+
for (const route of ordered) {
|
|
73
|
+
if (!(await route.matcher(ctx)))
|
|
74
|
+
continue;
|
|
75
|
+
matched = true;
|
|
76
|
+
await (0, compose_js_1.compose)(route.middleware)(ctx);
|
|
77
|
+
if (this.matchMode === "first")
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
if (!matched)
|
|
81
|
+
await terminal?.();
|
|
82
|
+
}
|
|
25
83
|
}
|
|
26
84
|
exports.Router = Router;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Wizard = exports.ConversationManager = exports.ConversationFlow = void 0;
|
|
4
|
+
const storage_js_1 = require("../storage/storage.js");
|
|
4
5
|
class ConversationFlow {
|
|
5
6
|
ctx;
|
|
6
7
|
state;
|
|
@@ -19,19 +20,86 @@ class ConversationFlow {
|
|
|
19
20
|
exports.ConversationFlow = ConversationFlow;
|
|
20
21
|
class ConversationManager {
|
|
21
22
|
active = new Map();
|
|
22
|
-
|
|
23
|
+
storage;
|
|
24
|
+
constructor(storage = new storage_js_1.MemoryStorage()) {
|
|
25
|
+
this.storage = storage;
|
|
26
|
+
}
|
|
27
|
+
start(key, name, values = {}) {
|
|
28
|
+
const state = { name, step: 0, values, status: "active", updatedAt: Date.now() };
|
|
29
|
+
this.active.set(key, state);
|
|
30
|
+
void this.storage.set(key, state);
|
|
31
|
+
return state;
|
|
32
|
+
}
|
|
23
33
|
get(key) { return this.active.get(key); }
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
async getAsync(key) {
|
|
35
|
+
const cached = this.active.get(key);
|
|
36
|
+
if (cached)
|
|
37
|
+
return cached;
|
|
38
|
+
const stored = await this.storage.get(key);
|
|
39
|
+
if (stored)
|
|
40
|
+
this.active.set(key, stored);
|
|
41
|
+
return stored;
|
|
42
|
+
}
|
|
43
|
+
cancel(key) {
|
|
44
|
+
const state = this.active.get(key);
|
|
45
|
+
if (!state)
|
|
46
|
+
return false;
|
|
47
|
+
state.status = "cancelled";
|
|
48
|
+
state.updatedAt = Date.now();
|
|
49
|
+
void this.storage.set(key, state);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
async cancelAsync(key) {
|
|
53
|
+
const state = await this.getAsync(key);
|
|
54
|
+
if (!state)
|
|
55
|
+
return false;
|
|
56
|
+
state.status = "cancelled";
|
|
57
|
+
state.updatedAt = Date.now();
|
|
58
|
+
await this.storage.set(key, state);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
clearExpired(maxAgeMs) {
|
|
62
|
+
if (!Number.isFinite(maxAgeMs) || maxAgeMs < 0)
|
|
63
|
+
throw new RangeError("maxAgeMs must be non-negative");
|
|
64
|
+
const threshold = Date.now() - maxAgeMs;
|
|
65
|
+
let removed = 0;
|
|
66
|
+
for (const [key, state] of this.active)
|
|
67
|
+
if (state.updatedAt < threshold) {
|
|
68
|
+
this.active.delete(key);
|
|
69
|
+
void this.storage.delete(key);
|
|
70
|
+
removed += 1;
|
|
71
|
+
}
|
|
72
|
+
return removed;
|
|
73
|
+
}
|
|
74
|
+
async clearExpiredAsync(maxAgeMs) {
|
|
75
|
+
if (!Number.isFinite(maxAgeMs) || maxAgeMs < 0)
|
|
76
|
+
throw new RangeError("maxAgeMs must be non-negative");
|
|
77
|
+
const threshold = Date.now() - maxAgeMs;
|
|
78
|
+
let removed = 0;
|
|
79
|
+
for await (const [key, state] of this.storage.entries()) {
|
|
80
|
+
if (state.updatedAt < threshold) {
|
|
81
|
+
await this.storage.delete(key);
|
|
82
|
+
this.active.delete(key);
|
|
83
|
+
removed += 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return removed;
|
|
87
|
+
}
|
|
88
|
+
async run(ctx, key, name, steps) {
|
|
89
|
+
const state = (await this.getAsync(key)) ?? this.start(key, name);
|
|
90
|
+
if (state.name !== name)
|
|
91
|
+
throw new Error(`Conversation ${key} belongs to ${state.name}, not ${name}`);
|
|
92
|
+
const flow = new ConversationFlow(ctx, state);
|
|
93
|
+
const step = steps[state.step];
|
|
94
|
+
if (!step)
|
|
95
|
+
state.status = "completed";
|
|
96
|
+
else
|
|
97
|
+
await step(flow);
|
|
98
|
+
state.updatedAt = Date.now();
|
|
99
|
+
await this.storage.set(key, state);
|
|
100
|
+
this.active.set(key, state);
|
|
33
101
|
return state;
|
|
34
|
-
}
|
|
102
|
+
}
|
|
35
103
|
}
|
|
36
104
|
exports.ConversationManager = ConversationManager;
|
|
37
105
|
class Wizard {
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Menu = void 0;
|
|
3
|
+
exports.MenuController = exports.Menu = void 0;
|
|
4
4
|
exports.paginate = paginate;
|
|
5
5
|
exports.paginationButtons = paginationButtons;
|
|
6
6
|
const index_js_1 = require("../keyboard/index.js");
|
|
7
|
-
function paginate(items, page, pageSize) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
function paginate(items, page, pageSize) {
|
|
8
|
+
if (!Number.isInteger(page) || page < 0)
|
|
9
|
+
throw new RangeError("page must be a non-negative integer");
|
|
10
|
+
if (!Number.isInteger(pageSize) || pageSize < 1)
|
|
11
|
+
throw new RangeError("pageSize must be a positive integer");
|
|
12
|
+
const pageCount = Math.max(1, Math.ceil(items.length / pageSize));
|
|
13
|
+
const safePage = Math.min(page, pageCount - 1);
|
|
14
|
+
const start = safePage * pageSize;
|
|
15
|
+
return { items: items.slice(start, start + pageSize), page: safePage, pageCount, hasPrevious: safePage > 0, hasNext: safePage < pageCount - 1 };
|
|
16
|
+
}
|
|
17
|
+
function paginationButtons(page, prefix, labels = {}) {
|
|
18
|
+
const buttons = [];
|
|
19
|
+
if (page.hasPrevious)
|
|
20
|
+
buttons.push({ text: labels.previous ?? "Previous", callback_data: `${prefix}:page:${page.page - 1}` });
|
|
21
|
+
buttons.push({ text: `${page.page + 1}/${page.pageCount}`, callback_data: `${prefix}:noop` });
|
|
22
|
+
if (page.hasNext)
|
|
23
|
+
buttons.push({ text: labels.next ?? "Next", callback_data: `${prefix}:page:${page.page + 1}` });
|
|
24
|
+
return buttons;
|
|
25
|
+
}
|
|
13
26
|
class Menu {
|
|
14
27
|
id;
|
|
15
28
|
itemsList = [];
|
|
@@ -17,17 +30,104 @@ class Menu {
|
|
|
17
30
|
constructor(id) {
|
|
18
31
|
this.id = id;
|
|
19
32
|
}
|
|
20
|
-
item(item) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
item(item) {
|
|
34
|
+
if (!item.id || !item.label)
|
|
35
|
+
throw new Error("Menu item id and label are required.");
|
|
36
|
+
if (item.url !== undefined && item.callbackData !== undefined)
|
|
37
|
+
throw new Error("A menu item cannot have both url and callbackData.");
|
|
38
|
+
this.itemsList.push(item);
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
breadcrumb(label) { if (label)
|
|
42
|
+
this.parents.push(label); return this; }
|
|
43
|
+
get items() { return this.itemsList; }
|
|
31
44
|
get breadcrumbs() { return this.parents; }
|
|
45
|
+
async visibleItems(context = {}) {
|
|
46
|
+
const permissions = context.permissions ? new Set(context.permissions) : undefined;
|
|
47
|
+
const result = [];
|
|
48
|
+
for (const item of this.itemsList) {
|
|
49
|
+
const visible = typeof item.visible === "function" ? await item.visible(context, item) : item.visible ?? true;
|
|
50
|
+
if (!visible)
|
|
51
|
+
continue;
|
|
52
|
+
const allowed = typeof item.permission === "function"
|
|
53
|
+
? await item.permission(context, item)
|
|
54
|
+
: item.permission === undefined
|
|
55
|
+
? true
|
|
56
|
+
: permissions?.has(item.permission) ?? false;
|
|
57
|
+
if (allowed)
|
|
58
|
+
result.push(item);
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
async build(context = {}, options = {}) {
|
|
63
|
+
const columns = options.columns ?? 1;
|
|
64
|
+
if (!Number.isInteger(columns) || columns < 1)
|
|
65
|
+
throw new RangeError("columns must be a positive integer");
|
|
66
|
+
const keyboard = new index_js_1.InlineKeyboard();
|
|
67
|
+
if (options.includeBreadcrumbs && this.parents.length)
|
|
68
|
+
keyboard.text(this.parents.join(" / "), `${this.id}:breadcrumb`);
|
|
69
|
+
const buttons = (await this.visibleItems(context)).map((item) => item.url
|
|
70
|
+
? { text: item.label, url: item.url }
|
|
71
|
+
: { text: item.label, callback_data: item.callbackData ?? `${this.id}:${item.id}` });
|
|
72
|
+
keyboard.grid(buttons, columns);
|
|
73
|
+
return keyboard;
|
|
74
|
+
}
|
|
32
75
|
}
|
|
33
76
|
exports.Menu = Menu;
|
|
77
|
+
class MenuController {
|
|
78
|
+
currentPage = 0;
|
|
79
|
+
options;
|
|
80
|
+
constructor(options) {
|
|
81
|
+
if (!options.id || !options.id.trim())
|
|
82
|
+
throw new Error("MenuController id is required.");
|
|
83
|
+
if (!Number.isInteger(options.pageSize) || options.pageSize < 1)
|
|
84
|
+
throw new RangeError("pageSize must be a positive integer");
|
|
85
|
+
this.options = options;
|
|
86
|
+
}
|
|
87
|
+
get page() { return this.currentPage; }
|
|
88
|
+
async allItems() { return typeof this.options.items === "function" ? await this.options.items() : this.options.items; }
|
|
89
|
+
async render(context = this.options.context ?? {}) {
|
|
90
|
+
const items = await this.allItems();
|
|
91
|
+
const page = paginate(items, this.currentPage, this.options.pageSize);
|
|
92
|
+
this.currentPage = page.page;
|
|
93
|
+
const keyboard = new index_js_1.InlineKeyboard();
|
|
94
|
+
for (let index = 0; index < page.items.length; index += 1) {
|
|
95
|
+
const absoluteIndex = page.page * this.options.pageSize + index;
|
|
96
|
+
keyboard.text(this.options.label(page.items[index], absoluteIndex), `${this.options.id}:select:${absoluteIndex}`);
|
|
97
|
+
}
|
|
98
|
+
const navigation = paginationButtons(page, this.options.id, this.options.labels);
|
|
99
|
+
if (navigation.length)
|
|
100
|
+
keyboard.row(...navigation);
|
|
101
|
+
void context;
|
|
102
|
+
return keyboard.build();
|
|
103
|
+
}
|
|
104
|
+
async handle(data, context = this.options.context ?? {}) {
|
|
105
|
+
const prefix = `${this.options.id}:`;
|
|
106
|
+
if (!data.startsWith(prefix))
|
|
107
|
+
return undefined;
|
|
108
|
+
const action = data.slice(prefix.length);
|
|
109
|
+
if (action === "noop" || action === "breadcrumb")
|
|
110
|
+
return { type: "noop" };
|
|
111
|
+
if (action.startsWith("page:")) {
|
|
112
|
+
const page = Number(action.slice("page:".length));
|
|
113
|
+
if (!Number.isInteger(page) || page < 0)
|
|
114
|
+
return undefined;
|
|
115
|
+
this.currentPage = page;
|
|
116
|
+
const items = await this.allItems();
|
|
117
|
+
const pageData = paginate(items, this.currentPage, this.options.pageSize);
|
|
118
|
+
this.currentPage = pageData.page;
|
|
119
|
+
return { type: "page", page: pageData, keyboard: await this.render(context) };
|
|
120
|
+
}
|
|
121
|
+
if (action.startsWith("select:")) {
|
|
122
|
+
const index = Number(action.slice("select:".length));
|
|
123
|
+
const items = await this.allItems();
|
|
124
|
+
const item = items[index];
|
|
125
|
+
if (item === undefined || !Number.isInteger(index) || index < 0)
|
|
126
|
+
return undefined;
|
|
127
|
+
await this.options.callback?.(item, context);
|
|
128
|
+
return { type: "select", item, index };
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
exports.MenuController = MenuController;
|