@riseonly/sdk 0.1.1 → 0.1.3
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 +51 -9
- package/dist/index.cjs +350 -79
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +84 -35
- package/dist/index.d.ts +84 -35
- package/dist/index.js +350 -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
|
|
|
@@ -27,6 +27,12 @@ var SUPPORTED_METHODS = [
|
|
|
27
27
|
"setMyCommands",
|
|
28
28
|
"getMyCommands",
|
|
29
29
|
"deleteMyCommands",
|
|
30
|
+
"setMyName",
|
|
31
|
+
"getMyName",
|
|
32
|
+
"setMyDescription",
|
|
33
|
+
"getMyDescription",
|
|
34
|
+
"setMyProfilePhoto",
|
|
35
|
+
"removeMyProfilePhoto",
|
|
30
36
|
"answerCallbackQuery",
|
|
31
37
|
"getChat"
|
|
32
38
|
];
|
|
@@ -92,6 +98,22 @@ var RiseonlyTimeoutError = class extends RiseonlyNetworkError {
|
|
|
92
98
|
this.name = "RiseonlyTimeoutError";
|
|
93
99
|
}
|
|
94
100
|
};
|
|
101
|
+
var RiseonlyAbortError = class extends RiseonlyNetworkError {
|
|
102
|
+
constructor(message = "Request was aborted", cause) {
|
|
103
|
+
super(message, cause, 499);
|
|
104
|
+
this.name = "RiseonlyAbortError";
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
var RiseonlyResponseError = class extends RiseonlyNetworkError {
|
|
108
|
+
status;
|
|
109
|
+
body;
|
|
110
|
+
constructor(message, status, body) {
|
|
111
|
+
super(message, void 0, status);
|
|
112
|
+
this.name = "RiseonlyResponseError";
|
|
113
|
+
this.status = status;
|
|
114
|
+
this.body = body;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
95
117
|
|
|
96
118
|
// src/client.ts
|
|
97
119
|
var ApiClient = class {
|
|
@@ -106,8 +128,15 @@ var ApiClient = class {
|
|
|
106
128
|
}
|
|
107
129
|
this.token = token.trim();
|
|
108
130
|
this.baseUrl = (options.baseUrl ?? DEFAULT_API_BASE).replace(/\/$/, "");
|
|
109
|
-
|
|
131
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
132
|
+
if (typeof fetchImpl !== "function") {
|
|
133
|
+
throw new Error("global fetch is unavailable; provide ClientOptions.fetch");
|
|
134
|
+
}
|
|
135
|
+
this.fetchImpl = options.fetch ? fetchImpl : fetchImpl.bind(globalThis);
|
|
110
136
|
this.requestTimeoutMs = options.requestTimeoutMs ?? 6e4;
|
|
137
|
+
if (!Number.isFinite(this.requestTimeoutMs) || this.requestTimeoutMs <= 0) {
|
|
138
|
+
throw new Error("requestTimeoutMs must be a positive number");
|
|
139
|
+
}
|
|
111
140
|
this.useCompatibleEndpoint = options.useCompatibleEndpoint ?? false;
|
|
112
141
|
}
|
|
113
142
|
/**
|
|
@@ -115,15 +144,21 @@ var ApiClient = class {
|
|
|
115
144
|
*/
|
|
116
145
|
async getCapabilities(signal) {
|
|
117
146
|
const url = `${this.baseUrl}${CANONICAL_API_PREFIX}/capabilities`;
|
|
118
|
-
const response = await this.
|
|
147
|
+
const { response, body } = await this.fetchJson(url, {
|
|
119
148
|
method: "GET",
|
|
120
|
-
headers: { Accept: "application/json" }
|
|
121
|
-
|
|
122
|
-
});
|
|
149
|
+
headers: { Accept: "application/json" }
|
|
150
|
+
}, { signal });
|
|
123
151
|
if (!response.ok) {
|
|
124
|
-
throw new
|
|
152
|
+
throw new RiseonlyResponseError(
|
|
153
|
+
`capabilities request failed with status ${response.status}`,
|
|
154
|
+
response.status,
|
|
155
|
+
serializeResponseBody(body)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (!isRecord(body) || !Array.isArray(body.methods)) {
|
|
159
|
+
throw new RiseonlyResponseError("capabilities response is invalid", response.status);
|
|
125
160
|
}
|
|
126
|
-
return
|
|
161
|
+
return body;
|
|
127
162
|
}
|
|
128
163
|
/**
|
|
129
164
|
* Calls a Bot API method and returns the typed result.
|
|
@@ -145,37 +180,36 @@ var ApiClient = class {
|
|
|
145
180
|
if (options.requestId) {
|
|
146
181
|
headers[IDEMPOTENCY_HEADER] = options.requestId;
|
|
147
182
|
}
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
let response;
|
|
152
|
-
try {
|
|
153
|
-
response = await this.fetchImpl(url, {
|
|
183
|
+
const { response, body } = await this.fetchJson(
|
|
184
|
+
url,
|
|
185
|
+
{
|
|
154
186
|
method: "POST",
|
|
155
187
|
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) {
|
|
188
|
+
body: JSON.stringify(payload)
|
|
189
|
+
},
|
|
190
|
+
options
|
|
191
|
+
);
|
|
192
|
+
if (isApiErrorResponse(body)) {
|
|
169
193
|
throw RiseonlyError.fromApiResponse(body);
|
|
170
194
|
}
|
|
195
|
+
if (!response.ok) {
|
|
196
|
+
throw new RiseonlyResponseError(
|
|
197
|
+
`Bot API request failed with status ${response.status}`,
|
|
198
|
+
response.status,
|
|
199
|
+
serializeResponseBody(body)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
if (!isRecord(body) || body.ok !== true || !("result" in body)) {
|
|
203
|
+
throw new RiseonlyResponseError("Bot API response envelope is invalid", response.status);
|
|
204
|
+
}
|
|
171
205
|
return body;
|
|
172
206
|
}
|
|
173
207
|
async getMe(options) {
|
|
174
208
|
return this.callMethod("getMe", {}, options);
|
|
175
209
|
}
|
|
176
210
|
async sendMessage(options) {
|
|
177
|
-
const {
|
|
178
|
-
return this.callMethod("sendMessage", payload,
|
|
211
|
+
const { request, payload } = splitRequestOptions(options);
|
|
212
|
+
return this.callMethod("sendMessage", payload, request);
|
|
179
213
|
}
|
|
180
214
|
async sendPhoto(photo, options) {
|
|
181
215
|
return this.sendMedia("sendPhoto", "photo", photo, options);
|
|
@@ -196,72 +230,96 @@ var ApiClient = class {
|
|
|
196
230
|
return this.sendMedia("sendAnimation", "animation", animation, options);
|
|
197
231
|
}
|
|
198
232
|
async editMessageText(options) {
|
|
199
|
-
const {
|
|
200
|
-
return this.callMethod("editMessageText", payload,
|
|
233
|
+
const { request, payload } = splitRequestOptions(options);
|
|
234
|
+
return this.callMethod("editMessageText", payload, request);
|
|
201
235
|
}
|
|
202
236
|
async deleteMessage(options) {
|
|
203
|
-
const {
|
|
204
|
-
return this.callMethod("deleteMessage", payload,
|
|
237
|
+
const { request, payload } = splitRequestOptions(options);
|
|
238
|
+
return this.callMethod("deleteMessage", payload, request);
|
|
205
239
|
}
|
|
206
240
|
async sendChatAction(options) {
|
|
207
|
-
const {
|
|
208
|
-
return this.callMethod("sendChatAction", payload,
|
|
241
|
+
const { request, payload } = splitRequestOptions(options);
|
|
242
|
+
return this.callMethod("sendChatAction", payload, request);
|
|
209
243
|
}
|
|
210
244
|
async getUpdates(options = {}) {
|
|
211
|
-
const {
|
|
212
|
-
|
|
245
|
+
const { request, payload } = splitRequestOptions(options);
|
|
246
|
+
const serverTimeoutMs = (options.timeout ?? 0) * 1e3 + 5e3;
|
|
247
|
+
request.timeoutMs ??= Math.max(this.requestTimeoutMs, serverTimeoutMs);
|
|
248
|
+
return this.callMethod("getUpdates", payload, request);
|
|
213
249
|
}
|
|
214
250
|
async setWebhook(options) {
|
|
215
|
-
const {
|
|
216
|
-
return this.callMethod("setWebhook", payload,
|
|
251
|
+
const { request, payload } = splitRequestOptions(options);
|
|
252
|
+
return this.callMethod("setWebhook", payload, request);
|
|
217
253
|
}
|
|
218
254
|
async deleteWebhook(options = {}) {
|
|
219
|
-
const {
|
|
220
|
-
return this.callMethod("deleteWebhook", payload,
|
|
255
|
+
const { request, payload } = splitRequestOptions(options);
|
|
256
|
+
return this.callMethod("deleteWebhook", payload, request);
|
|
221
257
|
}
|
|
222
258
|
async getWebhookInfo(options) {
|
|
223
259
|
return this.callMethod("getWebhookInfo", {}, options);
|
|
224
260
|
}
|
|
225
261
|
async setMyCommands(options) {
|
|
226
|
-
const {
|
|
227
|
-
return this.callMethod("setMyCommands", payload,
|
|
262
|
+
const { request, payload } = splitRequestOptions(options);
|
|
263
|
+
return this.callMethod("setMyCommands", payload, request);
|
|
228
264
|
}
|
|
229
265
|
async getMyCommands(options = {}) {
|
|
230
|
-
const {
|
|
231
|
-
return this.callMethod("getMyCommands", payload,
|
|
266
|
+
const { request, payload } = splitRequestOptions(options);
|
|
267
|
+
return this.callMethod("getMyCommands", payload, request);
|
|
232
268
|
}
|
|
233
269
|
async deleteMyCommands(options = {}) {
|
|
234
|
-
const {
|
|
235
|
-
return this.callMethod("deleteMyCommands", payload,
|
|
270
|
+
const { request, payload } = splitRequestOptions(options);
|
|
271
|
+
return this.callMethod("deleteMyCommands", payload, request);
|
|
272
|
+
}
|
|
273
|
+
async setMyName(options) {
|
|
274
|
+
const { request, payload } = splitRequestOptions(options);
|
|
275
|
+
return this.callMethod("setMyName", payload, request);
|
|
276
|
+
}
|
|
277
|
+
async getMyName(options = {}) {
|
|
278
|
+
return this.callMethod("getMyName", {}, options);
|
|
279
|
+
}
|
|
280
|
+
async setMyDescription(options) {
|
|
281
|
+
const { request, payload } = splitRequestOptions(options);
|
|
282
|
+
return this.callMethod("setMyDescription", payload, request);
|
|
283
|
+
}
|
|
284
|
+
async getMyDescription(options = {}) {
|
|
285
|
+
return this.callMethod("getMyDescription", {}, options);
|
|
286
|
+
}
|
|
287
|
+
async setMyProfilePhoto(options) {
|
|
288
|
+
const { request, payload } = splitRequestOptions(options);
|
|
289
|
+
return this.callMethod("setMyProfilePhoto", payload, request);
|
|
290
|
+
}
|
|
291
|
+
async removeMyProfilePhoto(options = {}) {
|
|
292
|
+
return this.callMethod("removeMyProfilePhoto", {}, options);
|
|
236
293
|
}
|
|
237
294
|
async answerCallbackQuery(options) {
|
|
238
|
-
const {
|
|
239
|
-
return this.callMethod("answerCallbackQuery", payload,
|
|
295
|
+
const { request, payload } = splitRequestOptions(options);
|
|
296
|
+
return this.callMethod("answerCallbackQuery", payload, request);
|
|
240
297
|
}
|
|
241
298
|
async getChat(options) {
|
|
242
|
-
const {
|
|
243
|
-
return this.callMethod("getChat", payload,
|
|
299
|
+
const { request, payload } = splitRequestOptions(options);
|
|
300
|
+
return this.callMethod("getChat", payload, request);
|
|
244
301
|
}
|
|
245
302
|
async sendMedia(method, field, media, options) {
|
|
246
|
-
const {
|
|
303
|
+
const { request, payload } = splitRequestOptions(options);
|
|
247
304
|
return this.callMethod(
|
|
248
305
|
method,
|
|
249
306
|
{
|
|
250
307
|
...payload,
|
|
251
308
|
[field]: this.normalizeMedia(media)
|
|
252
309
|
},
|
|
253
|
-
|
|
310
|
+
request
|
|
254
311
|
);
|
|
255
312
|
}
|
|
256
313
|
normalizeMedia(media) {
|
|
257
314
|
if (typeof media === "string") {
|
|
258
315
|
return media;
|
|
259
316
|
}
|
|
260
|
-
if (media.file_id) {
|
|
261
|
-
return
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
317
|
+
if (media.file_id || media.url) {
|
|
318
|
+
return Object.fromEntries(
|
|
319
|
+
Object.entries(media).filter(
|
|
320
|
+
(entry) => typeof entry[1] === "string" || typeof entry[1] === "number"
|
|
321
|
+
)
|
|
322
|
+
);
|
|
265
323
|
}
|
|
266
324
|
throw new Error("media requires file_id or url");
|
|
267
325
|
}
|
|
@@ -271,9 +329,77 @@ var ApiClient = class {
|
|
|
271
329
|
}
|
|
272
330
|
return `${this.baseUrl}${CANONICAL_API_PREFIX}/${method}`;
|
|
273
331
|
}
|
|
332
|
+
async fetchJson(url, init, options = {}) {
|
|
333
|
+
const controller = new AbortController();
|
|
334
|
+
let timedOut = false;
|
|
335
|
+
const timeoutMs = options.timeoutMs ?? this.requestTimeoutMs;
|
|
336
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
337
|
+
throw new Error("timeoutMs must be a positive number");
|
|
338
|
+
}
|
|
339
|
+
const abortFromCaller = () => controller.abort(options.signal?.reason);
|
|
340
|
+
if (options.signal?.aborted) {
|
|
341
|
+
throw new RiseonlyAbortError("Request was aborted", options.signal.reason);
|
|
342
|
+
}
|
|
343
|
+
options.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
344
|
+
const timeout = setTimeout(() => {
|
|
345
|
+
timedOut = true;
|
|
346
|
+
controller.abort();
|
|
347
|
+
}, timeoutMs);
|
|
348
|
+
let response;
|
|
349
|
+
try {
|
|
350
|
+
response = await this.fetchImpl(url, { ...init, signal: controller.signal });
|
|
351
|
+
} catch (error) {
|
|
352
|
+
if (controller.signal.aborted) {
|
|
353
|
+
if (timedOut) {
|
|
354
|
+
throw new RiseonlyTimeoutError();
|
|
355
|
+
}
|
|
356
|
+
throw new RiseonlyAbortError("Request was aborted", error);
|
|
357
|
+
}
|
|
358
|
+
throw new RiseonlyNetworkError("network request failed", error);
|
|
359
|
+
} finally {
|
|
360
|
+
clearTimeout(timeout);
|
|
361
|
+
options.signal?.removeEventListener("abort", abortFromCaller);
|
|
362
|
+
}
|
|
363
|
+
let text;
|
|
364
|
+
try {
|
|
365
|
+
text = await response.text();
|
|
366
|
+
} catch (error) {
|
|
367
|
+
throw new RiseonlyNetworkError("unable to read response body", error, response.status);
|
|
368
|
+
}
|
|
369
|
+
if (!text) {
|
|
370
|
+
throw new RiseonlyResponseError("Bot API returned an empty response", response.status);
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
return { response, body: JSON.parse(text) };
|
|
374
|
+
} catch {
|
|
375
|
+
throw new RiseonlyResponseError(
|
|
376
|
+
"Bot API returned a non-JSON response",
|
|
377
|
+
response.status,
|
|
378
|
+
text.slice(0, 4096)
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
274
382
|
};
|
|
275
|
-
|
|
276
|
-
|
|
383
|
+
function splitRequestOptions(options) {
|
|
384
|
+
const { requestId, signal, timeoutMs, ...payload } = options;
|
|
385
|
+
return {
|
|
386
|
+
request: { requestId, signal, timeoutMs },
|
|
387
|
+
payload
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function isRecord(value) {
|
|
391
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
392
|
+
}
|
|
393
|
+
function isApiErrorResponse(value) {
|
|
394
|
+
return isRecord(value) && value.ok === false && typeof value.error_code === "number" && typeof value.description === "string";
|
|
395
|
+
}
|
|
396
|
+
function serializeResponseBody(body) {
|
|
397
|
+
try {
|
|
398
|
+
return JSON.stringify(body).slice(0, 4096);
|
|
399
|
+
} catch {
|
|
400
|
+
return void 0;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
277
403
|
function parseWebhookUpdate(body) {
|
|
278
404
|
if (!body || typeof body !== "object") {
|
|
279
405
|
throw new Error("webhook body must be an object");
|
|
@@ -288,28 +414,49 @@ function verifyWebhookSecret(headers, secretToken) {
|
|
|
288
414
|
if (!secretToken) {
|
|
289
415
|
return true;
|
|
290
416
|
}
|
|
291
|
-
const
|
|
417
|
+
const headerEntry = Object.entries(headers).find(
|
|
418
|
+
([name]) => name.toLowerCase() === WEBHOOK_SECRET_HEADER
|
|
419
|
+
);
|
|
420
|
+
const headerValue = headerEntry?.[1];
|
|
292
421
|
if (!headerValue) {
|
|
293
422
|
return false;
|
|
294
423
|
}
|
|
295
424
|
const received = Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
296
|
-
|
|
425
|
+
if (typeof received !== "string") {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
const actual = Buffer.from(received);
|
|
429
|
+
const expected = Buffer.from(secretToken);
|
|
430
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
297
431
|
}
|
|
298
432
|
function createWebhookHandler(options) {
|
|
299
433
|
return async (request) => {
|
|
434
|
+
if (!verifyWebhookSecret(request.headers, options.secretToken)) {
|
|
435
|
+
return { status: 401, body: { ok: false, description: "invalid webhook secret" } };
|
|
436
|
+
}
|
|
437
|
+
let update;
|
|
438
|
+
try {
|
|
439
|
+
update = parseWebhookUpdate(request.body);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
await notifyWebhookError(options.onError, error);
|
|
442
|
+
return { status: 400, body: { ok: false, description: "invalid webhook payload" } };
|
|
443
|
+
}
|
|
300
444
|
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
445
|
await options.onUpdate(update);
|
|
306
446
|
return { status: 200, body: { ok: true } };
|
|
307
447
|
} catch (error) {
|
|
308
|
-
await options.onError
|
|
309
|
-
return { status:
|
|
448
|
+
await notifyWebhookError(options.onError, error);
|
|
449
|
+
return { status: 500, body: { ok: false, description: "webhook handler failed" } };
|
|
310
450
|
}
|
|
311
451
|
};
|
|
312
452
|
}
|
|
453
|
+
async function notifyWebhookError(handler, error) {
|
|
454
|
+
try {
|
|
455
|
+
await handler?.(error);
|
|
456
|
+
} catch {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
313
460
|
|
|
314
461
|
// src/bot.ts
|
|
315
462
|
var RiseonlyBot = class extends ApiClient {
|
|
@@ -325,6 +472,7 @@ var RiseonlyBot = class extends ApiClient {
|
|
|
325
472
|
webhookOptions;
|
|
326
473
|
polling = false;
|
|
327
474
|
pollingAbort;
|
|
475
|
+
pollingTask;
|
|
328
476
|
offset = 0;
|
|
329
477
|
webhookServer;
|
|
330
478
|
constructor(token, options = {}) {
|
|
@@ -366,6 +514,76 @@ var RiseonlyBot = class extends ApiClient {
|
|
|
366
514
|
});
|
|
367
515
|
return this.on(event, wrapper);
|
|
368
516
|
}
|
|
517
|
+
command(commands, handler) {
|
|
518
|
+
const accepted = new Set(
|
|
519
|
+
(Array.isArray(commands) ? commands : [commands]).map((command) => command.replace(/^\//, "").toLowerCase()).filter(Boolean)
|
|
520
|
+
);
|
|
521
|
+
return this.on("message", async (message, update) => {
|
|
522
|
+
const text = message.text?.trim();
|
|
523
|
+
if (!text?.startsWith("/")) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const token = text.split(/\s+/, 1)[0].slice(1).split("@", 1)[0].toLowerCase();
|
|
527
|
+
if (accepted.has(token)) {
|
|
528
|
+
await handler(message, update);
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
hears(trigger, handler) {
|
|
533
|
+
return this.on("message", async (message, update) => {
|
|
534
|
+
if (message.text === void 0) {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (typeof trigger === "string") {
|
|
538
|
+
if (message.text === trigger) {
|
|
539
|
+
await handler(message, null, update);
|
|
540
|
+
}
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
trigger.lastIndex = 0;
|
|
544
|
+
const match = trigger.exec(message.text);
|
|
545
|
+
if (match) {
|
|
546
|
+
await handler(message, match, update);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
action(trigger, handler) {
|
|
551
|
+
return this.on("callback_query", async (query, update) => {
|
|
552
|
+
if (query.data === void 0) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const matches = typeof trigger === "string" ? query.data === trigger : testPattern(trigger, query.data);
|
|
556
|
+
if (matches) {
|
|
557
|
+
await handler(query, update);
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
catch(handler) {
|
|
562
|
+
return this.on("error", handler);
|
|
563
|
+
}
|
|
564
|
+
async setup(options) {
|
|
565
|
+
if (options.name !== void 0) {
|
|
566
|
+
await this.setMyName({ name: options.name });
|
|
567
|
+
}
|
|
568
|
+
if (options.description !== void 0) {
|
|
569
|
+
await this.setMyDescription({ description: options.description });
|
|
570
|
+
}
|
|
571
|
+
if (options.profilePhoto !== void 0) {
|
|
572
|
+
await this.setMyProfilePhoto({ photo: options.profilePhoto });
|
|
573
|
+
}
|
|
574
|
+
if (options.commands) {
|
|
575
|
+
await this.setMyCommands({
|
|
576
|
+
commands: options.commands,
|
|
577
|
+
scope: options.commandScope,
|
|
578
|
+
language_code: options.languageCode
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
if (options.webhook === false) {
|
|
582
|
+
await this.deleteWebhook();
|
|
583
|
+
} else if (options.webhook) {
|
|
584
|
+
await this.setWebhook(options.webhook);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
369
587
|
/**
|
|
370
588
|
* Starts long polling for bot updates.
|
|
371
589
|
*/
|
|
@@ -376,7 +594,7 @@ var RiseonlyBot = class extends ApiClient {
|
|
|
376
594
|
this.polling = true;
|
|
377
595
|
this.pollingOptions = { ...this.pollingOptions, ...options, autoStart: true };
|
|
378
596
|
this.pollingAbort = new AbortController();
|
|
379
|
-
|
|
597
|
+
this.pollingTask = this.pollLoop();
|
|
380
598
|
}
|
|
381
599
|
/**
|
|
382
600
|
* Stops long polling.
|
|
@@ -384,7 +602,15 @@ var RiseonlyBot = class extends ApiClient {
|
|
|
384
602
|
async stopPolling() {
|
|
385
603
|
this.polling = false;
|
|
386
604
|
this.pollingAbort?.abort();
|
|
605
|
+
await this.pollingTask;
|
|
387
606
|
this.pollingAbort = void 0;
|
|
607
|
+
this.pollingTask = void 0;
|
|
608
|
+
}
|
|
609
|
+
async launch(options = {}) {
|
|
610
|
+
await this.startPolling(options);
|
|
611
|
+
}
|
|
612
|
+
async stop() {
|
|
613
|
+
await Promise.all([this.stopPolling(), this.stopWebhook()]);
|
|
388
614
|
}
|
|
389
615
|
/**
|
|
390
616
|
* Starts a built-in HTTP server that receives webhook updates.
|
|
@@ -395,6 +621,12 @@ var RiseonlyBot = class extends ApiClient {
|
|
|
395
621
|
const path = config.path ?? "/";
|
|
396
622
|
const host = config.host ?? "0.0.0.0";
|
|
397
623
|
const port = config.port ?? 3e3;
|
|
624
|
+
if (!path.startsWith("/")) {
|
|
625
|
+
throw new Error("webhook path must start with /");
|
|
626
|
+
}
|
|
627
|
+
if (config.maxBodyBytes !== void 0 && config.maxBodyBytes <= 0) {
|
|
628
|
+
throw new Error("maxBodyBytes must be positive");
|
|
629
|
+
}
|
|
398
630
|
const handler = createWebhookHandler({
|
|
399
631
|
secretToken: config.secretToken,
|
|
400
632
|
onUpdate: (update) => this.processUpdate(update),
|
|
@@ -409,15 +641,16 @@ var RiseonlyBot = class extends ApiClient {
|
|
|
409
641
|
return;
|
|
410
642
|
}
|
|
411
643
|
try {
|
|
412
|
-
const body = await readJsonBody(req);
|
|
644
|
+
const body = await readJsonBody(req, config.maxBodyBytes);
|
|
413
645
|
const result = await handler({
|
|
414
646
|
headers: normalizeHeaders(req.headers),
|
|
415
647
|
body
|
|
416
648
|
});
|
|
417
649
|
respond(res, result.status, result.body);
|
|
418
650
|
} catch (error) {
|
|
419
|
-
await this.
|
|
420
|
-
|
|
651
|
+
await this.emitError(error);
|
|
652
|
+
const status = error instanceof WebhookPayloadTooLargeError ? 413 : 500;
|
|
653
|
+
respond(res, status, { ok: false });
|
|
421
654
|
}
|
|
422
655
|
});
|
|
423
656
|
await new Promise((resolve, reject) => {
|
|
@@ -489,8 +722,8 @@ var RiseonlyBot = class extends ApiClient {
|
|
|
489
722
|
if (update.update_id < this.offset) {
|
|
490
723
|
continue;
|
|
491
724
|
}
|
|
492
|
-
this.offset = Math.max(this.offset, update.update_id + 1);
|
|
493
725
|
await this.processUpdate(update);
|
|
726
|
+
this.offset = Math.max(this.offset, update.update_id + 1);
|
|
494
727
|
processed += 1;
|
|
495
728
|
}
|
|
496
729
|
if (processed === 0) {
|
|
@@ -500,7 +733,10 @@ var RiseonlyBot = class extends ApiClient {
|
|
|
500
733
|
if (!this.polling || isAbortError(error)) {
|
|
501
734
|
return;
|
|
502
735
|
}
|
|
503
|
-
|
|
736
|
+
try {
|
|
737
|
+
await this.emit("polling_error", error);
|
|
738
|
+
} catch {
|
|
739
|
+
}
|
|
504
740
|
if (error instanceof RiseonlyError && error.retryAfter) {
|
|
505
741
|
await sleep(error.retryAfter * 1e3);
|
|
506
742
|
} else {
|
|
@@ -510,11 +746,27 @@ var RiseonlyBot = class extends ApiClient {
|
|
|
510
746
|
}
|
|
511
747
|
}
|
|
512
748
|
async emit(event, ...args) {
|
|
749
|
+
let firstError;
|
|
513
750
|
for (const listener of [...this.listeners[event]]) {
|
|
514
751
|
try {
|
|
515
752
|
await listener(...args);
|
|
516
753
|
} catch (error) {
|
|
517
|
-
|
|
754
|
+
firstError ??= error;
|
|
755
|
+
if (event !== "error") {
|
|
756
|
+
await this.emitError(error);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (firstError !== void 0) {
|
|
761
|
+
throw firstError;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
async emitError(error) {
|
|
765
|
+
for (const listener of [...this.listeners.error]) {
|
|
766
|
+
try {
|
|
767
|
+
await listener(error);
|
|
768
|
+
} catch {
|
|
769
|
+
continue;
|
|
518
770
|
}
|
|
519
771
|
}
|
|
520
772
|
}
|
|
@@ -538,7 +790,11 @@ function sleep(ms) {
|
|
|
538
790
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
539
791
|
}
|
|
540
792
|
function isAbortError(error) {
|
|
541
|
-
return error instanceof Error && error.name === "AbortError";
|
|
793
|
+
return error instanceof Error && (error.name === "AbortError" || error.name === "RiseonlyAbortError");
|
|
794
|
+
}
|
|
795
|
+
function testPattern(pattern, value) {
|
|
796
|
+
pattern.lastIndex = 0;
|
|
797
|
+
return pattern.test(value);
|
|
542
798
|
}
|
|
543
799
|
function normalizeHeaders(headers) {
|
|
544
800
|
const normalized = {};
|
|
@@ -547,10 +803,19 @@ function normalizeHeaders(headers) {
|
|
|
547
803
|
}
|
|
548
804
|
return normalized;
|
|
549
805
|
}
|
|
550
|
-
|
|
806
|
+
var WebhookPayloadTooLargeError = class extends Error {
|
|
807
|
+
};
|
|
808
|
+
async function readJsonBody(req, maxBodyBytes = 1024 * 1024) {
|
|
551
809
|
const chunks = [];
|
|
810
|
+
let received = 0;
|
|
552
811
|
for await (const chunk of req) {
|
|
553
|
-
|
|
812
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
813
|
+
received += buffer.length;
|
|
814
|
+
if (received > maxBodyBytes) {
|
|
815
|
+
req.resume();
|
|
816
|
+
throw new WebhookPayloadTooLargeError("webhook payload is too large");
|
|
817
|
+
}
|
|
818
|
+
chunks.push(buffer);
|
|
554
819
|
}
|
|
555
820
|
if (chunks.length === 0) {
|
|
556
821
|
return {};
|
|
@@ -593,8 +858,12 @@ function verifyInitData(botToken, encoded) {
|
|
|
593
858
|
if (left.length !== right.length || !timingSafeEqual(left, right)) {
|
|
594
859
|
throw new Error("init data signature is invalid");
|
|
595
860
|
}
|
|
596
|
-
const
|
|
597
|
-
|
|
861
|
+
const authDate = Number(fields.auth_date);
|
|
862
|
+
const expiresAt = Number(fields.expires_at);
|
|
863
|
+
if (!Number.isSafeInteger(authDate) || !Number.isSafeInteger(expiresAt) || expiresAt <= authDate) {
|
|
864
|
+
throw new Error("init data timestamps are invalid");
|
|
865
|
+
}
|
|
866
|
+
if (expiresAt < Date.now()) {
|
|
598
867
|
throw new Error("init data has expired");
|
|
599
868
|
}
|
|
600
869
|
return fields;
|
|
@@ -611,6 +880,6 @@ function createRequestId() {
|
|
|
611
880
|
return crypto.randomUUID();
|
|
612
881
|
}
|
|
613
882
|
|
|
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 };
|
|
883
|
+
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
884
|
//# sourceMappingURL=index.js.map
|
|
616
885
|
//# sourceMappingURL=index.js.map
|