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