@riseonly/sdk 0.1.1-next.0 → 0.1.2
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.md +46 -9
- package/dist/index.cjs +314 -79
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -34
- package/dist/index.d.ts +56 -34
- package/dist/index.js +314 -81
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createServer } from 'http';
|
|
2
|
-
import {
|
|
2
|
+
import { timingSafeEqual, createHmac } from 'crypto';
|
|
3
3
|
|
|
4
4
|
// src/bot.ts
|
|
5
5
|
|
|
@@ -92,6 +92,22 @@ var RiseonlyTimeoutError = class extends RiseonlyNetworkError {
|
|
|
92
92
|
this.name = "RiseonlyTimeoutError";
|
|
93
93
|
}
|
|
94
94
|
};
|
|
95
|
+
var RiseonlyAbortError = class extends RiseonlyNetworkError {
|
|
96
|
+
constructor(message = "Request was aborted", cause) {
|
|
97
|
+
super(message, cause, 499);
|
|
98
|
+
this.name = "RiseonlyAbortError";
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
var RiseonlyResponseError = class extends RiseonlyNetworkError {
|
|
102
|
+
status;
|
|
103
|
+
body;
|
|
104
|
+
constructor(message, status, body) {
|
|
105
|
+
super(message, void 0, status);
|
|
106
|
+
this.name = "RiseonlyResponseError";
|
|
107
|
+
this.status = status;
|
|
108
|
+
this.body = body;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
95
111
|
|
|
96
112
|
// src/client.ts
|
|
97
113
|
var ApiClient = class {
|
|
@@ -106,8 +122,15 @@ var ApiClient = class {
|
|
|
106
122
|
}
|
|
107
123
|
this.token = token.trim();
|
|
108
124
|
this.baseUrl = (options.baseUrl ?? DEFAULT_API_BASE).replace(/\/$/, "");
|
|
109
|
-
|
|
125
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
126
|
+
if (typeof fetchImpl !== "function") {
|
|
127
|
+
throw new Error("global fetch is unavailable; provide ClientOptions.fetch");
|
|
128
|
+
}
|
|
129
|
+
this.fetchImpl = options.fetch ? fetchImpl : fetchImpl.bind(globalThis);
|
|
110
130
|
this.requestTimeoutMs = options.requestTimeoutMs ?? 6e4;
|
|
131
|
+
if (!Number.isFinite(this.requestTimeoutMs) || this.requestTimeoutMs <= 0) {
|
|
132
|
+
throw new Error("requestTimeoutMs must be a positive number");
|
|
133
|
+
}
|
|
111
134
|
this.useCompatibleEndpoint = options.useCompatibleEndpoint ?? false;
|
|
112
135
|
}
|
|
113
136
|
/**
|
|
@@ -115,15 +138,21 @@ var ApiClient = class {
|
|
|
115
138
|
*/
|
|
116
139
|
async getCapabilities(signal) {
|
|
117
140
|
const url = `${this.baseUrl}${CANONICAL_API_PREFIX}/capabilities`;
|
|
118
|
-
const response = await this.
|
|
141
|
+
const { response, body } = await this.fetchJson(url, {
|
|
119
142
|
method: "GET",
|
|
120
|
-
headers: { Accept: "application/json" }
|
|
121
|
-
|
|
122
|
-
});
|
|
143
|
+
headers: { Accept: "application/json" }
|
|
144
|
+
}, { signal });
|
|
123
145
|
if (!response.ok) {
|
|
124
|
-
throw new
|
|
146
|
+
throw new RiseonlyResponseError(
|
|
147
|
+
`capabilities request failed with status ${response.status}`,
|
|
148
|
+
response.status,
|
|
149
|
+
serializeResponseBody(body)
|
|
150
|
+
);
|
|
125
151
|
}
|
|
126
|
-
|
|
152
|
+
if (!isRecord(body) || !Array.isArray(body.methods)) {
|
|
153
|
+
throw new RiseonlyResponseError("capabilities response is invalid", response.status);
|
|
154
|
+
}
|
|
155
|
+
return body;
|
|
127
156
|
}
|
|
128
157
|
/**
|
|
129
158
|
* Calls a Bot API method and returns the typed result.
|
|
@@ -145,37 +174,36 @@ var ApiClient = class {
|
|
|
145
174
|
if (options.requestId) {
|
|
146
175
|
headers[IDEMPOTENCY_HEADER] = options.requestId;
|
|
147
176
|
}
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
let response;
|
|
152
|
-
try {
|
|
153
|
-
response = await this.fetchImpl(url, {
|
|
177
|
+
const { response, body } = await this.fetchJson(
|
|
178
|
+
url,
|
|
179
|
+
{
|
|
154
180
|
method: "POST",
|
|
155
181
|
headers,
|
|
156
|
-
body: JSON.stringify(payload)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
throw new RiseonlyTimeoutError();
|
|
162
|
-
}
|
|
163
|
-
throw new RiseonlyNetworkError("network request failed", error);
|
|
164
|
-
} finally {
|
|
165
|
-
clearTimeout(timeout);
|
|
166
|
-
}
|
|
167
|
-
const body = await response.json();
|
|
168
|
-
if (!body.ok) {
|
|
182
|
+
body: JSON.stringify(payload)
|
|
183
|
+
},
|
|
184
|
+
options
|
|
185
|
+
);
|
|
186
|
+
if (isApiErrorResponse(body)) {
|
|
169
187
|
throw RiseonlyError.fromApiResponse(body);
|
|
170
188
|
}
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
throw new RiseonlyResponseError(
|
|
191
|
+
`Bot API request failed with status ${response.status}`,
|
|
192
|
+
response.status,
|
|
193
|
+
serializeResponseBody(body)
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (!isRecord(body) || body.ok !== true || !("result" in body)) {
|
|
197
|
+
throw new RiseonlyResponseError("Bot API response envelope is invalid", response.status);
|
|
198
|
+
}
|
|
171
199
|
return body;
|
|
172
200
|
}
|
|
173
201
|
async getMe(options) {
|
|
174
202
|
return this.callMethod("getMe", {}, options);
|
|
175
203
|
}
|
|
176
204
|
async sendMessage(options) {
|
|
177
|
-
const {
|
|
178
|
-
return this.callMethod("sendMessage", payload,
|
|
205
|
+
const { request, payload } = splitRequestOptions(options);
|
|
206
|
+
return this.callMethod("sendMessage", payload, request);
|
|
179
207
|
}
|
|
180
208
|
async sendPhoto(photo, options) {
|
|
181
209
|
return this.sendMedia("sendPhoto", "photo", photo, options);
|
|
@@ -196,72 +224,75 @@ var ApiClient = class {
|
|
|
196
224
|
return this.sendMedia("sendAnimation", "animation", animation, options);
|
|
197
225
|
}
|
|
198
226
|
async editMessageText(options) {
|
|
199
|
-
const {
|
|
200
|
-
return this.callMethod("editMessageText", payload,
|
|
227
|
+
const { request, payload } = splitRequestOptions(options);
|
|
228
|
+
return this.callMethod("editMessageText", payload, request);
|
|
201
229
|
}
|
|
202
230
|
async deleteMessage(options) {
|
|
203
|
-
const {
|
|
204
|
-
return this.callMethod("deleteMessage", payload,
|
|
231
|
+
const { request, payload } = splitRequestOptions(options);
|
|
232
|
+
return this.callMethod("deleteMessage", payload, request);
|
|
205
233
|
}
|
|
206
234
|
async sendChatAction(options) {
|
|
207
|
-
const {
|
|
208
|
-
return this.callMethod("sendChatAction", payload,
|
|
235
|
+
const { request, payload } = splitRequestOptions(options);
|
|
236
|
+
return this.callMethod("sendChatAction", payload, request);
|
|
209
237
|
}
|
|
210
238
|
async getUpdates(options = {}) {
|
|
211
|
-
const {
|
|
212
|
-
|
|
239
|
+
const { request, payload } = splitRequestOptions(options);
|
|
240
|
+
const serverTimeoutMs = (options.timeout ?? 0) * 1e3 + 5e3;
|
|
241
|
+
request.timeoutMs ??= Math.max(this.requestTimeoutMs, serverTimeoutMs);
|
|
242
|
+
return this.callMethod("getUpdates", payload, request);
|
|
213
243
|
}
|
|
214
244
|
async setWebhook(options) {
|
|
215
|
-
const {
|
|
216
|
-
return this.callMethod("setWebhook", payload,
|
|
245
|
+
const { request, payload } = splitRequestOptions(options);
|
|
246
|
+
return this.callMethod("setWebhook", payload, request);
|
|
217
247
|
}
|
|
218
248
|
async deleteWebhook(options = {}) {
|
|
219
|
-
const {
|
|
220
|
-
return this.callMethod("deleteWebhook", payload,
|
|
249
|
+
const { request, payload } = splitRequestOptions(options);
|
|
250
|
+
return this.callMethod("deleteWebhook", payload, request);
|
|
221
251
|
}
|
|
222
252
|
async getWebhookInfo(options) {
|
|
223
253
|
return this.callMethod("getWebhookInfo", {}, options);
|
|
224
254
|
}
|
|
225
255
|
async setMyCommands(options) {
|
|
226
|
-
const {
|
|
227
|
-
return this.callMethod("setMyCommands", payload,
|
|
256
|
+
const { request, payload } = splitRequestOptions(options);
|
|
257
|
+
return this.callMethod("setMyCommands", payload, request);
|
|
228
258
|
}
|
|
229
259
|
async getMyCommands(options = {}) {
|
|
230
|
-
const {
|
|
231
|
-
return this.callMethod("getMyCommands", payload,
|
|
260
|
+
const { request, payload } = splitRequestOptions(options);
|
|
261
|
+
return this.callMethod("getMyCommands", payload, request);
|
|
232
262
|
}
|
|
233
263
|
async deleteMyCommands(options = {}) {
|
|
234
|
-
const {
|
|
235
|
-
return this.callMethod("deleteMyCommands", payload,
|
|
264
|
+
const { request, payload } = splitRequestOptions(options);
|
|
265
|
+
return this.callMethod("deleteMyCommands", payload, request);
|
|
236
266
|
}
|
|
237
267
|
async answerCallbackQuery(options) {
|
|
238
|
-
const {
|
|
239
|
-
return this.callMethod("answerCallbackQuery", payload,
|
|
268
|
+
const { request, payload } = splitRequestOptions(options);
|
|
269
|
+
return this.callMethod("answerCallbackQuery", payload, request);
|
|
240
270
|
}
|
|
241
271
|
async getChat(options) {
|
|
242
|
-
const {
|
|
243
|
-
return this.callMethod("getChat", payload,
|
|
272
|
+
const { request, payload } = splitRequestOptions(options);
|
|
273
|
+
return this.callMethod("getChat", payload, request);
|
|
244
274
|
}
|
|
245
275
|
async sendMedia(method, field, media, options) {
|
|
246
|
-
const {
|
|
276
|
+
const { request, payload } = splitRequestOptions(options);
|
|
247
277
|
return this.callMethod(
|
|
248
278
|
method,
|
|
249
279
|
{
|
|
250
280
|
...payload,
|
|
251
281
|
[field]: this.normalizeMedia(media)
|
|
252
282
|
},
|
|
253
|
-
|
|
283
|
+
request
|
|
254
284
|
);
|
|
255
285
|
}
|
|
256
286
|
normalizeMedia(media) {
|
|
257
287
|
if (typeof media === "string") {
|
|
258
288
|
return media;
|
|
259
289
|
}
|
|
260
|
-
if (media.file_id) {
|
|
261
|
-
return
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
290
|
+
if (media.file_id || media.url) {
|
|
291
|
+
return Object.fromEntries(
|
|
292
|
+
Object.entries(media).filter(
|
|
293
|
+
(entry) => typeof entry[1] === "string" || typeof entry[1] === "number"
|
|
294
|
+
)
|
|
295
|
+
);
|
|
265
296
|
}
|
|
266
297
|
throw new Error("media requires file_id or url");
|
|
267
298
|
}
|
|
@@ -271,9 +302,77 @@ var ApiClient = class {
|
|
|
271
302
|
}
|
|
272
303
|
return `${this.baseUrl}${CANONICAL_API_PREFIX}/${method}`;
|
|
273
304
|
}
|
|
305
|
+
async fetchJson(url, init, options = {}) {
|
|
306
|
+
const controller = new AbortController();
|
|
307
|
+
let timedOut = false;
|
|
308
|
+
const timeoutMs = options.timeoutMs ?? this.requestTimeoutMs;
|
|
309
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
310
|
+
throw new Error("timeoutMs must be a positive number");
|
|
311
|
+
}
|
|
312
|
+
const abortFromCaller = () => controller.abort(options.signal?.reason);
|
|
313
|
+
if (options.signal?.aborted) {
|
|
314
|
+
throw new RiseonlyAbortError("Request was aborted", options.signal.reason);
|
|
315
|
+
}
|
|
316
|
+
options.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
317
|
+
const timeout = setTimeout(() => {
|
|
318
|
+
timedOut = true;
|
|
319
|
+
controller.abort();
|
|
320
|
+
}, timeoutMs);
|
|
321
|
+
let response;
|
|
322
|
+
try {
|
|
323
|
+
response = await this.fetchImpl(url, { ...init, signal: controller.signal });
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (controller.signal.aborted) {
|
|
326
|
+
if (timedOut) {
|
|
327
|
+
throw new RiseonlyTimeoutError();
|
|
328
|
+
}
|
|
329
|
+
throw new RiseonlyAbortError("Request was aborted", error);
|
|
330
|
+
}
|
|
331
|
+
throw new RiseonlyNetworkError("network request failed", error);
|
|
332
|
+
} finally {
|
|
333
|
+
clearTimeout(timeout);
|
|
334
|
+
options.signal?.removeEventListener("abort", abortFromCaller);
|
|
335
|
+
}
|
|
336
|
+
let text;
|
|
337
|
+
try {
|
|
338
|
+
text = await response.text();
|
|
339
|
+
} catch (error) {
|
|
340
|
+
throw new RiseonlyNetworkError("unable to read response body", error, response.status);
|
|
341
|
+
}
|
|
342
|
+
if (!text) {
|
|
343
|
+
throw new RiseonlyResponseError("Bot API returned an empty response", response.status);
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
return { response, body: JSON.parse(text) };
|
|
347
|
+
} catch {
|
|
348
|
+
throw new RiseonlyResponseError(
|
|
349
|
+
"Bot API returned a non-JSON response",
|
|
350
|
+
response.status,
|
|
351
|
+
text.slice(0, 4096)
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
274
355
|
};
|
|
275
|
-
|
|
276
|
-
|
|
356
|
+
function splitRequestOptions(options) {
|
|
357
|
+
const { requestId, signal, timeoutMs, ...payload } = options;
|
|
358
|
+
return {
|
|
359
|
+
request: { requestId, signal, timeoutMs },
|
|
360
|
+
payload
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function isRecord(value) {
|
|
364
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
365
|
+
}
|
|
366
|
+
function isApiErrorResponse(value) {
|
|
367
|
+
return isRecord(value) && value.ok === false && typeof value.error_code === "number" && typeof value.description === "string";
|
|
368
|
+
}
|
|
369
|
+
function serializeResponseBody(body) {
|
|
370
|
+
try {
|
|
371
|
+
return JSON.stringify(body).slice(0, 4096);
|
|
372
|
+
} catch {
|
|
373
|
+
return void 0;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
277
376
|
function parseWebhookUpdate(body) {
|
|
278
377
|
if (!body || typeof body !== "object") {
|
|
279
378
|
throw new Error("webhook body must be an object");
|
|
@@ -288,28 +387,49 @@ function verifyWebhookSecret(headers, secretToken) {
|
|
|
288
387
|
if (!secretToken) {
|
|
289
388
|
return true;
|
|
290
389
|
}
|
|
291
|
-
const
|
|
390
|
+
const headerEntry = Object.entries(headers).find(
|
|
391
|
+
([name]) => name.toLowerCase() === WEBHOOK_SECRET_HEADER
|
|
392
|
+
);
|
|
393
|
+
const headerValue = headerEntry?.[1];
|
|
292
394
|
if (!headerValue) {
|
|
293
395
|
return false;
|
|
294
396
|
}
|
|
295
397
|
const received = Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
296
|
-
|
|
398
|
+
if (typeof received !== "string") {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
const actual = Buffer.from(received);
|
|
402
|
+
const expected = Buffer.from(secretToken);
|
|
403
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
297
404
|
}
|
|
298
405
|
function createWebhookHandler(options) {
|
|
299
406
|
return async (request) => {
|
|
407
|
+
if (!verifyWebhookSecret(request.headers, options.secretToken)) {
|
|
408
|
+
return { status: 401, body: { ok: false, description: "invalid webhook secret" } };
|
|
409
|
+
}
|
|
410
|
+
let update;
|
|
411
|
+
try {
|
|
412
|
+
update = parseWebhookUpdate(request.body);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
await notifyWebhookError(options.onError, error);
|
|
415
|
+
return { status: 400, body: { ok: false, description: "invalid webhook payload" } };
|
|
416
|
+
}
|
|
300
417
|
try {
|
|
301
|
-
if (!verifyWebhookSecret(request.headers, options.secretToken)) {
|
|
302
|
-
return { status: 401, body: { ok: false, description: "invalid webhook secret" } };
|
|
303
|
-
}
|
|
304
|
-
const update = parseWebhookUpdate(request.body);
|
|
305
418
|
await options.onUpdate(update);
|
|
306
419
|
return { status: 200, body: { ok: true } };
|
|
307
420
|
} catch (error) {
|
|
308
|
-
await options.onError
|
|
309
|
-
return { status:
|
|
421
|
+
await notifyWebhookError(options.onError, error);
|
|
422
|
+
return { status: 500, body: { ok: false, description: "webhook handler failed" } };
|
|
310
423
|
}
|
|
311
424
|
};
|
|
312
425
|
}
|
|
426
|
+
async function notifyWebhookError(handler, error) {
|
|
427
|
+
try {
|
|
428
|
+
await handler?.(error);
|
|
429
|
+
} catch {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
313
433
|
|
|
314
434
|
// src/bot.ts
|
|
315
435
|
var RiseonlyBot = class extends ApiClient {
|
|
@@ -325,6 +445,7 @@ var RiseonlyBot = class extends ApiClient {
|
|
|
325
445
|
webhookOptions;
|
|
326
446
|
polling = false;
|
|
327
447
|
pollingAbort;
|
|
448
|
+
pollingTask;
|
|
328
449
|
offset = 0;
|
|
329
450
|
webhookServer;
|
|
330
451
|
constructor(token, options = {}) {
|
|
@@ -366,6 +487,67 @@ var RiseonlyBot = class extends ApiClient {
|
|
|
366
487
|
});
|
|
367
488
|
return this.on(event, wrapper);
|
|
368
489
|
}
|
|
490
|
+
command(commands, handler) {
|
|
491
|
+
const accepted = new Set(
|
|
492
|
+
(Array.isArray(commands) ? commands : [commands]).map((command) => command.replace(/^\//, "").toLowerCase()).filter(Boolean)
|
|
493
|
+
);
|
|
494
|
+
return this.on("message", async (message, update) => {
|
|
495
|
+
const text = message.text?.trim();
|
|
496
|
+
if (!text?.startsWith("/")) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const token = text.split(/\s+/, 1)[0].slice(1).split("@", 1)[0].toLowerCase();
|
|
500
|
+
if (accepted.has(token)) {
|
|
501
|
+
await handler(message, update);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
hears(trigger, handler) {
|
|
506
|
+
return this.on("message", async (message, update) => {
|
|
507
|
+
if (message.text === void 0) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (typeof trigger === "string") {
|
|
511
|
+
if (message.text === trigger) {
|
|
512
|
+
await handler(message, null, update);
|
|
513
|
+
}
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
trigger.lastIndex = 0;
|
|
517
|
+
const match = trigger.exec(message.text);
|
|
518
|
+
if (match) {
|
|
519
|
+
await handler(message, match, update);
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
action(trigger, handler) {
|
|
524
|
+
return this.on("callback_query", async (query, update) => {
|
|
525
|
+
if (query.data === void 0) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const matches = typeof trigger === "string" ? query.data === trigger : testPattern(trigger, query.data);
|
|
529
|
+
if (matches) {
|
|
530
|
+
await handler(query, update);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
catch(handler) {
|
|
535
|
+
return this.on("error", handler);
|
|
536
|
+
}
|
|
537
|
+
async setup(options) {
|
|
538
|
+
if (options.commands) {
|
|
539
|
+
await this.setMyCommands({
|
|
540
|
+
commands: options.commands,
|
|
541
|
+
scope: options.commandScope,
|
|
542
|
+
language_code: options.languageCode
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
if (options.webhook === false) {
|
|
546
|
+
await this.deleteWebhook();
|
|
547
|
+
} else if (options.webhook) {
|
|
548
|
+
await this.setWebhook(options.webhook);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
369
551
|
/**
|
|
370
552
|
* Starts long polling for bot updates.
|
|
371
553
|
*/
|
|
@@ -376,7 +558,7 @@ var RiseonlyBot = class extends ApiClient {
|
|
|
376
558
|
this.polling = true;
|
|
377
559
|
this.pollingOptions = { ...this.pollingOptions, ...options, autoStart: true };
|
|
378
560
|
this.pollingAbort = new AbortController();
|
|
379
|
-
|
|
561
|
+
this.pollingTask = this.pollLoop();
|
|
380
562
|
}
|
|
381
563
|
/**
|
|
382
564
|
* Stops long polling.
|
|
@@ -384,7 +566,15 @@ var RiseonlyBot = class extends ApiClient {
|
|
|
384
566
|
async stopPolling() {
|
|
385
567
|
this.polling = false;
|
|
386
568
|
this.pollingAbort?.abort();
|
|
569
|
+
await this.pollingTask;
|
|
387
570
|
this.pollingAbort = void 0;
|
|
571
|
+
this.pollingTask = void 0;
|
|
572
|
+
}
|
|
573
|
+
async launch(options = {}) {
|
|
574
|
+
await this.startPolling(options);
|
|
575
|
+
}
|
|
576
|
+
async stop() {
|
|
577
|
+
await Promise.all([this.stopPolling(), this.stopWebhook()]);
|
|
388
578
|
}
|
|
389
579
|
/**
|
|
390
580
|
* Starts a built-in HTTP server that receives webhook updates.
|
|
@@ -395,6 +585,12 @@ var RiseonlyBot = class extends ApiClient {
|
|
|
395
585
|
const path = config.path ?? "/";
|
|
396
586
|
const host = config.host ?? "0.0.0.0";
|
|
397
587
|
const port = config.port ?? 3e3;
|
|
588
|
+
if (!path.startsWith("/")) {
|
|
589
|
+
throw new Error("webhook path must start with /");
|
|
590
|
+
}
|
|
591
|
+
if (config.maxBodyBytes !== void 0 && config.maxBodyBytes <= 0) {
|
|
592
|
+
throw new Error("maxBodyBytes must be positive");
|
|
593
|
+
}
|
|
398
594
|
const handler = createWebhookHandler({
|
|
399
595
|
secretToken: config.secretToken,
|
|
400
596
|
onUpdate: (update) => this.processUpdate(update),
|
|
@@ -409,15 +605,16 @@ var RiseonlyBot = class extends ApiClient {
|
|
|
409
605
|
return;
|
|
410
606
|
}
|
|
411
607
|
try {
|
|
412
|
-
const body = await readJsonBody(req);
|
|
608
|
+
const body = await readJsonBody(req, config.maxBodyBytes);
|
|
413
609
|
const result = await handler({
|
|
414
610
|
headers: normalizeHeaders(req.headers),
|
|
415
611
|
body
|
|
416
612
|
});
|
|
417
613
|
respond(res, result.status, result.body);
|
|
418
614
|
} catch (error) {
|
|
419
|
-
await this.
|
|
420
|
-
|
|
615
|
+
await this.emitError(error);
|
|
616
|
+
const status = error instanceof WebhookPayloadTooLargeError ? 413 : 500;
|
|
617
|
+
respond(res, status, { ok: false });
|
|
421
618
|
}
|
|
422
619
|
});
|
|
423
620
|
await new Promise((resolve, reject) => {
|
|
@@ -489,8 +686,8 @@ var RiseonlyBot = class extends ApiClient {
|
|
|
489
686
|
if (update.update_id < this.offset) {
|
|
490
687
|
continue;
|
|
491
688
|
}
|
|
492
|
-
this.offset = Math.max(this.offset, update.update_id + 1);
|
|
493
689
|
await this.processUpdate(update);
|
|
690
|
+
this.offset = Math.max(this.offset, update.update_id + 1);
|
|
494
691
|
processed += 1;
|
|
495
692
|
}
|
|
496
693
|
if (processed === 0) {
|
|
@@ -500,7 +697,10 @@ var RiseonlyBot = class extends ApiClient {
|
|
|
500
697
|
if (!this.polling || isAbortError(error)) {
|
|
501
698
|
return;
|
|
502
699
|
}
|
|
503
|
-
|
|
700
|
+
try {
|
|
701
|
+
await this.emit("polling_error", error);
|
|
702
|
+
} catch {
|
|
703
|
+
}
|
|
504
704
|
if (error instanceof RiseonlyError && error.retryAfter) {
|
|
505
705
|
await sleep(error.retryAfter * 1e3);
|
|
506
706
|
} else {
|
|
@@ -510,11 +710,27 @@ var RiseonlyBot = class extends ApiClient {
|
|
|
510
710
|
}
|
|
511
711
|
}
|
|
512
712
|
async emit(event, ...args) {
|
|
713
|
+
let firstError;
|
|
513
714
|
for (const listener of [...this.listeners[event]]) {
|
|
514
715
|
try {
|
|
515
716
|
await listener(...args);
|
|
516
717
|
} catch (error) {
|
|
517
|
-
|
|
718
|
+
firstError ??= error;
|
|
719
|
+
if (event !== "error") {
|
|
720
|
+
await this.emitError(error);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (firstError !== void 0) {
|
|
725
|
+
throw firstError;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async emitError(error) {
|
|
729
|
+
for (const listener of [...this.listeners.error]) {
|
|
730
|
+
try {
|
|
731
|
+
await listener(error);
|
|
732
|
+
} catch {
|
|
733
|
+
continue;
|
|
518
734
|
}
|
|
519
735
|
}
|
|
520
736
|
}
|
|
@@ -538,7 +754,11 @@ function sleep(ms) {
|
|
|
538
754
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
539
755
|
}
|
|
540
756
|
function isAbortError(error) {
|
|
541
|
-
return error instanceof Error && error.name === "AbortError";
|
|
757
|
+
return error instanceof Error && (error.name === "AbortError" || error.name === "RiseonlyAbortError");
|
|
758
|
+
}
|
|
759
|
+
function testPattern(pattern, value) {
|
|
760
|
+
pattern.lastIndex = 0;
|
|
761
|
+
return pattern.test(value);
|
|
542
762
|
}
|
|
543
763
|
function normalizeHeaders(headers) {
|
|
544
764
|
const normalized = {};
|
|
@@ -547,10 +767,19 @@ function normalizeHeaders(headers) {
|
|
|
547
767
|
}
|
|
548
768
|
return normalized;
|
|
549
769
|
}
|
|
550
|
-
|
|
770
|
+
var WebhookPayloadTooLargeError = class extends Error {
|
|
771
|
+
};
|
|
772
|
+
async function readJsonBody(req, maxBodyBytes = 1024 * 1024) {
|
|
551
773
|
const chunks = [];
|
|
774
|
+
let received = 0;
|
|
552
775
|
for await (const chunk of req) {
|
|
553
|
-
|
|
776
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
777
|
+
received += buffer.length;
|
|
778
|
+
if (received > maxBodyBytes) {
|
|
779
|
+
req.resume();
|
|
780
|
+
throw new WebhookPayloadTooLargeError("webhook payload is too large");
|
|
781
|
+
}
|
|
782
|
+
chunks.push(buffer);
|
|
554
783
|
}
|
|
555
784
|
if (chunks.length === 0) {
|
|
556
785
|
return {};
|
|
@@ -593,8 +822,12 @@ function verifyInitData(botToken, encoded) {
|
|
|
593
822
|
if (left.length !== right.length || !timingSafeEqual(left, right)) {
|
|
594
823
|
throw new Error("init data signature is invalid");
|
|
595
824
|
}
|
|
596
|
-
const
|
|
597
|
-
|
|
825
|
+
const authDate = Number(fields.auth_date);
|
|
826
|
+
const expiresAt = Number(fields.expires_at);
|
|
827
|
+
if (!Number.isSafeInteger(authDate) || !Number.isSafeInteger(expiresAt) || expiresAt <= authDate) {
|
|
828
|
+
throw new Error("init data timestamps are invalid");
|
|
829
|
+
}
|
|
830
|
+
if (expiresAt < Date.now()) {
|
|
598
831
|
throw new Error("init data has expired");
|
|
599
832
|
}
|
|
600
833
|
return fields;
|
|
@@ -611,6 +844,6 @@ function createRequestId() {
|
|
|
611
844
|
return crypto.randomUUID();
|
|
612
845
|
}
|
|
613
846
|
|
|
614
|
-
export { ApiClient, CANONICAL_API_PREFIX, CHAT_ACTIONS, COMMAND_SCOPES, DEFAULT_API_BASE, IDEMPOTENCY_HEADER, RiseonlyBot, RiseonlyError, RiseonlyNetworkError, RiseonlyTimeoutError, SUPPORTED_METHODS, UPDATE_TYPES, WEBHOOK_SECRET_HEADER, buildInitDataCheckString, createRequestId, createWebhookHandler, deriveInitDataSecret, extractInitDataFromLaunchUrl, parseInitData, parseWebhookUpdate, signInitData, verifyInitData, verifyWebhookSecret };
|
|
847
|
+
export { ApiClient, CANONICAL_API_PREFIX, CHAT_ACTIONS, COMMAND_SCOPES, DEFAULT_API_BASE, IDEMPOTENCY_HEADER, RiseonlyAbortError, RiseonlyBot, RiseonlyError, RiseonlyNetworkError, RiseonlyResponseError, RiseonlyTimeoutError, SUPPORTED_METHODS, UPDATE_TYPES, WEBHOOK_SECRET_HEADER, buildInitDataCheckString, createRequestId, createWebhookHandler, deriveInitDataSecret, extractInitDataFromLaunchUrl, parseInitData, parseWebhookUpdate, signInitData, verifyInitData, verifyWebhookSecret };
|
|
615
848
|
//# sourceMappingURL=index.js.map
|
|
616
849
|
//# sourceMappingURL=index.js.map
|