@riseonly/sdk 0.1.0
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/LICENSE +21 -0
- package/README.md +163 -0
- package/dist/index.cjs +640 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +535 -0
- package/dist/index.d.ts +535 -0
- package/dist/index.js +616 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { createHmac, timingSafeEqual } from 'crypto';
|
|
3
|
+
|
|
4
|
+
// src/bot.ts
|
|
5
|
+
|
|
6
|
+
// src/constants.ts
|
|
7
|
+
var DEFAULT_API_BASE = "https://api.riseonly.net";
|
|
8
|
+
var CANONICAL_API_PREFIX = "/api/bot/v1";
|
|
9
|
+
var WEBHOOK_SECRET_HEADER = "x-riseonly-bot-api-secret-token";
|
|
10
|
+
var IDEMPOTENCY_HEADER = "x-request-id";
|
|
11
|
+
var SUPPORTED_METHODS = [
|
|
12
|
+
"getMe",
|
|
13
|
+
"sendMessage",
|
|
14
|
+
"sendPhoto",
|
|
15
|
+
"sendVideo",
|
|
16
|
+
"sendDocument",
|
|
17
|
+
"sendAudio",
|
|
18
|
+
"sendVoice",
|
|
19
|
+
"sendAnimation",
|
|
20
|
+
"editMessageText",
|
|
21
|
+
"deleteMessage",
|
|
22
|
+
"sendChatAction",
|
|
23
|
+
"getUpdates",
|
|
24
|
+
"setWebhook",
|
|
25
|
+
"deleteWebhook",
|
|
26
|
+
"getWebhookInfo",
|
|
27
|
+
"setMyCommands",
|
|
28
|
+
"getMyCommands",
|
|
29
|
+
"deleteMyCommands",
|
|
30
|
+
"answerCallbackQuery",
|
|
31
|
+
"getChat"
|
|
32
|
+
];
|
|
33
|
+
var UPDATE_TYPES = [
|
|
34
|
+
"message",
|
|
35
|
+
"edited_message",
|
|
36
|
+
"callback_query",
|
|
37
|
+
"chat_member",
|
|
38
|
+
"my_chat_member"
|
|
39
|
+
];
|
|
40
|
+
var CHAT_ACTIONS = [
|
|
41
|
+
"typing",
|
|
42
|
+
"upload_photo",
|
|
43
|
+
"record_video",
|
|
44
|
+
"upload_video",
|
|
45
|
+
"record_voice",
|
|
46
|
+
"upload_voice",
|
|
47
|
+
"upload_document",
|
|
48
|
+
"choose_sticker",
|
|
49
|
+
"find_location",
|
|
50
|
+
"record_video_note",
|
|
51
|
+
"upload_video_note"
|
|
52
|
+
];
|
|
53
|
+
var COMMAND_SCOPES = [
|
|
54
|
+
"default",
|
|
55
|
+
"all_private_chats",
|
|
56
|
+
"all_group_chats",
|
|
57
|
+
"all_chat_administrators"
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// src/errors.ts
|
|
61
|
+
var RiseonlyError = class _RiseonlyError extends Error {
|
|
62
|
+
code;
|
|
63
|
+
retryAfter;
|
|
64
|
+
response;
|
|
65
|
+
constructor(message, code, retryAfter, response) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = "RiseonlyError";
|
|
68
|
+
this.code = code;
|
|
69
|
+
this.retryAfter = retryAfter;
|
|
70
|
+
this.response = response;
|
|
71
|
+
}
|
|
72
|
+
static fromApiResponse(body) {
|
|
73
|
+
return new _RiseonlyError(
|
|
74
|
+
body.description,
|
|
75
|
+
body.error_code,
|
|
76
|
+
body.parameters?.retry_after,
|
|
77
|
+
body
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
var RiseonlyNetworkError = class extends RiseonlyError {
|
|
82
|
+
cause;
|
|
83
|
+
constructor(message, cause, code = 0) {
|
|
84
|
+
super(message, code);
|
|
85
|
+
this.name = "RiseonlyNetworkError";
|
|
86
|
+
this.cause = cause;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
var RiseonlyTimeoutError = class extends RiseonlyNetworkError {
|
|
90
|
+
constructor(message = "Request timed out") {
|
|
91
|
+
super(message, void 0, 408);
|
|
92
|
+
this.name = "RiseonlyTimeoutError";
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// src/client.ts
|
|
97
|
+
var ApiClient = class {
|
|
98
|
+
token;
|
|
99
|
+
baseUrl;
|
|
100
|
+
fetchImpl;
|
|
101
|
+
requestTimeoutMs;
|
|
102
|
+
useCompatibleEndpoint;
|
|
103
|
+
constructor(token, options = {}) {
|
|
104
|
+
if (!token?.trim()) {
|
|
105
|
+
throw new Error("bot token is required");
|
|
106
|
+
}
|
|
107
|
+
this.token = token.trim();
|
|
108
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_API_BASE).replace(/\/$/, "");
|
|
109
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
110
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 6e4;
|
|
111
|
+
this.useCompatibleEndpoint = options.useCompatibleEndpoint ?? false;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Returns the platform capabilities document.
|
|
115
|
+
*/
|
|
116
|
+
async getCapabilities(signal) {
|
|
117
|
+
const url = `${this.baseUrl}${CANONICAL_API_PREFIX}/capabilities`;
|
|
118
|
+
const response = await this.fetchImpl(url, {
|
|
119
|
+
method: "GET",
|
|
120
|
+
headers: { Accept: "application/json" },
|
|
121
|
+
signal
|
|
122
|
+
});
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
throw new RiseonlyNetworkError(`capabilities request failed with status ${response.status}`);
|
|
125
|
+
}
|
|
126
|
+
return response.json();
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Calls a Bot API method and returns the typed result.
|
|
130
|
+
*/
|
|
131
|
+
async callMethod(method, payload = {}, options = {}) {
|
|
132
|
+
const envelope = await this.request(method, payload, options);
|
|
133
|
+
return envelope.result;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Performs a raw Bot API request and returns the full envelope.
|
|
137
|
+
*/
|
|
138
|
+
async request(method, payload = {}, options = {}) {
|
|
139
|
+
const url = this.buildMethodUrl(method);
|
|
140
|
+
const headers = {
|
|
141
|
+
Authorization: `Bot ${this.token}`,
|
|
142
|
+
Accept: "application/json",
|
|
143
|
+
"Content-Type": "application/json"
|
|
144
|
+
};
|
|
145
|
+
if (options.requestId) {
|
|
146
|
+
headers[IDEMPOTENCY_HEADER] = options.requestId;
|
|
147
|
+
}
|
|
148
|
+
const controller = new AbortController();
|
|
149
|
+
const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs);
|
|
150
|
+
const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
151
|
+
let response;
|
|
152
|
+
try {
|
|
153
|
+
response = await this.fetchImpl(url, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers,
|
|
156
|
+
body: JSON.stringify(payload),
|
|
157
|
+
signal
|
|
158
|
+
});
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
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) {
|
|
169
|
+
throw RiseonlyError.fromApiResponse(body);
|
|
170
|
+
}
|
|
171
|
+
return body;
|
|
172
|
+
}
|
|
173
|
+
async getMe(options) {
|
|
174
|
+
return this.callMethod("getMe", {}, options);
|
|
175
|
+
}
|
|
176
|
+
async sendMessage(options) {
|
|
177
|
+
const { requestId, ...payload } = options;
|
|
178
|
+
return this.callMethod("sendMessage", payload, { requestId });
|
|
179
|
+
}
|
|
180
|
+
async sendPhoto(photo, options) {
|
|
181
|
+
return this.sendMedia("sendPhoto", "photo", photo, options);
|
|
182
|
+
}
|
|
183
|
+
async sendVideo(video, options) {
|
|
184
|
+
return this.sendMedia("sendVideo", "video", video, options);
|
|
185
|
+
}
|
|
186
|
+
async sendDocument(document, options) {
|
|
187
|
+
return this.sendMedia("sendDocument", "document", document, options);
|
|
188
|
+
}
|
|
189
|
+
async sendAudio(audio, options) {
|
|
190
|
+
return this.sendMedia("sendAudio", "audio", audio, options);
|
|
191
|
+
}
|
|
192
|
+
async sendVoice(voice, options) {
|
|
193
|
+
return this.sendMedia("sendVoice", "voice", voice, options);
|
|
194
|
+
}
|
|
195
|
+
async sendAnimation(animation, options) {
|
|
196
|
+
return this.sendMedia("sendAnimation", "animation", animation, options);
|
|
197
|
+
}
|
|
198
|
+
async editMessageText(options) {
|
|
199
|
+
const { requestId, ...payload } = options;
|
|
200
|
+
return this.callMethod("editMessageText", payload, { requestId });
|
|
201
|
+
}
|
|
202
|
+
async deleteMessage(options) {
|
|
203
|
+
const { requestId, ...payload } = options;
|
|
204
|
+
return this.callMethod("deleteMessage", payload, { requestId });
|
|
205
|
+
}
|
|
206
|
+
async sendChatAction(options) {
|
|
207
|
+
const { requestId, ...payload } = options;
|
|
208
|
+
return this.callMethod("sendChatAction", payload, { requestId });
|
|
209
|
+
}
|
|
210
|
+
async getUpdates(options = {}) {
|
|
211
|
+
const { requestId, ...payload } = options;
|
|
212
|
+
return this.callMethod("getUpdates", payload, { requestId });
|
|
213
|
+
}
|
|
214
|
+
async setWebhook(options) {
|
|
215
|
+
const { requestId, ...payload } = options;
|
|
216
|
+
return this.callMethod("setWebhook", payload, { requestId });
|
|
217
|
+
}
|
|
218
|
+
async deleteWebhook(options = {}) {
|
|
219
|
+
const { requestId, ...payload } = options;
|
|
220
|
+
return this.callMethod("deleteWebhook", payload, { requestId });
|
|
221
|
+
}
|
|
222
|
+
async getWebhookInfo(options) {
|
|
223
|
+
return this.callMethod("getWebhookInfo", {}, options);
|
|
224
|
+
}
|
|
225
|
+
async setMyCommands(options) {
|
|
226
|
+
const { requestId, ...payload } = options;
|
|
227
|
+
return this.callMethod("setMyCommands", payload, { requestId });
|
|
228
|
+
}
|
|
229
|
+
async getMyCommands(options = {}) {
|
|
230
|
+
const { requestId, ...payload } = options;
|
|
231
|
+
return this.callMethod("getMyCommands", payload, { requestId });
|
|
232
|
+
}
|
|
233
|
+
async deleteMyCommands(options = {}) {
|
|
234
|
+
const { requestId, ...payload } = options;
|
|
235
|
+
return this.callMethod("deleteMyCommands", payload, { requestId });
|
|
236
|
+
}
|
|
237
|
+
async answerCallbackQuery(options) {
|
|
238
|
+
const { requestId, ...payload } = options;
|
|
239
|
+
return this.callMethod("answerCallbackQuery", payload, { requestId });
|
|
240
|
+
}
|
|
241
|
+
async getChat(options) {
|
|
242
|
+
const { requestId, ...payload } = options;
|
|
243
|
+
return this.callMethod("getChat", payload, { requestId });
|
|
244
|
+
}
|
|
245
|
+
async sendMedia(method, field, media, options) {
|
|
246
|
+
const { requestId, ...payload } = options;
|
|
247
|
+
return this.callMethod(
|
|
248
|
+
method,
|
|
249
|
+
{
|
|
250
|
+
...payload,
|
|
251
|
+
[field]: this.normalizeMedia(media)
|
|
252
|
+
},
|
|
253
|
+
{ requestId }
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
normalizeMedia(media) {
|
|
257
|
+
if (typeof media === "string") {
|
|
258
|
+
return media;
|
|
259
|
+
}
|
|
260
|
+
if (media.file_id) {
|
|
261
|
+
return media.file_id;
|
|
262
|
+
}
|
|
263
|
+
if (media.url) {
|
|
264
|
+
return media.url;
|
|
265
|
+
}
|
|
266
|
+
throw new Error("media requires file_id or url");
|
|
267
|
+
}
|
|
268
|
+
buildMethodUrl(method) {
|
|
269
|
+
if (this.useCompatibleEndpoint) {
|
|
270
|
+
return `${this.baseUrl}/bot${this.token}/${method}`;
|
|
271
|
+
}
|
|
272
|
+
return `${this.baseUrl}${CANONICAL_API_PREFIX}/${method}`;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// src/webhook.ts
|
|
277
|
+
function parseWebhookUpdate(body) {
|
|
278
|
+
if (!body || typeof body !== "object") {
|
|
279
|
+
throw new Error("webhook body must be an object");
|
|
280
|
+
}
|
|
281
|
+
const update = body;
|
|
282
|
+
if (typeof update.update_id !== "number") {
|
|
283
|
+
throw new Error("webhook body is missing update_id");
|
|
284
|
+
}
|
|
285
|
+
return update;
|
|
286
|
+
}
|
|
287
|
+
function verifyWebhookSecret(headers, secretToken) {
|
|
288
|
+
if (!secretToken) {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
const headerValue = headers[WEBHOOK_SECRET_HEADER] ?? headers[WEBHOOK_SECRET_HEADER.toLowerCase()];
|
|
292
|
+
if (!headerValue) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
const received = Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
296
|
+
return received === secretToken;
|
|
297
|
+
}
|
|
298
|
+
function createWebhookHandler(options) {
|
|
299
|
+
return async (request) => {
|
|
300
|
+
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
|
+
await options.onUpdate(update);
|
|
306
|
+
return { status: 200, body: { ok: true } };
|
|
307
|
+
} catch (error) {
|
|
308
|
+
await options.onError?.(error);
|
|
309
|
+
return { status: 400, body: { ok: false, description: "invalid webhook payload" } };
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/bot.ts
|
|
315
|
+
var RiseonlyBot = class extends ApiClient {
|
|
316
|
+
listeners = {
|
|
317
|
+
message: [],
|
|
318
|
+
edited_message: [],
|
|
319
|
+
callback_query: [],
|
|
320
|
+
update: [],
|
|
321
|
+
polling_error: [],
|
|
322
|
+
error: []
|
|
323
|
+
};
|
|
324
|
+
pollingOptions;
|
|
325
|
+
webhookOptions;
|
|
326
|
+
polling = false;
|
|
327
|
+
pollingAbort;
|
|
328
|
+
offset = 0;
|
|
329
|
+
webhookServer;
|
|
330
|
+
constructor(token, options = {}) {
|
|
331
|
+
super(token, options);
|
|
332
|
+
this.pollingOptions = normalizePollingOptions(options.polling);
|
|
333
|
+
this.webhookOptions = options.webhook === false ? void 0 : options.webhook;
|
|
334
|
+
if (options.allowed_updates) {
|
|
335
|
+
this.pollingOptions.allowed_updates = options.allowed_updates;
|
|
336
|
+
}
|
|
337
|
+
if (this.pollingOptions.autoStart) {
|
|
338
|
+
void this.startPolling();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Registers an event listener.
|
|
343
|
+
*/
|
|
344
|
+
on(event, handler) {
|
|
345
|
+
this.listeners[event].push(handler);
|
|
346
|
+
return this;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Removes a previously registered listener.
|
|
350
|
+
*/
|
|
351
|
+
off(event, handler) {
|
|
352
|
+
const bucket = this.listeners[event];
|
|
353
|
+
const index = bucket.indexOf(handler);
|
|
354
|
+
if (index >= 0) {
|
|
355
|
+
bucket.splice(index, 1);
|
|
356
|
+
}
|
|
357
|
+
return this;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Registers a one-time event listener.
|
|
361
|
+
*/
|
|
362
|
+
once(event, handler) {
|
|
363
|
+
const wrapper = ((...args) => {
|
|
364
|
+
this.off(event, wrapper);
|
|
365
|
+
return handler(...args);
|
|
366
|
+
});
|
|
367
|
+
return this.on(event, wrapper);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Starts long polling for bot updates.
|
|
371
|
+
*/
|
|
372
|
+
async startPolling(options = {}) {
|
|
373
|
+
if (this.polling) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
this.polling = true;
|
|
377
|
+
this.pollingOptions = { ...this.pollingOptions, ...options, autoStart: true };
|
|
378
|
+
this.pollingAbort = new AbortController();
|
|
379
|
+
void this.pollLoop();
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Stops long polling.
|
|
383
|
+
*/
|
|
384
|
+
async stopPolling() {
|
|
385
|
+
this.polling = false;
|
|
386
|
+
this.pollingAbort?.abort();
|
|
387
|
+
this.pollingAbort = void 0;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Starts a built-in HTTP server that receives webhook updates.
|
|
391
|
+
*/
|
|
392
|
+
async startWebhook(options = {}) {
|
|
393
|
+
const config = { ...this.webhookOptions, ...options };
|
|
394
|
+
this.webhookOptions = config;
|
|
395
|
+
const path = config.path ?? "/";
|
|
396
|
+
const host = config.host ?? "0.0.0.0";
|
|
397
|
+
const port = config.port ?? 3e3;
|
|
398
|
+
const handler = createWebhookHandler({
|
|
399
|
+
secretToken: config.secretToken,
|
|
400
|
+
onUpdate: (update) => this.processUpdate(update),
|
|
401
|
+
onError: (error) => this.emit("error", error)
|
|
402
|
+
});
|
|
403
|
+
if (this.webhookServer) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
this.webhookServer = createServer(async (req, res) => {
|
|
407
|
+
if (req.method !== "POST" || req.url?.split("?")[0] !== path) {
|
|
408
|
+
respond(res, 404, { ok: false });
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
const body = await readJsonBody(req);
|
|
413
|
+
const result = await handler({
|
|
414
|
+
headers: normalizeHeaders(req.headers),
|
|
415
|
+
body
|
|
416
|
+
});
|
|
417
|
+
respond(res, result.status, result.body);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
await this.emit("error", error);
|
|
420
|
+
respond(res, 500, { ok: false });
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
await new Promise((resolve, reject) => {
|
|
424
|
+
this.webhookServer.once("error", reject);
|
|
425
|
+
this.webhookServer.listen(port, host, () => resolve());
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Stops the built-in webhook server.
|
|
430
|
+
*/
|
|
431
|
+
async stopWebhook() {
|
|
432
|
+
if (!this.webhookServer) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const server = this.webhookServer;
|
|
436
|
+
this.webhookServer = void 0;
|
|
437
|
+
await new Promise((resolve, reject) => {
|
|
438
|
+
server.close((error) => error ? reject(error) : resolve());
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Processes a single update and dispatches events.
|
|
443
|
+
*/
|
|
444
|
+
async processUpdate(update) {
|
|
445
|
+
await this.emit("update", update);
|
|
446
|
+
if (update.message) {
|
|
447
|
+
await this.emit("message", update.message, update);
|
|
448
|
+
}
|
|
449
|
+
if (update.edited_message) {
|
|
450
|
+
await this.emit("edited_message", update.edited_message, update);
|
|
451
|
+
}
|
|
452
|
+
if (update.callback_query) {
|
|
453
|
+
await this.emit("callback_query", update.callback_query, update);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Sends a text message to a chat.
|
|
458
|
+
*/
|
|
459
|
+
async reply(message, text, extra = {}) {
|
|
460
|
+
return this.sendMessage({
|
|
461
|
+
chat_id: message.chat.id,
|
|
462
|
+
text,
|
|
463
|
+
reply_to_message_id: message.message_id,
|
|
464
|
+
...extra
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Answers a callback query and optionally sends a toast.
|
|
469
|
+
*/
|
|
470
|
+
async answer(query, text, showAlert = false) {
|
|
471
|
+
return this.answerCallbackQuery({
|
|
472
|
+
callback_query_id: query.id,
|
|
473
|
+
text,
|
|
474
|
+
show_alert: showAlert
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
async pollLoop() {
|
|
478
|
+
while (this.polling) {
|
|
479
|
+
try {
|
|
480
|
+
const updates = await this.getUpdates({
|
|
481
|
+
offset: this.offset,
|
|
482
|
+
timeout: this.pollingOptions.timeout,
|
|
483
|
+
limit: this.pollingOptions.limit,
|
|
484
|
+
allowed_updates: this.pollingOptions.allowed_updates,
|
|
485
|
+
signal: this.pollingAbort?.signal
|
|
486
|
+
});
|
|
487
|
+
let processed = 0;
|
|
488
|
+
for (const update of updates) {
|
|
489
|
+
if (update.update_id < this.offset) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
this.offset = Math.max(this.offset, update.update_id + 1);
|
|
493
|
+
await this.processUpdate(update);
|
|
494
|
+
processed += 1;
|
|
495
|
+
}
|
|
496
|
+
if (processed === 0) {
|
|
497
|
+
await sleep(this.pollingOptions.interval ?? 1e3);
|
|
498
|
+
}
|
|
499
|
+
} catch (error) {
|
|
500
|
+
if (!this.polling || isAbortError(error)) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
await this.emit("polling_error", error);
|
|
504
|
+
if (error instanceof RiseonlyError && error.retryAfter) {
|
|
505
|
+
await sleep(error.retryAfter * 1e3);
|
|
506
|
+
} else {
|
|
507
|
+
await sleep(this.pollingOptions.interval ?? 1e3);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
async emit(event, ...args) {
|
|
513
|
+
for (const listener of [...this.listeners[event]]) {
|
|
514
|
+
try {
|
|
515
|
+
await listener(...args);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
await this.emit("error", error);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
function normalizePollingOptions(value) {
|
|
523
|
+
if (!value) {
|
|
524
|
+
return { interval: 1e3, timeout: 30, limit: 100, autoStart: false };
|
|
525
|
+
}
|
|
526
|
+
if (value === true) {
|
|
527
|
+
return { interval: 1e3, timeout: 30, limit: 100, autoStart: true };
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
interval: 1e3,
|
|
531
|
+
timeout: 30,
|
|
532
|
+
limit: 100,
|
|
533
|
+
autoStart: true,
|
|
534
|
+
...value
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function sleep(ms) {
|
|
538
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
539
|
+
}
|
|
540
|
+
function isAbortError(error) {
|
|
541
|
+
return error instanceof Error && error.name === "AbortError";
|
|
542
|
+
}
|
|
543
|
+
function normalizeHeaders(headers) {
|
|
544
|
+
const normalized = {};
|
|
545
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
546
|
+
normalized[key] = value;
|
|
547
|
+
}
|
|
548
|
+
return normalized;
|
|
549
|
+
}
|
|
550
|
+
async function readJsonBody(req) {
|
|
551
|
+
const chunks = [];
|
|
552
|
+
for await (const chunk of req) {
|
|
553
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
554
|
+
}
|
|
555
|
+
if (chunks.length === 0) {
|
|
556
|
+
return {};
|
|
557
|
+
}
|
|
558
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
559
|
+
}
|
|
560
|
+
function respond(res, status, body) {
|
|
561
|
+
res.statusCode = status;
|
|
562
|
+
res.setHeader("Content-Type", "application/json");
|
|
563
|
+
res.end(body === void 0 ? void 0 : JSON.stringify(body));
|
|
564
|
+
}
|
|
565
|
+
var INIT_DATA_HASH_RE = /^[0-9a-f]{64}$/;
|
|
566
|
+
function parseInitData(encoded) {
|
|
567
|
+
const fields = Object.fromEntries(new URLSearchParams(encoded).entries());
|
|
568
|
+
return fields;
|
|
569
|
+
}
|
|
570
|
+
function buildInitDataCheckString(fields) {
|
|
571
|
+
return Object.entries(fields).filter(([key]) => key !== "hash").sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}=${value}`).join("\n");
|
|
572
|
+
}
|
|
573
|
+
function deriveInitDataSecret(botToken) {
|
|
574
|
+
return createHmac("sha256", "WebAppData").update(botToken).digest();
|
|
575
|
+
}
|
|
576
|
+
function signInitData(botToken, dataCheckString) {
|
|
577
|
+
const secret = deriveInitDataSecret(botToken);
|
|
578
|
+
return createHmac("sha256", secret).update(dataCheckString).digest("hex");
|
|
579
|
+
}
|
|
580
|
+
function verifyInitData(botToken, encoded) {
|
|
581
|
+
const fields = parseInitData(encoded);
|
|
582
|
+
const hash = fields.hash;
|
|
583
|
+
if (!hash) {
|
|
584
|
+
throw new Error("init data hash is missing");
|
|
585
|
+
}
|
|
586
|
+
if (!INIT_DATA_HASH_RE.test(hash)) {
|
|
587
|
+
throw new Error("init data signature is invalid");
|
|
588
|
+
}
|
|
589
|
+
const dataCheckString = buildInitDataCheckString(fields);
|
|
590
|
+
const expected = signInitData(botToken, dataCheckString);
|
|
591
|
+
const left = Buffer.from(hash, "hex");
|
|
592
|
+
const right = Buffer.from(expected, "hex");
|
|
593
|
+
if (left.length !== right.length || !timingSafeEqual(left, right)) {
|
|
594
|
+
throw new Error("init data signature is invalid");
|
|
595
|
+
}
|
|
596
|
+
const expiresAt = fields.expires_at ? Number(fields.expires_at) : void 0;
|
|
597
|
+
if (expiresAt !== void 0 && Number.isFinite(expiresAt) && expiresAt < Date.now()) {
|
|
598
|
+
throw new Error("init data has expired");
|
|
599
|
+
}
|
|
600
|
+
return fields;
|
|
601
|
+
}
|
|
602
|
+
function extractInitDataFromLaunchUrl(launchUrl) {
|
|
603
|
+
const url = new URL(launchUrl);
|
|
604
|
+
const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
|
605
|
+
if (!hash) {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
return new URLSearchParams(hash).get("riseonlyInitData");
|
|
609
|
+
}
|
|
610
|
+
function createRequestId() {
|
|
611
|
+
return crypto.randomUUID();
|
|
612
|
+
}
|
|
613
|
+
|
|
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 };
|
|
615
|
+
//# sourceMappingURL=index.js.map
|
|
616
|
+
//# sourceMappingURL=index.js.map
|